| Commit message (Collapse) | Author | Age | Files | Lines |
| ... | |
| | |
| |
| |
| |
| |
| |
| |
| |
| | |
The header reads wallet.spendable (and pendingIncoming), which reduce over
this.utxos only; SP outputs live in this.spUtxos, so received SP value showed in
the Receive card but not the balance. Add confirmed SP to spendable and pending
SP to pendingIncoming.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| | |
The visible spScanning state is now set only for manual scans; auto-scans use a
separate concurrency guard (_spScanBusy). Previously an auto-scan set spScanning
and the final emit fired before it was cleared (and silent scans skipped the
clearing emit), leaving the Receive card stuck on 'Scanning…'.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
The indexer now serves a /ws endpoint and publishes each newly-indexed block's
SP items to subscribers. The wallet subscribes (_spConnect) and scans pushed
items on arrival (_scanSpItems) — folding receipts into the balance instantly —
catching up via /scan on connect/reconnect. Polling for SP is now only a fallback
for when the socket isn't live.
Verified on regtest: connect, mine a block, wallet receives the push.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
scanSilentPayments() now recomputes the balance so received SP value shows in the
wallet total (not just the Receive card), and gains a silent mode. It's triggered
automatically after each full scan and on the poll loop (incremental, cheap), so
payments appear without tapping anything. The Receive card now shows a status
line ('Watching for payments' / found amount) with a small manual Rescan instead
of a primary Scan button.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
buildTx now includes received SP outputs in the coin pool, and sign() spends them
as taproot key-path inputs signed manually with d = spend_priv + t_k (the output
key is used raw — no BIP-341 tweak — so btc-signer's auto key-path signing
doesn't apply; we set tapKeySig from preimageWitnessV1 + schnorr.sign). The fee
estimator is fed a tapInternalKey (sizing only) with disableScriptCheck. SP value
folds into the displayed balance. Paying an sp1… recipient still uses BIP84 coins
(SP-input-funded SP sends need even-Y handling, deferred).
Validated on regtest via testmempoolaccept: a wallet-built, wallet-signed spend
of a received SP output is accepted by Core.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
wallet.js: scanSilentPayments() pulls the indexer's /scan range, matches outputs
on-device, and verifies each is unspent via the chain backend; spUtxos + balance
are tracked in their own bucket (one-time taproot keys), persisted in the cache.
silentPaymentsAvailable()/spBalance() expose state.
app.js: the Receive tab shows the reusable sp address (QR + copy), a 'Scan for
silent payments' button, and the found amount; Settings gains a per-network SP
indexer picker (under Data source) with custom/self-host support.
sp-indexer.js: added /scan/{from} so a client fetches a whole range in one
request instead of one fetch per block.
Receiving verified end-to-end on regtest (scan finds the output, 49,999,800 sat
confirmed). Spending SP outputs is the remaining piece.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
api.js: per-network SP indexer config (presets + custom, stored per network) with
getSpIndexerConfig/spIndexerUrl/spReceiveEnabled — regtest defaults to the local
indexer, others to custom; a hosted mainnet default slots in once we run one.
wallet.js: silentPaymentKeys() derives BIP-352 scan/spend keys from the master
seed (needs the mnemonic / depth-0 xprv), and silentPaymentAddress() returns the
sp1…/tsp1… address. Verified it matches the vector-validated e2e address.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
tools/sp-indexer.js: a minimal BIP-352 oracle (Bun) that walks Core and serves,
per block, one entry per eligible tx — the tweak (input_hash*A) plus that tx's
taproot outputs — for the wallet to scan on-device. Shares src/silentpay.js so
the tweak math is the vector-validated code. Added to the regtest compose as the
'sp' service (oven/bun, pointed at bc:18443).
inputPubKey now accepts a witness as either serialized hex (vectors) or an array
of stack items (Core getblock RPC). Verified end-to-end on regtest: a real SP
payment is indexed and the scan recovers the output, value, and spend tweak.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
silentpay.js gains the receiving half of silent payments, validated against all
official BIP-352 vectors (29/29 receiving sub-cases):
- inputPubKey(): recover the eligible input pubkey from P2PKH/P2WPKH/P2SH-P2WPKH/
P2TR inputs (skips uncompressed keys, excludes NUMS script-path, hash160-matches
malleated P2PKH scriptSigs)
- silentPaymentTweak(): the oracle tweak input_hash*A (sum over eligible inputs,
input hash over all outpoints, point-at-infinity guarded) — used by the indexer
- silentPaymentScan(): ecdh = scan_priv*tweak, derive P_k = B_spend + t_k*G, match
the tx's taproot outputs, return per-output spend tweaks
- silentPaymentOutputPrivKey(), deriveSilentPaymentKeys() (m/352' paths),
encodeSilentPaymentAddress()
Labels not yet implemented (wallet uses a single no-label address).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |\| |
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
On a fresh open, receiveSeenIndex was baselined to the pre-scan frontier (line
691), so the moment the socket's reconcile credited a deposit that predated the
open, nextReceiveIndex advanced past it and the celebration flashed for ~2s until
the post-scan baseline caught up. Leave receiveSeenIndex null until that final
baseline for fresh opens (matching the documented intent); the != null guard then
suppresses the flash. Same-session refreshes still use the persisted ack.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |\| |
|
| | |
| |
| |
| | |
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |\| |
|
| | |
| |
| |
| | |
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |\| |
|
| | |
| |
| |
| |
| |
| |
| |
| | |
Hal stands on its own, so the data-source descriptions shouldn't name coinos:
the Electrum description now says "Point it at your own server … no REST" and the
block-explorer one drops "nothing is sent to coinos".
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |\| |
|
| | |
| |
| |
| |
| |
| |
| | |
The mempool parenthetical is jargon for non-technical users; the status line now
reuses the plain `pending` string. Drop the now-unused pendingInMempool key.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |\|
| |
| |
| |
| | |
# Conflicts:
# src/app.js
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
All pending txs share blockTime 0, so _sortTxs's blockTime comparator returned 0
for every pending pair and they kept array (scan/arrival-append) order — so the
most recently received tx sat at the bottom of the pending group instead of the
top. Order pending txs by firstSeen descending (the timestamp we already record
when a tx is first seen unconfirmed); confirmed txs still sort by block time.
Also sort on cache restore so the order is right immediately, not only after the
first refresh.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
detect
After a socket drop the in-flight _rpcCall requests were never rejected (only
stopRealtime did that), so dead calls held every _rpcPump slot until they timed
out — stalling the post-reconnect refresh by ~10s even though the new socket and
its pushes were already live. Now reject pending calls in ws.onclose and in
_reconnectNow (which nulls onclose), so the pump frees instantly and the on-open
refreshLive runs right away.
Also tighten the half-open watchdog: ping every 10s and recycle the socket after
21s of silence (two unanswered pings) instead of 45s, so a silently-dead
connection — no onclose, no online/visibility event — resumes pushing in ~20s
instead of ~45s. Verified via CDP: force-closing the live socket reconnects in
~0.6s and the next deposit pushes in ~0.4s (UI ~1.2s).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
The recovery-phrase section of the per-wallet settings page now shows an Import
seed action for watch-only wallets, with the load-seed card inline. The load-seed
flow takes an account id (defaulting to the active wallet), so it can upgrade a
specific watch-only wallet — validating the entered seed against that wallet's
xpub before making it spendable.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| | |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| | |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
The global Settings tab no longer shows the recovery phrase / public key — those
live only on each wallet's settings page now (the watch-only 'load seed' upgrade
stays in the global tab since it targets the active wallet). On the per-wallet
page, recovery phrase moves to the top and auto-logout to the bottom. Removes the
now-dead pubkeyCard/autoLockCard. Lowercases 'THIS' in the auto-logout copy.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
The accounts list now shows a settings cog (instead of a rename pencil) that
opens a per-wallet settings page: edit name, set auto-logout (moved here from the
global settings tab), reveal the recovery phrase, and show the public key (zpub)
— all read from the account itself, so it works for any wallet, not just the
active one. Room for more per-wallet settings later.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
The network card is now just the network picker. Each network's endpoints
(Electrum server + block explorer) live solely in the Data source card, whose
options change per network and are stored per network (so switching networks
remembers each one's choice). Removes the duplicate regtest Esplora/WS fields.
Adds mutinynet (signet — testnet address format, explorer at mutinynet.com).
Per-network defaults: mainnet → coinos Electrum, testnet/mutinynet → explorer,
regtest → local Fulcrum. Fixes tx-explorer links to use each network's web base
(no double /testnet).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| | |
The data-source picker is now just Electrum vs Block explorer; coinos is the
first/default entry in the Electrum server list (our own Fulcrum), with public
servers as failover. Drops the standalone 'coinos' mode and the retired watcher
shim. Old 'coinos' selections migrate to electrum.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| | |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
Flip COINOS_ELECTRUM on: the default 'coinos' mode is now a full Electrum backend
served same-origin at wss://halwallet.app/electrum (nginx -> Fulcrum on cs, synced
+ using bitcoind hashblock ZMQ), with the public Electrum servers as automatic
failover. Replaces the explorer-REST-data + coinos-watcher-push setup — one
connection to our own node now serves both data and instant notifications.
Verified the endpoint end-to-end through Cloudflare (server.version + get_balance).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |\| |
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
_rpcId could go non-numeric, so ++ produced NaN and requests serialized with
"id":null. _handleRpc matches responses with `msg.id != null`, so every
data-call response was dropped → the call timed out → _reconcileHeld skipped
the address → confirmations never reflected (until an unrelated full refresh).
Coerce to a number on every send. Verified via CDP: a mined deposit now flips
pending→confirmed in ~1s instead of stalling.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |\| |
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
Regtest now talks to a local Fulcrum over its native Electrum WebSocket for
both chain data and realtime push, instead of esplora REST + a separate watcher.
This matches prod (Fulcrum) and gives ~2-3x lower latency than the previous
electrs polling. getBackend()/electrumCandidates() route regtest to the
Electrum backend at the configured Fulcrum WS (default ws://localhost:50003);
the wallet's realtime path is ungated for regtest. ElectrumApi already covers
every call (utxos, history, fees, broadcast), incl. the swap feature.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |\|
| |
| |
| |
| | |
# Conflicts:
# src/app.js
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
On regtest there's no realtime push (coinos watcher is mainnet-only), so the UI
only updated on the 5–10s poll — a funded tx took a while to appear and a
just-mined block stayed "pending" until the next reconcile. electrs speaks the
Electrum protocol the watcher already understands, but only over TCP; route it
through a small WS<->TCP bridge (see coinos-server/electrum-ws) so the wallet
subscribes scripthashes and reconciles instantly on each notification. Data
still comes from esplora; this only adds the push channel. Configurable in
Settings (default ws://localhost:50003); falls back to polling if unreachable.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
Replaces the single hardcoded Boltz endpoint with a provider selector.
Presets are independent Boltz-compatible instances from SwapMarket's list
(Boltz Exchange, Middle Way, ZEUS Swaps, Eldamar) plus Local (regtest) and a
Custom option; default is Local on regtest, Boltz Exchange otherwise. The
swap-status WebSocket is derived from the API URL (http->ws + /v2/ws) unless
overridden. Swaps remain non-custodial regardless of provider — a provider can
fail a swap but never take funds (hal claims/refunds on-chain).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Non-custodial swaps via Boltz, fully client-side: hal holds the swap key +
preimage and claims (reverse) / refunds (submarine) on-chain via the taproot
script path, so Boltz can never strand funds.
- src/swap.js: tapleaf/tree construction (byte-exact vs boltz-core vectors),
swap P2TR address derivation (MuSig2 internal key + taptweak), script-path
claim/refund tx builder, bolt11 decode, Boltz api-v2 client, and a polling
SwapManager (reverse auto-claim; submarine fund + status + refund).
- wallet.js: deterministic swap-key derivation (dedicated chain) + persistence.
- api.js: configurable Boltz REST/WS endpoints.
- app.js: ⚡ Swap tab — Receive over LN (reverse) + Pay an invoice (submarine).
- tools/swap-*.test.js: vectors, live-address, and end-to-end regtest tests.
Verified end-to-end on regtest against a local Boltz wired to coinos's bc +
a dedicated CLN/hold node.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
| |
New `regtest` network (bech32 `bcrt`, testnet version bytes) selectable in
Settings, pointed at a configurable local Esplora (default http://localhost:3000).
Adds getNetwork/setNetwork + getRegtestEsplora persistence, a regtest branch in
resolveHosts, mainnet-only gating for the realtime watcher + Electrum backend,
and a Network selector card. Account loading reads the active network.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
The light frontier poll walks past already-used addresses, and _reconcileHeld
refreshed held-coin balances but not the history tx summaries — so a
pending->confirmed transition only showed via the realtime WebSocket push.
Backends without that push (custom electrs/esplora, regtest) left the History
row stuck on "pending" even after the tx confirmed. Re-fetch and update tx
summaries for changed held addresses during reconcile.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
The Electrum data path fired every request at once, so a scan burst dozens of
calls at a rate-limited public server, which then dropped some (the timeouts).
Add a concurrency cap (4 in flight; Electrum pipelines, so it stays fast) with
excess queued — mirrors the Esplora scheduler's intent. Verified a full scan via
the public jochen server completes in ~4.5s with peak concurrency 4, no timeouts.
The throttle queue is drained on disconnect/stop alongside in-flight calls.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two Electrum-backend issues:
1. Stale balance after a send made on another device: _reconcileHeld set each
address's balance from get_balance/address-stats, where a confirmed coin being
spent by an unconfirmed tx STILL counts as confirmed (with a compensating
negative unconfirmed). So a device that didn't make the send kept showing the
spent coin's value. Now the entry balance is derived from the actual unspent
set (listunspent, which excludes spent coins) — verified a spent coin clears.
2. Per-address rescan failing with 'electrum timeout: blockchain.scripthash.
get_balance': public Electrum servers can drop a response or the socket can
flap mid-call. _rpcCall now retries a couple of times on a fresh id (12s each)
before giving up.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
On the Electrum backend the post-send rescan is many round-trips, so the balance
lagged a few seconds. Now a successful broadcast immediately applies the tx
locally (applySentTx): drop the spent coins, credit change as pending, add a
pending history row, recompute the balance, and emit — so the UI reflects the
send instantly. The realtime watcher / backstop poll reconciles and fills in the
confirmation once the explorer has indexed it (no immediate rescan, so a
not-yet-indexed tx can't briefly revert the optimistic state). Verified the
balance/utxo/history math.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The Esplora broadcast cleared its abort timer right after fetch() resolved, so
the subsequent res.text() body read had no timeout — a stalled response body
hung the whole broadcast (and the send spinner) indefinitely, with the tx never
reaching the network. Keep the abort timer active through the body read, and add
a hard 30s outer timeout in the send handler so a wedged broadcast always surfaces
as 'timed out — try again' instead of spinning forever. (The silent-payment tx
itself is valid — verified structure + BIP143 signature over the real outputs.)
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
| |
broadcast() set the success result but kept ui.busy=true and didn't render until
after awaiting wallet.scan(), so the 'Sign & broadcast' button spun for the whole
post-send rescan (slow under rate-limiting / on the Electrum backend) — looking
stuck even though the tx had already broadcast. Now it shows the success screen
the instant the broadcast resolves and refreshes balances in the background.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
| |
The ✕ on a wallet now opens a confirmation screen warning it's removed from this
device (keep its recovery phrase; other wallets unaffected) instead of deleting
immediately.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
- goHome: when a wallet is open, the logo goes to its Receive page (its tabs)
instead of the create/import screen — so it's always a way back to your wallet.
Only with no wallet open does it show create/import.
- Paste buttons: a clipboard 'Paste' icon on the send address field and the
import (seed/key/xpub) and 'load seed to spend' fields, filling them from the
clipboard. Hidden where the Clipboard read API isn't available.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
A vault exists → Accounts screen shows Change password: enter the current
password, then a new one (with confirm; leave blank to remove it). It decrypts
the actual vault and re-encrypts it with the new password, so wallets not loaded
in the current session are never dropped. Keeps the in-memory password in sync
when the vault is already unlocked. Verified the re-encrypt round-trip
(old rejected, new works, blank works).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The logout timer is now an account setting (stored per account in the session and
the durable directory), not a global one, and it removes only that wallet from
the device — its session entry, directory entry, cached state (xpub + seed keys),
and its vault entry — leaving other wallets untouched.
- wipeAccount(id) surgically removes one wallet; the active one falls back to the
next account (or the unlock screen).
- Background-away timestamp drives per-account evaluation on resume/boot; an armed
timer fires for the soonest-due wallet so a blurred-but-visible window still
signs out on time. Overdue saved wallets are removed from the vault when it's
unlocked (the only point we can re-encrypt it).
- New cacheKeyFor() export lets app code target one wallet's cache keys exactly
(verified against the wallet's seed/xpub/watch keys).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|