diff options
| author | Adam Soltys <[email protected]> | 2026-06-15 11:48:19 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-15 11:48:19 -0700 |
| commit | 769589f431725a275751bfc84a0d7e6bacee6d0d (patch) | |
| tree | 251afc0a09c1043ef2dafefc3f4506120e105af8 /src/wallet.js | |
| parent | 13d72fa27c709ed3511365e070ee1b8c86a961a9 (diff) | |
| download | coinosv3-769589f431725a275751bfc84a0d7e6bacee6d0d.tar.gz coinosv3-769589f431725a275751bfc84a0d7e6bacee6d0d.zip | |
Global rate-aware request proxy + lighter polling
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]>
Diffstat (limited to 'src/wallet.js')
| -rw-r--r-- | src/wallet.js | 58 |
1 files changed, 43 insertions, 15 deletions
diff --git a/src/wallet.js b/src/wallet.js index 4f6cf0d..2f9fed7 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -64,6 +64,8 @@ export class Wallet { this.utxos = []; // { txid, vout, value, address, chain, index, confirmed } this.txs = []; // aggregated history this.feeRates = null; + this.confirmed = 0; + this.pending = 0; this.scanning = false; this.loaded = false; // true once a scan/snapshot has populated balances once @@ -115,6 +117,8 @@ export class Wallet { this.addrMap = new Map(); this.utxos = []; this.txs = []; + this.confirmed = 0; + this.pending = 0; this.loaded = false; this.nextReceiveIndex = 0; this.nextChangeIndex = 0; @@ -183,9 +187,13 @@ export class Wallet { while (gap < GAP_LIMIT) { const { address } = this.derive(chain, i); const info = await this.api.addressInfo(address); - const used = - info.chain_stats.tx_count > 0 || info.mempool_stats.tx_count > 0; - found.push({ chain, index: i, address, used }); + const cs = info.chain_stats || {}; + const ms = info.mempool_stats || {}; + const used = (cs.tx_count || 0) > 0 || (ms.tx_count || 0) > 0; + // Balance straight from chain_stats — no need to fetch /utxo just to total. + const confirmed = (cs.funded_txo_sum || 0) - (cs.spent_txo_sum || 0); + const pending = (ms.funded_txo_sum || 0) - (ms.spent_txo_sum || 0); + found.push({ chain, index: i, address, used, confirmed, pending }); this.addrMap.set(address, { chain, index: i }); if (used) { gap = 0; @@ -202,6 +210,8 @@ export class Wallet { // refreshes only re-render when something actually changed. _sig() { return JSON.stringify({ + bc: this.confirmed, + bp: this.pending, u: this.utxos.map((u) => `${u.txid}:${u.vout}:${u.value}:${u.confirmed ? 1 : 0}`), t: this.txs.map((t) => `${t.txid}:${t.confirmed ? 1 : 0}`), r: this.nextReceiveIndex, @@ -221,19 +231,23 @@ export class Wallet { this.emit(); } try { + const prevBal = `${this.confirmed}|${this.pending}|${this.nextReceiveIndex}|${this.nextChangeIndex}`; this.addrMap = new Map(); this.receive = await this.scanChain(0); this.change = await this.scanChain(1); this.nextReceiveIndex = firstUnused(this.receive); this.nextChangeIndex = firstUnused(this.change); + this._recomputeBalanceFromChains(); - // Fees are only needed at load — skip on routine background polls. - if (!silent || !this.feeRates) { - this.feeRates = await this.api.feeRates(); + // Only pull /utxo and /txs (the heavy part) on first load or when the + // balance/addresses actually changed. Idle polls stay /address-only. + const balanceChanged = + `${this.confirmed}|${this.pending}|${this.nextReceiveIndex}|${this.nextChangeIndex}` !== prevBal; + if (!silent || balanceChanged || !this.loaded) { + if (!silent || !this.feeRates) this.feeRates = await this.api.feeRates(); + await this.refreshUtxos(); + await this.refreshHistory(); } - - await this.refreshUtxos(); - await this.refreshHistory(); this.loaded = true; this.saveCache(); } finally { @@ -309,16 +323,28 @@ export class Wallet { } // --- balances ----------------------------------------------------------- - get confirmed() { - return this.utxos.reduce((s, u) => s + (u.confirmed ? u.value : 0), 0); - } - get pending() { - return this.utxos.reduce((s, u) => s + (u.confirmed ? 0 : u.value), 0); - } + // confirmed/pending are plain fields, set from chain_stats during scan (and + // from UTXOs when restoring an offline snapshot). total is derived. get total() { return this.confirmed + this.pending; } + _recomputeBalanceFromChains() { + let c = 0; + let p = 0; + for (const a of [...this.receive, ...this.change]) { + c += a.confirmed || 0; + p += a.pending || 0; + } + this.confirmed = c; + this.pending = p; + } + + _recomputeBalanceFromUtxos() { + this.confirmed = this.utxos.reduce((s, u) => s + (u.confirmed ? u.value : 0), 0); + this.pending = this.utxos.reduce((s, u) => s + (u.confirmed ? 0 : u.value), 0); + } + freshReceive() { return this.derive(0, this.nextReceiveIndex); } @@ -564,6 +590,7 @@ export class Wallet { for (const a of [...this.receive, ...this.change]) { this.addrMap.set(a.address, { chain: a.chain, index: a.index }); } + this._recomputeBalanceFromChains(); this.loaded = true; this.emit(); return true; @@ -632,6 +659,7 @@ export class Wallet { } utxos.sort((a, b) => b.value - a.value); this.utxos = utxos; + this._recomputeBalanceFromUtxos(); this.loaded = true; this.emit(); return { imported: utxos.length, unmatched }; |
