aboutsummaryrefslogtreecommitdiffstats
path: root/src/wallet.js
Commit message (Collapse)AuthorAgeFilesLines
* 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]>
* Add How it works page, plainer copy, faster import scanAdam Soltys2026-06-191-14/+52
| | | | | | | | | | | | | | | | | | | | | | | | | | | | UI / copy: - Footer: "made by Adam Soltys" (drop the heart) + a "How it works" link on its own line, matching the footer's size. - New "How it works" page: plain-language basics plus an expandable "for the technically curious" section. Routed via ui.screen, with Back/logo returning to wherever you opened it from. - Scrub jargon from the main UI for newcomers: drop BIP39, soften "raw hex" -> "code", "txid" -> "transaction ID", "air-gapped" -> "offline"; replace em dashes in the intro/keys notes. The technical terms now live only in the How it works technical section. Import / scan performance (fewer requests -> fewer 429s, faster): - Skip the /utxo fetch for fully-settled, zero-balance addresses (they can't hold UTXOs) -- the bulk of a history-heavy wallet. - Show balance as soon as the address scan finishes, then load UTXOs and history in the background. - Probe the receive and change chains concurrently so their per-hit delays overlap. - History fills in progressively as each address resolves, with a loading indicator on the History tab. i18n.js is now tracked; new English keys fall back to English for the other languages until translated. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Show pending txs in history with a clear unconfirmed indicatorAdam Soltys2026-06-161-0/+13
| | | | | | | | | WS-detected deposits now get added to history (previously the ingest advanced past the address so the tx never entered history — balance showed but History was empty). Pending txs render with a "⏳ pending" badge, an orange-tinted row, and "awaiting confirmation" so they're clearly distinct from confirmed ones. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Persist payment-received ack across reloads; footer + READMEAdam Soltys2026-06-161-0/+18
| | | | | | | | | | | - The "Payment received!" celebration now persists: the acknowledged fresh index is stored (per seed) in localStorage, so a received payment keeps showing the dialog until you tap past it, even after a refresh. - Footer: only "GitHub" is the link now. - README rewritten for the current state (mainnet-only, realtime, Nostr sync, multi-recipient, sats default, explorer resilience, etc.). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Advance fresh address instantly on WS depositAdam Soltys2026-06-161-3/+18
| | | | | | | | | The pending balance showed immediately from the WS ingest, but the next receive address/QR only appeared after the debounced refreshLive. Now the WS ingest marks the receiving address used and advances the fresh index right away, so the new address shows in the same update as the balance. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Incremental frontier refresh (no full re-scan on deposit)Adam Soltys2026-06-151-46/+97
| | | | | | | | | | refreshLive escalated to a full scan() when the fresh address changed, which re-queried every coin's /utxo and /txs. Rewrite it to walk the frontier incrementally: advance through newly-used addresses, fetching /utxo + /txs for only those, and never revisit already-scanned coin addresses. WS events use the same path. A new deposit now costs ~5 requests instead of a full re-scan. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Watch only the fresh receive + change address over the WSAdam Soltys2026-06-151-7/+9
| | | | | | | | | | mempool's track-addresses caps at 10 per connection; we were sending all coin addresses + a 15-wide frontier, so the subscription was rejected and realtime updates silently stopped. With the frontier model (coins handled by post-send refresh + Nostr sync), only the next receive and change address need watching — so track exactly those two. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Sync wallet state across devices via encrypted Nostr eventsAdam Soltys2026-06-151-27/+76
| | | | | | | | | | | | | | Persist wallet state (addresses, utxos, history, indices, fees) as a NIP-44 encrypted, parameterized-replaceable event (kind 30078) addressed to self on relay.coinos.io. The Nostr identity is derived from the same seed (NIP-06), so no extra secret. On load we pull the latest state from Nostr — a device with no local cache gets full state without re-scanning, and another device's updates propagate — using savedAt to keep the newest copy. localStorage stays as the instant/offline cache; state is published (debounced) on every change. (Relay note: added kind 30078 to the strfry write-policy allowlist on prod.) Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Stop re-scanning coin addresses; frontier-only poll + manual rescanAdam Soltys2026-06-151-14/+14
| | | | | | | | | | | Per request: an address is scanned once, then never auto-scanned again. The routine poll now checks only the fresh frontier (next receive + change address) for new deposits; coin-holding addresses are no longer polled and the periodic full re-scan is removed. Spends on existing coins arrive via the WebSocket while connected; a new Settings → Rescan button does a full scan to catch anything missed (e.g. a payment to a reused old address). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Route explorer traffic through VPS proxy (temporary)Adam Soltys2026-06-151-4/+2
| | | | | | | | | | This IP is rate-limited/banned by mempool.space, so REST + WS now go through an nginx CORS proxy on the OVH VPS (mempool via /mp, blockstream via /bs). Controlled by a single PROXY constant in api.js — set it to '' to revert to the public explorers once the ban is lifted. Hosts are tagged by kind so fee format detection still works. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Cache stable addresses; poll only the live frontierAdam Soltys2026-06-151-4/+50
| | | | | | | | | | | Idle polls were re-querying every used address (~N requests each cycle). Now the background poll checks only addresses that can change — those holding coins and the next fresh receive/change address — and escalates to a full scan only when one of them moves. Stable historical addresses stay cached. A full scan still runs every 5 min as a safety net. Cuts steady-state request volume dramatically for wallets with many used addresses. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Keep the WebSocket alive: heartbeat + reconnect-on-wakeAdam Soltys2026-06-151-0/+47
| | | | | | | | | | | The mempool WS sends no periodic traffic on a track-addresses-only subscription, so a half-open connection (died without firing onclose) went undetected and updates fell back to the slow poll. Add: - ping every 15s ({action:'ping'} -> pong); if no message for 45s, force a reconnect (handles half-open sockets). - reconnect + refresh on tab refocus (visibilitychange) and network 'online'. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Apply WS tx payload instantly; faster reconcileAdam Soltys2026-06-151-1/+41
| | | | | | | | | | | | | | The mempool.space WebSocket push already contains the transaction, so parse it on arrival to add the incoming UTXO and bump pending immediately, instead of waiting on a debounced re-scan. The scan still follows (400ms) to reconcile spends/confirmations. Removes the ~3s our handler added on top of mempool.space's own push latency. (Measured: mempool.space itself takes ~8s to see a coinos-broadcast tx due to p2p propagation — the real fix for incoming-payment latency is the sender pushing directly to the explorer.) Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Global rate-aware request proxy + lighter pollingAdam Soltys2026-06-151-15/+43
| | | | | | | | | | | | | | 429 handling: route every outgoing request (address, utxo, txs, fees, broadcast) through one scheduler. A 429 from ANY request now pauses ALL subsequent requests with exponential backoff (2s→30s), easing off after a streak of successes — instead of only cooling down the same host/kind. Fewer requests: derive balance from /address chain_stats (no /utxo needed just to total), and on background polls fetch only /address. The heavier /utxo + /txs are pulled only on first load or when the balance/addresses actually change. Idle polls drop from ~3N requests to ~N. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Fix Max button, slash scan requests (429s), cache history locallyAdam Soltys2026-06-151-12/+74
| | | | | | | | | | | | | | | | | | | | Max button: fill the amount field with the computed maximum (n inputs, 1 output, current fee rate) so it gives visible feedback instead of just blanking/disabling the field. Rate limits: eliminate look-ahead (gap limit 1 — stop at the first unused address, safe because we only ever expose one unused address at a time), query one address at a time (API concurrency 1, 400ms spacing across two backends), scan chains sequentially, and pause 500ms after each used address. An empty scan is now ~2-3 requests instead of ~10+. History cache: persist scanned state (addresses, utxos, txs, indices) in localStorage keyed by sha256(seed) — the seed itself is never stored. Re- importing the same seed in this browser shows the last-known balance/history instantly while a fresh scan runs. Cleared by nothing automatically (public chain data); different seeds get separate keys. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Drop live badge + fiat price; persist wallet in sessionStorageAdam Soltys2026-06-151-8/+2
| | | | | | | | | | | | | - Remove the online/live status badge and the USD price line, which both appeared after load and caused layout shift. Stop fetching price entirely. - Persist the seed + passphrase in sessionStorage so a page refresh restores the open wallet instead of returning to the unlock screen. Cleared on logout and (by the browser) when the tab closes. Note: sessionStorage holds the seed in plaintext for the tab session — an acceptable tradeoff for this self-custody tool and scoped to the tab. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Add polling safety-net so pending balances appear reliablyAdam Soltys2026-06-151-14/+47
| | | | | | | | | | | | | | | | | | Relying solely on the mempool WebSocket was unreliable: it only pushes new events (no replay), and pushes can be missed (drops, server quirks), so a deposit could go unseen with no manual refresh to fall back on. - Add a 12s background poll alongside the WebSocket. - Make background refreshes "silent": scan({silent:true}) re-renders only when the visible state (utxos/txs/fresh indices) actually changes, so polls never flash the UI or steal input focus while typing. - Skip fees/price fetches on routine polls to keep request volume low. - WS events now trigger the same silent refresh (debounce 800ms). Unit-tested: silent refresh emits once when a pending deposit appears and not at all when nothing changed. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Simplify: auto-detect offline, mainnet-only, no manual refreshAdam Soltys2026-06-141-0/+4
| | | | | | | | | | | | | | - Remove the unlock-screen "Offline mode" checkbox and network selector. Always mainnet. Attempt a scan on open; if navigator is offline or the scan fails (network unreachable), fall back to offline automatically with a banner + Retry instead of an upfront toggle. - Drop the manual balance "Refresh" and receive "Check for payments" buttons; the WebSocket keeps things current. - Update balances silently: add a wallet.loaded flag so the balance only dims on the very first load and history/coins only show a spinner before the first load. Background WS-driven rescans no longer flash spinners. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Reduce gap limit to 5; clarify receive-address noteAdam Soltys2026-06-141-1/+7
| | | | | | | | | | | Since the wallet only ever exposes one unused receive address at a time, used addresses stay contiguous (gap never exceeds 1), so a gap limit of 5 is a safe margin and cuts a scan from ~40 to ~10 requests. Also reword the receive note: the address advances automatically (via the realtime WebSocket) when paid, so drop the confusing "scan again". Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Rebuild as single-file offline BIP84 walletAdam Soltys2026-06-141-0/+565
Replace the SvelteKit/daisyUI prototype with a vanilla JS + hand-rolled CSS app that Bun bundles (code + scure/noble crypto + QR + CSS) into one self-contained dist/index.html runnable offline from file://. Wallet core (src/wallet.js): - BIP84 native-segwit HD wallet from BIP39 seed (+optional passphrase) - gap-limit scan of receive/change chains, aggregate balance, tx history - fresh receive/change addresses; signs with witnessUtxo only (enables offline signing with no prev-tx fetch) - coin selection + fee estimation via btc-signer selectUTXO; send-max; manual coin control - offline JSON snapshot export/import (keyless) + signed-tx hex/QR export - realtime mempool.space WebSocket: track-addresses -> debounced auto refresh of balances/history (no polling); live status badge UI (src/app.js): clean light paper-wallet; create (with optional, skippable backup verification) / import flow; receive (QR) / send / history / coins / tools tabs; clickable logo returns home. API (src/api.js): mempool.space wrapper with retry/backoff for scan-burst rate limiting; single network choke-point for offline enforcement. Verified: BIP84 spec vectors, build/sign/parse for auto/max/manual flows, snapshot roundtrip, live scan + WebSocket, headless-Chrome UI smoke tests. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>