diff options
| author | Adam Soltys <[email protected]> | 2026-06-19 08:23:57 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-19 08:23:57 -0700 |
| commit | 47293eb1dd2016358af0af18ff68d417630fca3d (patch) | |
| tree | ca3f00fe532d0d4e629de5f1db1572153a6e971c /src/wallet.js | |
| parent | 4527ec3815caf314a717823220fc630ab9831988 (diff) | |
| download | coinosv3-47293eb1dd2016358af0af18ff68d417630fca3d.tar.gz coinosv3-47293eb1dd2016358af0af18ff68d417630fca3d.zip | |
Add How it works page, plainer copy, faster import scan
UI / copy:
- Footer: "made by Adam Soltys" (drop the heart) + a "How it works"
link on its own line, matching the footer's size.
- New "How it works" page: plain-language basics plus an expandable
"for the technically curious" section. Routed via ui.screen, with
Back/logo returning to wherever you opened it from.
- Scrub jargon from the main UI for newcomers: drop BIP39, soften
"raw hex" -> "code", "txid" -> "transaction ID", "air-gapped" ->
"offline"; replace em dashes in the intro/keys notes. The technical
terms now live only in the How it works technical section.
Import / scan performance (fewer requests -> fewer 429s, faster):
- Skip the /utxo fetch for fully-settled, zero-balance addresses
(they can't hold UTXOs) -- the bulk of a history-heavy wallet.
- Show balance as soon as the address scan finishes, then load UTXOs
and history in the background.
- Probe the receive and change chains concurrently so their per-hit
delays overlap.
- History fills in progressively as each address resolves, with a
loading indicator on the History tab.
i18n.js is now tracked; new English keys fall back to English for the
other languages until translated.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src/wallet.js')
| -rw-r--r-- | src/wallet.js | 66 |
1 files changed, 52 insertions, 14 deletions
diff --git a/src/wallet.js b/src/wallet.js index ba22af2..7152d31 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -70,6 +70,7 @@ export class Wallet { this.scanning = false; this.loaded = false; // true once a scan/snapshot has populated balances once + this.historyLoading = false; // history is still being fetched in the background this._refreshing = false; // a scan/refresh is in flight this.nextReceiveIndex = 0; this.nextChangeIndex = 0; @@ -206,10 +207,11 @@ export class Wallet { const cs = info.chain_stats || {}; const ms = info.mempool_stats || {}; const used = (cs.tx_count || 0) > 0 || (ms.tx_count || 0) > 0; + const hasMempool = (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 }); + found.push({ chain, index: i, address, used, confirmed, pending, hasMempool }); this.addrMap.set(address, { chain, index: i }); if (used) { gap = 0; @@ -336,8 +338,11 @@ export class Wallet { 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); + // Probe both chains at once — they're independent, so interleaving lets + // their per-hit delays overlap instead of running back-to-back. + const [receive, change] = await Promise.all([this.scanChain(0), this.scanChain(1)]); + this.receive = receive; + this.change = change; this.nextReceiveIndex = firstUnused(this.receive); this.nextChangeIndex = firstUnused(this.change); this._recomputeBalanceFromChains(); @@ -347,14 +352,23 @@ export class Wallet { const balanceChanged = `${this.confirmed}|${this.pending}|${this.nextReceiveIndex}|${this.nextChangeIndex}` !== prevBal; if (!silent || balanceChanged || !this.loaded) { + // Balance + receive address are already known from the chain_stats above. + // Show them right away so the wallet looks ready, then keep loading the + // heavier UTXO set and full history in the background. + if (!silent) { + this.loaded = true; + this.historyLoading = true; + this.emit(); + } if (!silent || !this.feeRates) this.feeRates = await this.api.feeRates(); await this.refreshUtxos(); - await this.refreshHistory(); + await this.refreshHistory(!silent); } this.loaded = true; this.saveCache(); } finally { this._refreshing = false; + this.historyLoading = false; // clear even if an earlier step threw if (!silent) { this.scanning = false; this.emit(); @@ -369,7 +383,13 @@ export class Wallet { } async refreshUtxos() { - const used = this.usedAddresses(); + // An address whose confirmed balance is 0 and which has no mempool activity + // is fully spent and settled — it cannot hold any UTXOs, so skip the /utxo + // round-trip for it. In a wallet with history most addresses are spent, so + // this avoids the bulk of the requests (and the 429s that come with them). + const used = this.usedAddresses().filter( + (a) => (a.confirmed || 0) > 0 || a.hasMempool + ); const lists = await pool(used, (a) => this.api.addressUtxos(a.address)); const utxos = []; used.forEach((a, idx) => { @@ -418,17 +438,35 @@ export class Wallet { }); } - async refreshHistory() { + // progressive: emit after each address resolves so the History tab fills in + // as transactions arrive (foreground load) instead of all at once at the end. + async refreshHistory(progressive = false) { const used = this.usedAddresses(); - const lists = await pool(used, (a) => this.api.addressTxs(a.address)); - const byId = new Map(); - for (const list of lists) { - for (const tx of list) { - if (!byId.has(tx.txid)) byId.set(tx.txid, this._txSummary(tx)); - } + this.historyLoading = true; + const seen = new Set(); // txids found in this pass (a tx can touch >1 address) + try { + await pool(used, async (a) => { + const list = await this.api.addressTxs(a.address); + let added = false; + for (const tx of list) { + if (seen.has(tx.txid)) continue; + seen.add(tx.txid); + const summary = this._txSummary(tx); + const at = this.txs.findIndex((t) => t.txid === tx.txid); + if (at >= 0) this.txs[at] = summary; // refresh confirmations/fee + else { this.txs.push(summary); added = true; } + } + if (added && progressive) { + this._sortTxs(); + this.emit(); + } + }); + // Drop anything that no longer appears in any address's history. + this.txs = this.txs.filter((t) => seen.has(t.txid)); + this._sortTxs(); + } finally { + this.historyLoading = false; } - this.txs = [...byId.values()]; - this._sortTxs(); } // --- balances ----------------------------------------------------------- |
