aboutsummaryrefslogtreecommitdiffstats
path: root/src/wallet.js
Commit message (Collapse)AuthorAgeFilesLines
...
| * Per-network data source; drop duplicate regtest fields; add mutinynetAdam Soltys2026-06-271-6/+9
| | | | | | | | | | | | | | | | | | | | | | | | | | | | 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]>
* | Merge branch 'master' into boltz-swapsAdam Soltys2026-06-271-2/+5
|\|
| * Fix electrum reconcile stall: keep _rpcId numeric (id:null broke matching)Adam Soltys2026-06-271-2/+5
| | | | | | | | | | | | | | | | | | | | | | _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]>
* | Merge branch 'master' into boltz-swapsAdam Soltys2026-06-271-2/+2
|\|
| * Use Fulcrum (Electrum/WS) for regtest data + push, dropping esploraAdam Soltys2026-06-271-2/+2
| | | | | | | | | | | | | | | | | | | | | | | | 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]>
* | Add Boltz submarine + reverse swaps (Lightning <-> on-chain)Adam Soltys2026-06-271-0/+20
|/ | | | | | | | | | | | | | | | | | | | 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]>
* Add regtest network supportAdam Soltys2026-06-271-2/+6
| | | | | | | | | | 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]>
* Refresh tx confirmations on poll so non-WS backends don't show stale pendingAdam Soltys2026-06-271-0/+11
| | | | | | | | | | | 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]>
* Throttle Electrum requests so scans don't overwhelm public serversAdam Soltys2026-06-261-7/+32
| | | | | | | | | | | 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]>
* Fix stale cross-device balance + Electrum call timeoutsAdam Soltys2026-06-261-16/+17
| | | | | | | | | | | | | | | | | | 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]>
* Update balance/history optimistically right after a sendAdam Soltys2026-06-261-0/+52
| | | | | | | | | | | | | 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]>
* Make auto log-out per-wallet — only signs out the wallet it's set forAdam Soltys2026-06-261-0/+7
| | | | | | | | | | | | | | | | | | 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]>
* Persist wallets as watch-only across sessions; re-enter seed to spendAdam Soltys2026-06-261-1/+30
| | | | | | | | | | | | | | | | | | | | | | | Closing the browser without 'Save to device' used to wipe the wallet entirely. Now every wallet's account xpub is mirrored to a durable localStorage directory (never the seed), so it survives a session wipe as a watch-only entry — you keep seeing balance and history, and re-enter the seed to spend again. - Durable account directory: saveDirectory() mirrors {id,label,xpub} for every account; restore falls back to it as watch-only when there's no session/vault. - Instant view: a full wallet also caches under its xpub key, so the downgraded watch-only shows balance/history immediately (no rescan). Verified round-trip. - Re-enter seed to spend: the Send tab (now shown for watch-only) and Settings prompt for the recovery phrase / xprv; it must derive to this wallet's xpub (verified via accountXpubFor — wrong seed/passphrase rejected), with an option to Save to device at the same time. Upgrade is in place (id/label/cache kept). - Vault merge now dedupes by xpub so saved + non-saved wallets coexist without duplicates. Privacy note: the xpub now persists by default (someone with device access can see balance/history without the seed); 'Remove' clears it. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Stage Electrum failover + coinos-primary cutover switchAdam Soltys2026-06-261-2/+17
| | | | | | | | | | | | | | | | | Failover (live now): the Electrum backend now tries an ordered list of servers, advancing to the next when one fails to connect or dies within ~5s. Named public presets fall over to the other public servers; a custom your-node URL does NOT fall over (won't silently leak your addresses to a server you didn't pick). Verified: dead primary → auto-advances to a working server. Cutover switch (dormant): COINOS_ELECTRUM (false) flips 'coinos' mode from explorer-REST + watcher push to a full Electrum backend pointed at wss://coinos.io/electrum, with the public servers as failover, and disables the watcher for coinos mode. Flip it true once Fulcrum serves data behind that endpoint — no new logic needed at cutover. Flag off = current behavior exactly (default coinos → esplora + watcher), so this ships safely while Fulcrum syncs. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Add Electrum-over-WebSocket as an alternative data backendAdam Soltys2026-06-261-22/+91
| | | | | | | | | | | | | | | | | | | | | | | | | | Settings → Data source now offers 'Electrum' alongside the Esplora REST explorer. One Electrum WebSocket serves BOTH the wallet's data queries and scripthash push notifications, so pointing Hal at your own Fulcrum/electrs gives a fully self-hosted setup — no coinos, no public REST, nothing shared off your node. - src/electrum.js: ElectrumApi maps the Esplora shapes the wallet consumes onto Electrum methods (scripthash get_balance/listunspent/get_history, estimatefee, transaction.broadcast). electrs has no verbose tx lookup, so it parses raw txs and resolves input values from parent txs to compute fees (cached, in-flight deduped, bounded-parallel). Validated field-for-field against mempool.space: addressTxs (fee/vin/vout/status), getTx, outspend, balance, utxos. - wallet.js: the realtime socket already spoke Electrum's notify protocol; added request/response (_rpcCall) so the same connection answers data calls. Real Electrum notifications carry no tx data, so they reconcile via the backend. Replaced the fragile newline parser with a string-aware brace-depth scanner that handles every framing (newline, one-per-frame, concatenated, split) — fixes a buffer-poisoning hang under concurrent calls; also hardens the coinos watcher path. Esplora stays the default; nothing changes for existing users. Verified end-to-end against blockstream's electrs: concurrent transport, a full fresh-wallet scan (~2s), and the parser unit-tested on all framings. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Add Silent Payments (BIP-352) sendingAdam Soltys2026-06-251-7/+66
| | | | | | | | | | | | | | | | | Pay sp1… addresses: the wallet derives the one-time taproot output from the recipient's address + the transaction's own input keys (ECDH), so payments are unlinkable on-chain with no interaction. New src/silentpay.js implements the sender derivation + bech32m address decode, validated against the official BIP-352 test vectors (all P2WPKH-input / single-recipient cases pass; the rest exercise input types Hal never spends). buildTx detects sp1… recipients, selects coins against a same-size placeholder taproot output, then swaps in the script derived from the final input set (updateOutput). Works for normal sends, multi-recipient, and send-max. The review screen shows the sp1… address with a 'Silent payment' label (the on-chain output is a derived one-time taproot address). End-to-end signing verified. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Celebrate a received payment once; don't re-show after refresh/reopenAdam Soltys2026-06-251-3/+3
| | | | | | | | Persist the receive-ack the moment the 'Payment received!' screen is shown (not only on tap), so refreshing or navigating away without tapping won't bring it back. It still stays visible in the current session until dismissed. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Compact gift links: base32 codec + uppercase /g/ path for a smaller QRAdam Soltys2026-06-251-5/+65
| | | | | | | | | | | | | | | | | | Gift links now carry a compact binary encoding of just the claim-essential data (presigned inputs + the committed change output) instead of a full PSBT (~25% fewer bytes), base32-encoded and served at an uppercase /g/<code> path. The QR renders the uppercase URL in the QR alphanumeric mode (5.5 bits/char vs byte mode's 8), so the grid drops from ~73 to ~53 modules — much easier to scan. - wallet: _encodeCompactGift/_decodeCompactGift + _giftTx dispatcher; legacy base64url-PSBT links still claim (auto-detected by a magic byte). Verified round-trip + claim-byte-identical across 1/2/3-input and whole-balance gifts, against the real exported preview/claim/outpoint functions. - app: giftUrl uses /g/<code>; opener parses /g/ (and legacy #gift=); QR uses uppercase + Alphanumeric mode (qrSvg gains a mode option w/ byte fallback). - nginx already SPA-routes /g/*; the SW serves the shell for the new path. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Gift claim: detect already-claimed (even unconfirmed) and block the raceAdam Soltys2026-06-251-0/+16
| | | | | | | | | | | | Opening a gift link now checks whether its funding coin is already spent — via the Esplora outspend endpoint, which reports a spend even by an unconfirmed claim. The Claim button is gated until that check passes; if the coin is spent, show an 'Already claimed' screen with a link to the claim tx instead of a claimable amount. doClaim also re-checks on broadcast failure (the race window), and refuses to run while unchecked/taken — so a second opener can't attempt a doomed double-spend. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Confirm transactions via the WebSocket push, not RESTAdam Soltys2026-06-251-37/+46
| | | | | | | | | | The watcher's block (rawblock) push now carries confirmed=true + the block time, and _applyTxNotification flips the matching history row and coins from pending to confirmed directly — for incoming and outgoing txs (flag-only, so no overcount). Confirmations no longer wait on a mempool.space reconcile; the 30s backstop poll just fills in block height / fee afterward. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Credit payments instantly from the watcher push (no REST on critical path)Adam Soltys2026-06-251-23/+89
| | | | | | | | | | | | | | | | | | The watcher push now carries the tx data (matched outputs + confirmed flag), so the app credits an incoming payment the moment the push arrives — no mempool.space round-trip. This removes the rate-limited REST reconcile from the path a user actually waits on, and lets us back the frontier poll way off when the socket is live (30s backstop vs the old 3s receive-tab hammer), cutting the 429s to a trickle. - watcher: notification includes { txid, confirmed, outputs:[{vout,value}] }. - wallet: _applyTxNotification credits the receive coin + a pending history row optimistically (receives only; change reconciles in the background), advances the frontier, updates the balance, and schedules a background reconcile for full accuracy off the critical path. - poll intervals relaxed now that the WS does the real work instantly. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Realtime via self-hosted ZMQ watcher (Electrum-over-WS)Adam Soltys2026-06-251-104/+76
| | | | | | | | | | | | | | | | | | | Replace the unreliable public WebSocket push (mempool/blockchain.info address subscriptions never reliably fire) with a private notification service: - tools/zmq-watcher.js: a standalone Bun service (reusing coinos lib/zmq.ts's ZMTP subscriber) that reads Bitcoin Core's ZMQ rawtx/rawblock, computes the scripthash of each output, and pushes a blockchain.scripthash.subscribe notification to subscribed clients. No historical index (~24MB RAM) — just the live tx stream. Runs on the coinos box reading its Core's ZMQ. - wallet.js: realtime client now speaks the Electrum protocol (server.version, scripthash.subscribe, server.ping) over the WS; any scripthash notification triggers the existing refreshLive() (REST stays the source of truth). - api.js: wsUrl -> wss://coinos.io/electrum. Verified end-to-end: live mempool txs to subscribed addresses notify instantly. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Fast-poll the receive address while watching for a paymentAdam Soltys2026-06-251-1/+38
| | | | | | | | | | | The WS badge reads Live but mempool.space's free address tracking doesn't reliably push incoming mempool txs, so a deposit could take ~18s to appear (the 25s safety poll catching it). While the user is on the Receive tab (online), now poll just the fresh receive address every ~3s regardless of Live state — one cheap request that guarantees near-instant arrival. Toggled idempotently from render() based on the active tab; stops when they navigate away. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Faster WS reconnect; stop double-running the held-coin reconcileAdam Soltys2026-06-251-1/+9
| | | | | | | | | | | | - _scheduleReconnect now backs off exponentially from ~0.6s (was a flat 4s), so a first socket attempt that's transiently rejected retries fast — the wallet reaches Live in well under a second instead of ~4s. Backoff resets on open. - refreshLive now stamps _lastPoll at its start, so a socket-open refresh and the poll-timer tick don't both fire reconcileHeld back-to-back on load. A cached load does one held-coin reconcile (verify coins still unspent + frontier check) instead of ~3x. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Detect stuck low-fee txs and stop actively monitoring themAdam Soltys2026-06-241-3/+29
| | | | | | | | | | | | | | | A pending tx is judged 'stuck' once it has waited ~10 blocks (≈100 min) and its fee rate (fee/vsize) is below the mempool's current floor (minimumFee) — i.e. it's been purged and won't confirm without a bump. _txSummary now records vsize and a firstSeen timestamp (persisted in cache) to support this. Stuck txs are dropped from active monitoring: excluded from the watched-address set and from the fast-poll trigger, so they don't hold a socket slot or keep us in the 6s lane. The slow ~25s reconcile still catches one if the floor later drops and it confirms. History flags them (⚠ stuck) and the detail view explains it — pointing outgoing ones at the existing fee bump. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Watch pending-coin addresses; let per-address rescans queueAdam Soltys2026-06-241-13/+20
| | | | | | | | | | | | | | - watchedAddresses() now includes any address holding an unconfirmed coin (not just the frontier), so a pending deposit's confirmation pushes over the socket instantly. Capped at the socket's 10-address limit. refreshLive/rescanAddress retrack when a new pending coin changes the set. - While any coin is pending, the frontier poll runs every ~6s even when live, so confirmations show fast as a belt-and-suspenders to the WS push. - Per-address rescans no longer disable each other: clicking many queues them, and the api.js global scheduler (1 concurrent, 500ms spacing, 429 backoff) serializes and staggers the underlying requests within the rate limit. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Drop background/full rescans for on-demand per-address rescanAdam Soltys2026-06-241-9/+58
| | | | | | | | | | | | | | | Remove the periodic full re-scan entirely — the WS and the frontier poll keep the balance current, and refreshLive's _reconcileHeld catches spends of held coins. The only gap (a deposit to a reused old address) is now handled explicitly: Settings → Rescan opens a paginated list of every known address, each with its own Rescan button (wallet.rescanAddress re-queries just that one). Replaces the full 'Rescan wallet' button. This sharply cuts recurring REST volume (the main rate-limit driver). Trade-off: when the socket is down, a pending tx's confirmed badge in History may lag until the WS reconnects or you rescan — the balance itself stays correct via reconcile. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Make pending receives fast: cut REST load, surface live/polling stateAdam Soltys2026-06-241-7/+24
| | | | | | | | | | | | | | | | | | | Incoming deposits show instantly when the mempool.space WebSocket is healthy (_ingestWs updates immediately). The lag came from the socket being starved: a 20s frontier poll plus a full wallet re-scan every 120s generated enough REST volume to trip mempool's per-IP rate limit (the 429/CORS errors), which in turn degraded the socket — a feedback loop. - While the socket is live, skip the periodic full scan entirely (the WS pushes confirmations and block events) and poll the frontier only every ~25s as a half-open-socket safety net — near-zero recurring REST. - While the socket is down, poll the frontier every ~6s (was 20s) so receives still show quickly, and keep the 120s reconcile scan. - Refresh the frontier on WS (re)connect to catch deposits that landed while disconnected. - Surface a Live/Polling badge on the balance card so a stuck socket is visible. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Allow gifting the whole balance (no-change sweep) via MaxAdam Soltys2026-06-241-15/+36
| | | | | | | | | | | | A gift PSBT commits a sender change output (>= dust), so the entered amount always needed gift + 294 — making it impossible to gift the entire balance. Add a Max toggle on the gift form that builds a no-change sweep: all inputs SIGHASH_NONE, no change output, so the recipient gets the whole balance minus their claim fee. previewGift/buildClaimTx now treat a zero-output PSBT as zero change. A specific-amount gift that can't keep a dust change now points the user to Max instead of failing with a confusing 'not enough funds'. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Balance, claim button, and gift-recipient backup verificationAdam Soltys2026-06-241-2/+16
| | | | | | | | | | | | - Spendable headline no longer counts pending incoming receives: compute it from UTXOs (confirmed coins + our own unconfirmed change, minus gift locks) so a pending spend still debits immediately but a pending receive shows only on the PENDING line (new pendingIncoming getter). - Claim button shows the fee in parens in slightly smaller font, no dot. - Gift recipients now get the same "Verify backup" / "Skip verification" flow as new-wallet creation instead of a single "I've written it down" button. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Instant gift-via-split and spend-unconfirmed supportAdam Soltys2026-06-241-23/+83
| | | | | | | | | | | | | | | | Gift split no longer makes the sender wait for confirmation: createGiftFromSplit broadcasts the carve-out, waits only until it's visible in the mempool, then builds the gift spending that still-unconfirmed coin. Funding + claim form a chain that confirms together; the recipient sees a pending balance until then. Polls for mempool visibility before reserving so a background refresh can't drop a not-yet-indexed coin and prune the reservation (double-spend risk). Regular sends can already draw on unconfirmed coins; surface that honestly: a 'pending' tag on unconfirmed coins in coin control, and a plain-language warning at review when a payment spends unconfirmed bitcoin (won't confirm until the ancestor does). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Minimize gift lock-up: best-fit coins + optional pre-splitAdam Soltys2026-06-241-17/+47
| | | | | | | | | | | | | | Gift coin selection now picks the smallest single coin that covers the gift (was largest-first), locking the least value until claim. When no near-exact coin exists, offer to pre-split: broadcast a self-send carving out a gift+dust coin so the gift later locks only ~the gift amount. The split is only offered when the liquidity it frees (lock - gift - dust) exceeds the split's own fee, so it never costs more than it regains. Decision is surfaced as an interstitial on Create, since a full re-render drops input focus and a live preview isn't viable. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Deduct gift-locked coins from the spendable balanceAdam Soltys2026-06-241-0/+13
| | | | | | | | | Coins reserved behind unclaimed gift links were already excluded from spending, but the headline balance still counted them. Show the spendable balance (total minus gift locks) as the headline and surface the locked amount as its own line, so a gift debits immediately rather than at claim. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Gift links: stop pre-reserving fee, show it at claim timeAdam Soltys2026-06-241-9/+11
| | | | | | | | | | | | | | Previously createGift reserved a claim-fee headroom on top of the entered amount based on the fee rate at creation. Because the fee was only actually charged at claim time, fee-rate drift between the two moments silently ate into (or padded) the recipient's amount. Now the entered amount is exactly the recipient's full received total (sum of inputs less our change); no fee is reserved. The claim screen shows that full total as the headline and surfaces the live network-fee estimate on the Claim button, where it's deducted from the recipient's balance at claim time. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Add dark mode; gift amount unit toggle; multi-input giftsAdam Soltys2026-06-241-22/+36
| | | | | | | | | | | | | | - Dark mode: theme variables + a footer "Dark mode/Light mode" toggle, persisted and applied pre-paint via a small head script (defaults to the OS preference). Hardcoded surfaces moved to CSS variables. - Gift amount: the unit label is now the clickable sats/BTC toggle, and the amount is parsed in the selected unit. - Gifts can now span multiple coins: createGift selects several confirmed coins (first input SIGHASH_SINGLE to lock the change, the rest SIGHASH_NONE), so a gift larger than any single coin works. Claim/preview sum all inputs and size the fee by input count. Verified end-to-end. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Fold watch-only into Import; support xprv spending importAdam Soltys2026-06-241-27/+40
| | | | | | | | | | | | | | | | | | Home screen is back to two tabs (Create / Import). The Import box now takes a recovery phrase, an xpub/zpub, or an xprv/zprv: - mnemonic → full wallet (as before) - xpub/zpub → watch-only (view/receive, no Send) - xprv/zprv → full spending wallet with no mnemonic. parseExtendedKey classifies + normalizes version bytes; account() derives the BIP84 path from a master xprv (depth 0) or uses an account-level xprv as-is; signing works (only watch-only blocks it). Vault persists xprv accounts too; Settings shows an "Imported key" note instead of a recovery card and hides Nostr sync (no seed). Public-key export still works. Note: a seed phrase cannot be derived from an xprv (one-way KDF), so an xprv wallet has no recovery words by design. New strings in all 19 locales. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Fix confirmed txs stuck showing "pending"Adam Soltys2026-06-241-2/+11
| | | | | | | | | | | | | | | | Two paths left a tx's confirmed flag stale once the balance had already reconciled: - The frontier poll re-fetched an address's txs but only appended new ones (`if (!txs.some(...))`), so a tx that moved from mempool to a block kept its old pending status. Now it refreshes the record in place. - On reload, scan() skipped the history re-pull when the cached balance already matched on-chain, so a stale-pending cached tx never updated. scan() now also re-pulls history whenever any tx is still pending. Verified: a cached pending tx that is confirmed on-chain flips to confirmed (and pending balance clears) after a scan. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Add optional password-encrypted persistence for full accountsAdam Soltys2026-06-241-0/+23
| | | | | | | | | | | | | Per-account "Save on this device" encrypts a seed account into a localStorage vault (scrypt KDF + XChaCha20-Poly1305 via @noble), so it survives a browser close. On reopen, a vault prompt unlocks all saved wallets with the password (or "Use another wallet" to skip). Default stays session-only; the password is held only in memory for the session and re-encrypts on save/forget. Wrong password fails closed (AEAD). Adds @noble/ciphers dependency. New strings translated across all 19 locales. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Add account switching and watch-only walletsAdam Soltys2026-06-241-4/+58
| | | | | | | | | | | | | | | | | | | | | | Accounts: a switcher (header chip → Accounts screen) lets you keep several wallets loaded and flip between them without re-importing. Per the chosen model, full (seed) accounts live only in sessionStorage — ephemeral, no seed on disk; watch-only accounts (no seed) are persisted in localStorage and reload across restarts. enterWallet is refactored around an accounts list (add/switch/remove/lock); Add wallet reaches the unlock screen with a back button. Watch-only: load any wallet from its account xpub/zpub (parseAccountKey normalizes zpub→xpub; the existing public-derivation path yields the same addresses). Signing is blocked, the Send tab and seed/sync/offline cards are hidden, and a watch-only note is shown. Settings gains a "Public key" card (export this wallet's zpub) so it can be watched read-only elsewhere. Password-encrypted persistence for full accounts is the planned next step. New strings translated across all 19 locales. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Fee-aware gift minimum; move gift entrypoint to Send pageAdam Soltys2026-06-241-1/+11
| | | | | | | | | | | | | | | | | | Minimum: the flat 546-sat floor is replaced by a fee-aware minimum (dust + one claim fee of headroom, floored at 546) via giftMinimum(), so the recipient clears dust comfortably even if fees rise before they claim — e.g. ~1000 sats at 5 sat/vB, ~7300 at 50. The create form shows the live minimum and rejects below it; buildClaimTx keeps the hard 294-sat dust guard as a final backstop (claim fails rather than handing over dust). UI: the gift entrypoint moves from Settings to a "Gift some Bitcoin" link on the Send page, opening the gift view as a send sub-view with a back button. New strings translated across all 19 locales. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Keep reclaimed gifts revocable until the coin is spentAdam Soltys2026-06-241-20/+46
| | | | | | | | | | | | Passive reclaim previously dropped the coin from tracking, so there was no way to revoke afterward. Now reclaim moves the coin from "reserved" to a "reclaimed" set: it becomes spendable, but its still-live link stays listed (tagged "still claimable") with a Revoke option, until the coin is actually spent (by a payment or by revoking), at which point it drops off. revokeGift and outstandingGifts handle both sets; existing reserved gifts are unaffected. Strings updated across all 19 locales. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Make gift Reclaim a real on-chain revokeAdam Soltys2026-06-241-0/+11
| | | | | | | | | | | | The old "Reclaim" only un-reserved the coin locally, so the gift link stayed claimable. Replace it with "Revoke": a confirmed action that sweeps the reserved coin back into the sender's own wallet at a high fee (wallet.revokeGift), double-spending the gift's presigned input so the link can't be claimed once it confirms. Races a simultaneous claimer; if the gift was already claimed the broadcast just fails. New strings translated across all 19 locales. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Replace seed gift links with presigned PSBT claim linksAdam Soltys2026-06-241-17/+91
| | | | | | | | | | | | | | | | | | | | | | Gifting no longer shares a seed. "Gift some Bitcoin" (Settings) presigns a chosen amount as a #gift= link: one SIGHASH_SINGLE input (the sender's change output is cryptographically fixed, so a front-runner can only take the gift portion, never the change) plus the rest left open. The coin is reserved per-wallet so normal sends skip it; reclaim it anytime. Opening the link generates a brand-new wallet the sender never sees, shows the amount, and on Claim adds the claimer's address as the output, finalizes, and broadcasts — then prompts backup of the fresh seed. This gives forward secrecy (sender locked out once confirmed), one-shot semantics (claiming spends the input), and keeps funds in the sender's wallet until claimed. Crypto verified end-to-end (sign NONE/SINGLE -> add output -> finalize -> valid tx; deterministic-signature equality proves the presigned sig is valid for any output the claimer picks). Strings retranslated across all 19 locales. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Add shareable gift links for fast onboardingAdam Soltys2026-06-241-2/+23
| | | | | | | | | | | | | | | | Create a #claim=<code> link from Settings → "Gift this wallet" (gated behind a reveal + warning, with a QR): the seed (and passphrase, if any) are encoded as BIP39 entropy in base64url in the URL fragment, which never reaches a server. Opening such a link loads that wallet and shows a welcome → balance → firm "back up your recovery phrase" flow, then drops into the wallet. The claim code is scrubbed from the address bar on load. Bearer by design — whoever opens the link controls the funds — so the creator UI warns to keep amounts small. New strings translated into all 19 locales. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Add fee-rate picker to RBF bumpAdam Soltys2026-06-241-31/+45
| | | | | | | | | | | Bump now opens its own screen with the Economy/Normal/Priority/Custom fee tiers (defaults to Priority) and a live "fee: old → new" preview as you change the rate, then Replace builds + signs + broadcasts. Refactor bumpTx into prepareBump (async fetch/reconstruct) + planBump (pure preview) + buildBump (sign), so the picker can preview each rate without rebuilding. New bumpInsufficient string translated into all 19 locales. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Add RBF fee bumping for unconfirmed sendsAdam Soltys2026-06-241-0/+77
| | | | | | | | | | | | | | | | | | - Signal opt-in RBF (BIP125, sequence 0xfffffffd) on all new sends so they're replaceable. - "Bump fee" on an unconfirmed sent tx (its detail view): rebuilds the same inputs + recipient outputs at the Priority rate, taking the extra fee from the change output (pulling in a confirmed coin if change can't cover it), re-signs, and routes to a "Replace with a higher fee?" confirmation showing old → new fee before broadcasting. - New keys translated into all 19 locales. Honest scope: P2A/TRUC/package-relay are L2/pre-signed-tx + node-relay features that don't apply to a single-sig wallet broadcasting single txs via public explorers, so they're intentionally out. CPFP (for receives / unbumpable sends) is a possible follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Choose your own block explorer; flatten How it worksAdam Soltys2026-06-241-1/+16
| | | | | | | | | | | | | | | - Settings → Block explorer: pick mempool.space (default, with blockstream.info as a silent failover), blockstream.info only, or a custom Esplora/electrs REST URL (e.g. your own node). The realtime WebSocket only applies to mempool.space; other explorers fall back to polling. Switching rebuilds the API and rescans. Replaces the old undocumented explorer-proxy hack. - How it works: merge the "For the technically curious" disclosure into the main body so it's all visible under "The basics", and dedupe the overlapping no-server/offline sentences. Mentions the new explorer setting. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Configurable Nostr sync; fully reconcile wallet state on loadAdam Soltys2026-06-241-9/+24
| | | | | | | | | | | | | | | | | | | | | Device sync: - Add a "Device sync" settings card: toggle (on by default) + editable relay list (defaults to wss://relay.coinos.io). When off, the wallet never contacts a relay. nostr.js is now multi-relay and reads an enabled/relays preference from localStorage. Reconciliation (fixes a stray "payment received" + stale history on import, especially via the Nostr/cache load path): - Always full-scan on load instead of a frontier-only refreshLive, so balance, history confirmations and spends are reconciled and the celebration baselines against everything that already exists (cache/ Nostr still shows instantly first). - Actually start the periodic full-scan safety net (_deepTimer, 2 min); it was declared but never wired up. - WS pushes now refresh an existing tx's pending→confirmed status, not only add new transactions. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Reconcile spends of held coins on refreshAdam Soltys2026-06-231-0/+58
| | | | | | | | | | | | refreshLive only walked the frontier forward, so a coin spent from an already-scanned address never reconciled — the balance stayed stale until a full rescan, even after the spend confirmed (e.g. a tx broadcast from another device). Add _reconcileHeld: re-verify every address that currently holds a UTXO and rebuild its balance/UTXOs from the explorer, catching spends whether pending or confirmed. Runs in the refreshLive path (load, poll, WS), so a refresh now self-corrects. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Add QR scanner to the send page (lazy-loaded decoder)Adam Soltys2026-06-231-0/+19
| | | | | | | | | | | | | | | | | | | Scan a Bitcoin address, a BIP21 URI (fills address + amount), or a signed-transaction QR (opens a broadcast confirmation, then relays it) from a viewfinder button on the Send form. Decoder strategy keeps the main bundle small: - Prefer the native BarcodeDetector (Chrome desktop/Android) — no lib. - Otherwise lazy-load jsQR from a separate dist/jsqr.js, only when the scanner is opened. jsQR stays out of index.html (~424 KB vs ~551 KB) and off the initial load. - The service worker precaches jsqr.js so offline scanning works for installed PWAs on browsers without BarcodeDetector. The scanner needs a camera + secure context, so it never shows on a standalone file:// page; offline air-gapped signing is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>