aboutsummaryrefslogtreecommitdiffstats
path: root/src
Commit message (Collapse)AuthorAgeFilesLines
* Send gift DMs as NIP-17 only (drop NIP-04)Adam Soltys2026-06-281-11/+7
| | | | | | | Stop also sending the legacy NIP-04 kind-4 DM — clients that support both (e.g. Amethyst) were showing the claim code twice. NIP-17 alone covers current clients. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Nostr: use SimplePool (fixes WS console errors) + send DMs as NIP-04 AND NIP-17Adam Soltys2026-06-281-121/+33
| | | | | | | | | | | | | | | | - Migrated all relay I/O (profile/inbox fetch, DM publish, state sync) to a shared SimplePool. The old code opened a relay per request and closed it manually, which raced nostr-tools' CLOSE-after-EOSE and spammed 'WebSocket is already in CLOSING or CLOSED state'. SimplePool owns the connection lifecycle, so that's gone. Also reuses connections instead of churning one per fetch. - Gift DMs now go out as BOTH NIP-04 (kind 4) and NIP-17 (kind 1059 gift wrap). Older clients (Amethyst) read NIP-04; newer ones (Jumble) only support NIP-17 and ignore NIP-04 — sending both means the claim code shows up either way. Verified: profile/inbox fetch + DM still work via the pool; the NIP-17 wrap is published and unwraps to the original message; the NIP-04 kind-4 still lands too. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Remove the 'reusable address that can't be tracked' note from the SP receive ↵Adam Soltys2026-06-281-2/+1
| | | | | | card Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Consolidate coins: a Settings action + a gift-time recommendationAdam Soltys2026-06-283-11/+76
| | | | | | | | | | | | | | | | | - Settings gains a 'Consolidate coins' card (shown when you hold >1 spendable coin): wallet.consolidate() does a max self-send merging them into one, so future sends/gifts use a single input — cheaper and a smaller gift QR. - At gift creation, detect when the gift would bundle several coins (giftCoinSummary returns the input count) and trigger the split/consolidate offer when count ≥ 3 (not just when it frees change). The offer now explains it plainly ('would bundle N coins, costly to claim') and recommends splitting into one coin first, with 'Use N coins anyway' as the explicit opt-out. Verified on regtest: 6 small coins → a 1000-sat gift needs 3 inputs and triggers the offer; consolidate merges them to 1 coin, after which the same gift is a clean single input. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Fix gift creation silently failing on large (many-input) gifts; one Back buttonAdam Soltys2026-06-282-3/+10
| | | | | | | | | | | - A many-input gift produces a URL too long for a QR, and qrSvg threw — uncaught, inside render() — so the click did nothing and showed no error. Wrap the gift QR in try/catch: render the QR when it fits, otherwise fall back to a 'too large for a QR — use the link' note. The link itself still works. - The split-offer card has its own Back; hide the gift sub-view's outer Back while the split offer is showing, so there aren't two stacked Back buttons. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Locked gifts: let NIP-07 extension users decrypt in-place, skip the codeAdam Soltys2026-06-284-13/+45
| | | | | | | | | | | | | | The gift payload is now encrypted two ways in the link: under the one-time code (the DM / manual path) AND to the recipient's nostr pubkey via an ephemeral key. When a NIP-07 browser extension (window.nostr) is present, the claim view offers 'Claim with nostr extension' — it checks the extension's pubkey matches the gift, decrypts ctKey via window.nostr.nip44.decrypt, and claims into a fresh wallet with no code. The pasted-code path remains as the fallback for everyone else. Verified: both decrypt paths recover the payload from the blob; a different extension account is rejected. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Route the gift DM to the recipient's published inbox relaysAdam Soltys2026-06-281-4/+32
| | | | | | | | | | | | Discover where a recipient reads DMs before sending the claim code: prefer their NIP-17 DM relay list (kind 10050), else their NIP-65 read relays (kind 10002), unioned with the broad fallback set (so users without a list still get it). This makes the locked-gift DM far more likely to land in the recipient's client. Verified: resolves fiatjaf's real inbox relays; a key with no list returns none and falls back to defaults. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Lock a gift to a nostr key: public link, claim code DM'd to the recipientAdam Soltys2026-06-284-13/+251
| | | | | | | | | | | | | | | | | | | | | | | | A gift can now be locked to a recipient's nostr npub. The shareable link (/lg/<blob>) carries the amount + recipient in the clear (anyone can see what it is and who it's for), but the claim payload is encrypted under a one-time code. That code is delivered as an encrypted nostr DM (NIP-04) to the recipient — so only they can read it. They paste the code to decrypt, then claim into a fresh wallet exactly like a normal bearer gift. - nostr.js: parseNostrPubkey/npubOf, encryptWithCode/decryptWithCode (random 32-byte code), fetchNostrProfile, NostrSync.sendDM (broad relays). - wallet.js: lockGift → { blob, claimCode }, previewLockedGift, nostr identity + sendNostrDM. - app.js: gift form gains an optional 'Lock to npub' field that shows the recipient's avatar + name (nostr kind-0 profile); create encrypts + DMs the code (with sent/failed status + a copyable fallback); /lg/ links open a claim view showing the gift + recipient where you paste the code; account settings shows your own npub to share. Verified: code lock/unlock round-trip, a real nostr DM published and read back from relay.damus.io, and the UI loads + renders with no errors. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Source lists: show only the URL, protocol strippedAdam Soltys2026-06-281-2/+2
| | | | | | | | Drop the human-readable name and the scheme prefix in the data-source and SP-indexer dropdowns, e.g. 'electrum.coinos.io', 'mempool.space/api', 'tweaks.coinos.io'. 'Custom' stays bare. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Show full URLs in the source lists; SP indexer → tweaks.coinos.ioAdam Soltys2026-06-282-3/+3
| | | | | | | | | | | | - Data-source and SP-indexer dropdowns now show the endpoint URL alongside the name (e.g. 'coinos — wss://electrum.coinos.io'), so it's clear where data comes from; 'Custom' stays bare. - The tweak indexer is now served at https://tweaks.coinos.io (was halwallet.app/ sp): a cloudflared route on the bitcoin tunnel → the indexer at :8888, with a DNS CNAME. Verified from a browser: /height, /mempool (1064 items), and /ws all work (the coinos.io Bot Fight Mode challenges non-browser clients only). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Unify data sources into one list; coinos → electrum.coinos.ioAdam Soltys2026-06-283-106/+111
| | | | | | | | | | | | | | | | | | | - Combined the separate Electrum and Explorer settings into a single data-source list. Each preset knows its type (electrum vs esplora); a custom source's type is detected from its URL scheme (ws/wss → Electrum, http/https → Esplora). The settings UI is now one dropdown + a URL field for Custom, with a one-line note showing the detected/selected type. getBackend/electrumCandidates/resolveHosts derive from the selected source. Old split settings migrate automatically. - coinos now points at wss://electrum.coinos.io (was wss://halwallet.app/electrum). Set up as a cloudflared route on the bitcoin tunnel → the Fulcrum WS (:50003), with a DNS CNAME. Verified the endpoint serves Electrum from a browser (Fulcrum 2.1.1 replies; cloudflare Bot Fight Mode only challenges non-browser clients). Verified: combined list + auto-detection (unit + browser), old-settings migration, regtest data flow unchanged, and the app loads over electrum.coinos.io with no errors. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Spend SP coins like any coin — confirmed-only, and able to fund SP paymentsAdam Soltys2026-06-282-14/+29
| | | | | | | | | | | | | | | | | | | | | | | | Two things: - Only confirmed SP coins enter the spend pool. Unconfirmed ones are excluded (the sender's tx could be replaced, invalidating our spend) — matching the spendable headline, which already counted confirmed-only. - SP→SP sending: our own silent-payment coins can now fund a payment TO an sp1 address (previously excluded). _applySilentPayments includes SP inputs with their key d = spend_priv + t_k, and silentPaymentScripts negates a taproot input's key when it gives odd Y, per BIP-352 (so the sender's A = a·G matches what the indexer reconstructs). signing was already SP-aware. Validated: BIP-352 sending vectors pass for all clean key-path-taproot-input cases (the remaining vector failures are NUMS script-path / labels / uncompressed keys — input types the wallet never spends). End-to-end on regtest: an SP coin funds a payment to another sp address, testmempoolaccept passes, and the recipient scans and finds it. Note: silentPaymentScripts derives input_hash from the eligible inputs it's given, which is correct because the wallet only ever spends all-eligible coins (P2WPKH + key-path SP); it isn't a general-purpose sender for txs with ineligible inputs. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Stop input/select fields losing focusAdam Soltys2026-06-282-4/+36
| | | | | | | | | | | | | | | | | Two causes, both fixed: - _scanSpMempool emitted on every WS push even when our pending set was unchanged. The mainnet mempool churns constantly, so this re-rendered the whole app every few seconds, stealing focus mid-type. Now it only emits when our pending SP set actually changes. - render() rebuilds the DOM (replaceChildren), which inherently drops focus. Added focus + caret preservation: capture the focused field's index-path and selection before the rebuild, restore them after. So any render source (poll, payment push, scan) is now invisible to someone typing. Verified via CDP: typing 'bc1q…' in the Send address, caret at 6, then forcing a re-render — focus, value, and caret all retained. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Run silent-payment scanning in a Web WorkerAdam Soltys2026-06-282-39/+152
| | | | | | | | | | | | | | | | | The EC crypto (candidate derivation + full scan) now runs in a worker, off the main thread, so even a large mainnet scan can't jank the UI at all. The worker is bundled separately and embedded as base64 (single-file app), spawned from a Blob URL; it holds the keys + per-tweak cache. If workers are unavailable or the bundle is missing, scanning falls back to the main thread (still cached + yielding). scanSilentPayments / _scanSpItems / _scanSpMempool now go through _spHitBlocks and _spScanItems, which dispatch to the worker (or the fallback). +27KB to index.html for the worker's crypto bundle. Verified via CDP: worker runs keys/candidates/scan correctly in-browser; a real reload + mainnet scan threw 0 exceptions and the SP address renders. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Scan silent payments from each wallet's birth, not the indexer's startAdam Soltys2026-06-282-7/+18
| | | | | | | | | | | | | A freshly-generated wallet can't have received SP before it existed, so set its scan watermark to the current tip on first connect (skipping the whole historical catch-up) instead of scanning from 0. Threaded a 'generated' flag through the create flow (openWallet → enterWallet → activateAccount → load spFresh); imported wallets still scan from the indexer start since they may have prior receipts. Verified on regtest: a fresh wallet skips the block-5780 receipt (watermark=tip, balance 0); the same seed imported finds it. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Fix SP scanning jank: memoize by tweak + yield + coalesceAdam Soltys2026-06-271-21/+73
| | | | | | | | | | | | | | | | | | Profiling (CDP) showed the toggle lag/high CPU was _scanSpMempool re-scanning the ENTIRE mempool item set on every WS push (the mempool churns constantly), redoing an EC point-multiplication per tweak on the main thread — ~1s blocks. - Memoize silentPaymentScan/silentPaymentCandidate by tweak (outcome is fixed), cleared when the SP keys change, capped to bound memory. Steady-state CPU from re-scanning dropped from ~1s/push to ~0 (measured 18ms over 9s idle). - Yield to the event loop during the catch-up block loop and the mempool scan, so a large first/cold scan (mainnet) doesn't freeze the UI. - Coalesce mempool scans (latest-wins, never two at once) with a race guard so a block confirming mid-scan can't duplicate an outpoint. Measured after: receive toggle 8/8 switches, avg 5ms / max 9ms (was 1-2s). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Wire in-app back buttons to pop browser historyAdam Soltys2026-06-271-6/+15
| | | | | | | | | | | | | | The screen-changing back buttons (tx detail, gift sub-view, all-gifts, bump, account settings, accounts) now call goBack(), which does history.back() when there's an entry to pop — so they no longer push a duplicate entry. Falls back to an explicit parent-screen navigation when there's no in-app history (e.g. reloaded straight into a sub-screen). Modal-dismiss buttons (revoke, rename, password, draft) are unchanged — they don't touch nav state. Verified via CDP: detail (i=2) → in-app back → i=1 (popped) → Forward restores the detail. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Browser history / back-button navigationAdam Soltys2026-06-271-0/+50
| | | | | | | | | | | | | | Mirror the app's screen position (screen, tab, txDetail, bump, giftsAll, giftMode, claimStep) into the browser history, so Back/Forward (and Android system back) move between screens actually viewed. render() snapshots just these nav fields and pushState()s on change — incidental re-renders (typing, polling) don't add entries. popstate restores the snapshot and re-renders. History-API calls are guarded so they can never break a render. Verified via CDP: tab nav and tx-detail push entries; Back/Forward (incl. A→B→A) restore the right screen and the DOM follows. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Default mainnet to the hosted coinos silent-payment indexerAdam Soltys2026-06-271-2/+2
| | | | | | | Mainnet wallets now use https://halwallet.app/sp (our indexer on cs, behind nginx /sp), enabling silent-payment receiving on mainnet. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Make silent-payment receives first-class in the UIAdam Soltys2026-06-273-56/+65
| | | | | | | | | | | | | | | - One Receive card with an Address ⇄ Silent payment toggle (no separate card, no BIP-352 badge, no scan-status line or Rescan button — discovery is automatic). - SP receipts appear in History like normal payments: wallet.history merges BIP84 txs with per-tx SP receive rows (spTxRows), newest first; the detail view and pagination use it. SP outputs carry a firstSeen timestamp. - Receive description simplified to 'A reusable address that can't be tracked.' - Removed the '· Silent payment' tag from the Send recipient line. Verified on regtest: a received SP shows in history and balance with no manual scan. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Catch-up scaling: dust filter + per-block Bloom filtersAdam Soltys2026-06-272-13/+67
| | | | | | | | | | | | | | | | | silentpay.js: silentPaymentCandidate() (our P_0 for a tweak) + buildBloom/bloomHas (double-hashed sha256, ~0.8% FP). Validated: candidate == P_0 on all no-label vectors. Indexer /scan now returns, per block, the tweaks + a Bloom over that block's taproot output keys (no outputs), with an optional dustLimit. The wallet derives a candidate per tweak, tests the filter, and only fetches /block on a hit — so a big historical scan costs candidate math + a handful of fetches instead of downloading and scanning every block. Live WS pushes still carry full items. SP_DUST_LIMIT defaults to 0 (receive everything; the indexer honors any value). Verified end-to-end on regtest: the filter flow finds the output. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Mempool (unconfirmed) silent-payment discoveryAdam Soltys2026-06-271-13/+41
| | | | | | | | | | | | | | | | | Indexer: scan the mempool (getrawmempool + getrawtransaction; prevouts fetched explicitly since verbosity-2 omits them for mempool txs), track SP-eligible unconfirmed txs, serve /mempool, and push {type:'mempool'} on change. Switched Core RPC to node:http (no keep-alive) + serialized calls — Bun's fetch was dropping sockets under the added mempool load. Wallet: handle the mempool push and /mempool catch-up (_scanSpMempool) — pending SP receipts rebuild on each update (also drops RBF'd/vanished ones); block pushes promote a pending entry to confirmed. So a silent payment shows the instant it's broadcast, like a normal receive, with no mining. Verified on regtest: an unconfirmed SP is detected and served; clean logs. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Fix SP balance not showing — fold SP into spendable/pendingIncoming gettersAdam Soltys2026-06-271-3/+4
| | | | | | | | | 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]>
* Fix stuck 'Scanning…' — auto-scans no longer touch the visible flagAdam Soltys2026-06-271-5/+7
| | | | | | | | | 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]>
* Push silent payments over WebSocket instead of pollingAdam Soltys2026-06-271-1/+65
| | | | | | | | | | | | 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]>
* Auto-scan for silent payments + fold found value into balanceAdam Soltys2026-06-273-10/+19
| | | | | | | | | | | 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]>
* Spend received silent-payment outputs (taproot key-path)Adam Soltys2026-06-271-23/+70
| | | | | | | | | | | | | | | 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]>
* Wire silent-payment receiving into the wallet (scan + balance + UI)Adam Soltys2026-06-273-6/+171
| | | | | | | | | | | | | | | | | | | 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]>
* Wire SP foundations into the wallet: configurable indexer + keys/addressAdam Soltys2026-06-272-1/+57
| | | | | | | | | | | | 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]>
* Add regtest SP tweak indexer; accept Core's array-witness in inputPubKeyAdam Soltys2026-06-271-11/+17
| | | | | | | | | | | | | | 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]>
* Add BIP-352 receiving crypto (scan, tweak, keys, address)Adam Soltys2026-06-271-0/+177
| | | | | | | | | | | | | | | | | | 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]>
* Don't flash 'Payment received' on import for a pre-existing depositAdam Soltys2026-06-271-1/+6
| | | | | | | | | | | 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]>
* Offline transfer copy: drop the '(with this wallet + seed)' asideAdam Soltys2026-06-271-1/+1
| | | | Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Data source copy: drop the '— no REST' aside from the Electrum descriptionAdam Soltys2026-06-271-1/+1
| | | | Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Data source copy: drop coinos branding, "your own server" not Fulcrum/electrsAdam Soltys2026-06-271-2/+2
| | | | | | | | 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]>
* Payment details: say "Pending", not "Pending (in mempool)"Adam Soltys2026-06-272-2/+1
| | | | | | | 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]>
* Sort pending txs newest-first in History (by firstSeen, not blockTime)Adam Soltys2026-06-271-0/+4
| | | | | | | | | | | | 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]>
* Harden Electrum WS recovery: fail in-flight calls on drop, faster half-open ↵Adam Soltys2026-06-271-4/+12
| | | | | | | | | | | | | | | | | | | 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]>
* Allow importing a seed from a watch-only wallet's settings pageAdam Soltys2026-06-271-4/+15
| | | | | | | | | | 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]>
* Make the wallet-settings link a card with a clear button, not a bare headingAdam Soltys2026-06-272-1/+6
| | | | Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Link to the current wallet's settings from the top of global SettingsAdam Soltys2026-06-271-0/+4
| | | | Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* De-dup wallet settings; reorder per-wallet page; lowercase 'this'Adam Soltys2026-06-272-94/+22
| | | | | | | | | | 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]>
* Add a per-wallet settings page (cog from the accounts list)Adam Soltys2026-06-272-2/+88
| | | | | | | | | | 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]>
* Per-network data source; drop duplicate regtest fields; add mutinynetAdam Soltys2026-06-274-154/+137
| | | | | | | | | | | | | | 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]>
* Make coinos the default Electrum server instead of a separate modeAdam Soltys2026-06-272-42/+26
| | | | | | | | | 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]>
* Update coinos data-source copy now that it's our own Fulcrum nodeAdam Soltys2026-06-271-1/+1
| | | | Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Cut 'coinos' data source over to our own Fulcrum (Electrum)Adam Soltys2026-06-271-6/+6
| | | | | | | | | | | 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]>
* 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]>
* Use Fulcrum (Electrum/WS) for regtest data + push, dropping esploraAdam Soltys2026-06-272-2/+6
| | | | | | | | | | | | 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]>
* Regtest realtime via a local Electrum-over-WebSocket bridgeAdam Soltys2026-06-272-11/+31
| | | | | | | | | | | | | 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]>