aboutsummaryrefslogtreecommitdiffstats
path: root/src/wallet.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-15 11:48:19 -0700
committerAdam Soltys <[email protected]>2026-06-15 11:48:19 -0700
commit769589f431725a275751bfc84a0d7e6bacee6d0d (patch)
tree251afc0a09c1043ef2dafefc3f4506120e105af8 /src/wallet.js
parent13d72fa27c709ed3511365e070ee1b8c86a961a9 (diff)
downloadcoinosv3-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.js58
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 };