diff options
| author | Adam Soltys <[email protected]> | 2026-06-23 23:40:09 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-23 23:40:09 -0700 |
| commit | 6190f25ca44ed12a0e136004096aef042c472b77 (patch) | |
| tree | cca5b22cdd0b8626624d567cfd5e59928d97f65b /src | |
| parent | ecdc460769fc6daad66df23604ceee6456b29061 (diff) | |
| download | coinosv3-6190f25ca44ed12a0e136004096aef042c472b77.tar.gz coinosv3-6190f25ca44ed12a0e136004096aef042c472b77.zip | |
Reconcile spends of held coins on refresh
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]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/wallet.js | 58 |
1 files changed, 58 insertions, 0 deletions
diff --git a/src/wallet.js b/src/wallet.js index 2547b45..79c6353 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -309,6 +309,12 @@ export class Wallet { } } + // Re-verify the addresses that currently hold coins, to catch spends from + // an already-scanned address (the frontier walk above only looks forward, + // so a coin spent here — including by a tx broadcast on another device — + // would otherwise never reconcile without a full rescan). + if (await this._reconcileHeld()) changed = true; + if (changed) { this.utxos.sort((x, y) => y.value - x.value); this._sortTxs(); @@ -324,6 +330,58 @@ export class Wallet { } } + // Re-check every address that currently holds a UTXO: refresh its balance from + // chain/mempool stats and rebuild its UTXOs. This is what reconciles spends + // (a held coin that's now gone) — confirmed or still in the mempool, regardless + // of which device broadcast the spending transaction. Returns true if anything + // changed. Only touches addresses we already know, so it stays cheap. + async _reconcileHeld() { + const addrs = [...new Set(this.utxos.map((u) => u.address))]; + let changed = false; + for (const address of addrs) { + const p = this.addrMap.get(address); + if (!p) continue; + let info; + try { + info = await this.api.addressInfo(address); + } catch { + continue; + } + const cs = info.chain_stats || {}; + const ms = info.mempool_stats || {}; + const confirmed = (cs.funded_txo_sum || 0) - (cs.spent_txo_sum || 0); + const pending = (ms.funded_txo_sum || 0) - (ms.spent_txo_sum || 0); + const arr = p.chain === 0 ? this.receive : this.change; + const entry = arr.find((e) => e.index === p.index); + if (entry && entry.confirmed === confirmed && entry.pending === pending) continue; + + let us; + try { + us = await this.api.addressUtxos(address); + } catch { + continue; + } + if (entry) { + entry.confirmed = confirmed; + entry.pending = pending; + } + this.utxos = this.utxos.filter((u) => u.address !== address); + for (const u of us) { + this.utxos.push({ + txid: u.txid, + vout: u.vout, + value: u.value, + address, + chain: p.chain, + index: p.index, + confirmed: !!u.status.confirmed, + }); + } + changed = true; + } + return changed; + } + // silent=false: foreground load (shows loading state, always re-renders). // silent=true: background refresh (poll / WS) — no spinner, and only emits // if the visible state changed, so it never disrupts typing. |
