aboutsummaryrefslogtreecommitdiffstats
path: root/src/wallet.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-24 23:36:17 -0700
committerAdam Soltys <[email protected]>2026-06-24 23:36:17 -0700
commit6d8df4d5b004ea9a84057ff78b9276262cb8f264 (patch)
tree1b9f82551726bc6088fe21b81d745a0becc67b0f /src/wallet.js
parent3b1adc89a361fd4c1a0552673c621a06f041dc75 (diff)
downloadcoinosv3-6d8df4d5b004ea9a84057ff78b9276262cb8f264.tar.gz
coinosv3-6d8df4d5b004ea9a84057ff78b9276262cb8f264.zip
Drop background/full rescans for on-demand per-address rescan
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]>
Diffstat (limited to 'src/wallet.js')
-rw-r--r--src/wallet.js67
1 files changed, 58 insertions, 9 deletions
diff --git a/src/wallet.js b/src/wallet.js
index f52efdf..0f2a763 100644
--- a/src/wallet.js
+++ b/src/wallet.js
@@ -428,6 +428,59 @@ export class Wallet {
return changed;
}
+ // Every address the wallet currently knows about (scanned receive + change),
+ // sorted by chain then index, for the per-address rescan list in Settings.
+ knownAddresses() {
+ const mk = (e, chain) => ({ chain, index: e.index, address: e.address, used: !!e.used, balance: (e.confirmed || 0) + (e.pending || 0) });
+ return [
+ ...this.receive.map((e) => mk(e, 0)),
+ ...this.change.map((e) => mk(e, 1)),
+ ].sort((a, b) => a.chain - b.chain || a.index - b.index);
+ }
+
+ // Re-query a single address and reconcile just its coins/history into state —
+ // the targeted alternative to a full rescan, for recovering a deposit to a
+ // reused old address. Returns whether the address is used.
+ async rescanAddress(chain, index) {
+ const { address } = this.derive(chain, index);
+ const info = await this.api.addressInfo(address);
+ const cs = info.chain_stats || {};
+ const ms = info.mempool_stats || {};
+ const used = (cs.tx_count || 0) > 0 || (ms.tx_count || 0) > 0;
+ 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 = chain === 0 ? this.receive : this.change;
+ let entry = arr.find((e) => e.index === index);
+ if (!entry) { entry = { chain, index, address }; arr.push(entry); arr.sort((a, b) => a.index - b.index); }
+ this.addrMap.set(address, { chain, index });
+ entry.used = used;
+ entry.confirmed = confirmed;
+ entry.pending = pending;
+
+ const us = await this.api.addressUtxos(address);
+ 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, index, confirmed: !!u.status.confirmed });
+
+ const list = await this.api.addressTxs(address);
+ for (const tx of list) {
+ const summary = this._txSummary(tx);
+ const at = this.txs.findIndex((t) => t.txid === tx.txid);
+ if (at >= 0) this.txs[at] = summary;
+ else this.txs.push(summary);
+ }
+
+ this.nextReceiveIndex = firstUnused(this.receive);
+ this.nextChangeIndex = firstUnused(this.change);
+ this.utxos.sort((x, y) => y.value - x.value);
+ this._sortTxs();
+ this._recomputeBalanceFromChains();
+ this.loaded = true;
+ this.saveCache();
+ this.emit();
+ return used;
+ }
+
// 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.
@@ -1079,16 +1132,12 @@ export class Wallet {
this._lastPoll = Date.now();
this.refreshLive();
}, 3000);
- // Periodic full scan: reconciles pending→confirmed history, spends, and
- // stale cache/relay state — but only when the socket is down. While live the
- // WS already pushes confirmations and block events, so a full re-scan would
- // just burn the rate limit (and risk starving the very socket we rely on).
- this._deepTimer = setInterval(() => {
- if (this.live) return;
- this.scan({ silent: true }).catch(() => {});
- }, 120000);
+ // No periodic full re-scan: the WS (and the frontier poll above) keep the
+ // balance current, and refreshLive's _reconcileHeld catches spends of held
+ // coins. The only thing neither covers is a deposit to a reused old address,
+ // which is handled on demand via per-address rescan in Settings.
// Only mempool.space has a live socket; other explorers rely on the poll
- // and deep scan above (wsUrl returns null → no socket).
+ // above (wsUrl returns null → no socket).
if (typeof WebSocket === 'undefined' || !this.wsUrl()) return;
this._wsWant = true;