aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-15 15:41:23 -0700
committerAdam Soltys <[email protected]>2026-06-15 15:41:23 -0700
commitc57f06e47eb5b662a00cddf27cd4270e8418d163 (patch)
treecf72cd6204e07ac29b37d30839e58be216876ef4 /src
parente2556617ef4c1632217b3731afe8e519369cbeab (diff)
downloadcoinosv3-c57f06e47eb5b662a00cddf27cd4270e8418d163.tar.gz
coinosv3-c57f06e47eb5b662a00cddf27cd4270e8418d163.zip
Cache stable addresses; poll only the live frontier
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]>
Diffstat (limited to 'src')
-rw-r--r--src/wallet.js54
1 files changed, 50 insertions, 4 deletions
diff --git a/src/wallet.js b/src/wallet.js
index b0d37a9..241572a 100644
--- a/src/wallet.js
+++ b/src/wallet.js
@@ -84,6 +84,8 @@ export class Wallet {
this._wsRetry = null;
this._refreshTimer = null;
this._pollTimer = null;
+ this._deepTimer = null; // periodic full-scan safety net
+ this._polling = false; // a light poll is in flight
this._hbTimer = null; // heartbeat / liveness watchdog
this._lastMsg = 0; // time of last WS message (incl. pong)
this._wakeHooked = false;
@@ -222,6 +224,48 @@ export class Wallet {
});
}
+ _addrInfo(chain, index) {
+ const arr = chain === 0 ? this.receive : this.change;
+ return arr.find((a) => a.index === index);
+ }
+
+ // Light poll: only re-query the addresses that can actually change — ones
+ // holding coins (could be spent) and the next fresh receive/change address
+ // (could receive). Stable historical addresses stay cached and aren't
+ // re-fetched. If anything moved, fall back to a full scan to reconcile.
+ async refreshLive() {
+ if (this.offline || this._refreshing || this._polling) return;
+ this._polling = true;
+ try {
+ const targets = new Map();
+ for (const u of this.utxos) targets.set(`${u.chain}/${u.index}`, { chain: u.chain, index: u.index });
+ targets.set(`0/${this.nextReceiveIndex}`, { chain: 0, index: this.nextReceiveIndex });
+ targets.set(`1/${this.nextChangeIndex}`, { chain: 1, index: this.nextChangeIndex });
+
+ let changed = false;
+ for (const { chain, index } of targets.values()) {
+ const { address } = this.derive(chain, index);
+ const info = await this.api.addressInfo(address);
+ 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 used = (cs.tx_count || 0) > 0 || (ms.tx_count || 0) > 0;
+ const prev = this._addrInfo(chain, index);
+ if (!prev || prev.confirmed !== confirmed || prev.pending !== pending || prev.used !== used) {
+ changed = true;
+ break;
+ }
+ }
+ if (changed) {
+ this._polling = false;
+ await this.scan({ silent: true });
+ }
+ } finally {
+ this._polling = false;
+ }
+ }
+
// 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.
@@ -455,10 +499,11 @@ export class Wallet {
startRealtime() {
if (this.offline) return;
this.stopRealtime();
- // Safety-net poll: the WebSocket gives instant updates, but its pushes can
- // be missed (connection drops, server quirks). A light periodic silent
- // refresh guarantees pending balances appear within a few seconds.
- this._pollTimer = setInterval(() => this.scan({ silent: true }), 12000);
+ // Safety-net polling (the WebSocket gives instant updates, but its pushes
+ // can be missed): a light poll of just the live addresses every 15s, plus
+ // an occasional full scan to catch anything the frontier check can't.
+ this._pollTimer = setInterval(() => this.refreshLive(), 15000);
+ this._deepTimer = setInterval(() => this.scan({ silent: true }), 300000);
if (typeof WebSocket === 'undefined') return;
this._wsWant = true;
@@ -623,6 +668,7 @@ export class Wallet {
clearTimeout(this._wsRetry);
clearTimeout(this._refreshTimer);
clearInterval(this._pollTimer);
+ clearInterval(this._deepTimer);
clearInterval(this._hbTimer);
if (this._ws) {
try {