aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/app.js17
-rw-r--r--src/wallet.js33
2 files changed, 30 insertions, 20 deletions
diff --git a/src/app.js b/src/app.js
index 4195ef6..5146414 100644
--- a/src/app.js
+++ b/src/app.js
@@ -66,7 +66,7 @@ const ui = {
giftsPage: 0, // History: current page within the all-gifts list
addrScan: false, // Settings: showing the per-address rescan list
addrScanPage: 0, // Settings: current page within the address list
- rescanningAddr: null, // 'chain/index' of the address currently rescanning
+ rescanning: new Set(), // 'chain/index' ids queued/in-flight for rescan
send: blankSend(),
draft: null, // built tx summary awaiting review
broadcastTx: null, // scanned signed tx awaiting broadcast confirmation
@@ -915,18 +915,21 @@ function brandHeader(withLock) {
// The phrase is gated: the real words are never put in the DOM until "Reveal",
// so the warning is read first.
// Rescan a single address on demand — recovers a deposit to a reused old
-// address without a full wallet rescan.
+// address without a full wallet rescan. Multiple can be queued at once; the
+// API layer's global scheduler serializes and spaces the underlying requests
+// (and backs off on 429), so a flurry of clicks stays within the rate limit.
async function doRescanAddress(chain, index) {
if (wallet.offline) { toast(t('scanOffline')); return; }
- ui.rescanningAddr = chain + '/' + index;
+ const id = chain + '/' + index;
+ if (ui.rescanning.has(id)) return; // already queued
+ ui.rescanning.add(id);
render();
try {
await wallet.rescanAddress(chain, index);
- toast(t('rescanDone'));
} catch (e) {
toast(e.message || t('rescanFailed'));
}
- ui.rescanningAddr = null;
+ ui.rescanning.delete(id);
render();
}
@@ -948,7 +951,7 @@ function addressScanView() {
h('div', { class: 'list' },
...slice.map((a) => {
const id = a.chain + '/' + a.index;
- const busy = ui.rescanningAddr === id;
+ const busy = ui.rescanning.has(id);
return h('div', { class: 'item' },
h('div', { class: 'grow' },
h('div', { class: 'mono small break' }, shortAddr(a.address, 16, 10),
@@ -957,7 +960,7 @@ function addressScanView() {
),
busy
? h('button', { class: 'btn-sm', disabled: true }, h('span', { class: 'spinner sm' }))
- : h('button', { class: 'btn-sm', disabled: !!ui.rescanningAddr, onClick: () => doRescanAddress(a.chain, a.index) }, t('rescanOne'))
+ : h('button', { class: 'btn-sm', onClick: () => doRescanAddress(a.chain, a.index) }, t('rescanOne'))
);
})
),
diff --git a/src/wallet.js b/src/wallet.js
index 0f2a763..54e427a 100644
--- a/src/wallet.js
+++ b/src/wallet.js
@@ -367,6 +367,7 @@ export class Wallet {
this._recomputeBalanceFromChains();
this.loaded = true;
this.saveCache();
+ this.retrack(); // a new pending coin may have changed the watched set
this.emit();
}
} catch {
@@ -477,6 +478,7 @@ export class Wallet {
this._recomputeBalanceFromChains();
this.loaded = true;
this.saveCache();
+ this.retrack(); // watch this address over the socket if its coin is pending
this.emit();
return used;
}
@@ -1105,29 +1107,34 @@ export class Wallet {
return wsUrl(this.netName);
}
- // Only the fresh frontier needs realtime watching: the next receive address
- // (incoming deposits) and the next change address (our change landing). Coin
- // spends are handled by the post-send refresh and cross-device Nostr sync, so
- // we don't track coin addresses — which also stays under the relay's 10-addr
- // per-connection limit.
+ // What realtime watches: the fresh frontier (next receive + next change), plus
+ // any address holding an unconfirmed coin — so a pending deposit's confirmation
+ // pushes instantly instead of waiting for a poll. Capped at the socket's 10-
+ // address-per-connection limit (frontier first, then pending coins).
watchedAddresses() {
- return [
+ const set = new Set([
this.derive(0, this.nextReceiveIndex).address,
this.derive(1, this.nextChangeIndex).address,
- ];
+ ]);
+ for (const u of this.utxos) {
+ if (set.size >= 10) break;
+ if (!u.confirmed) set.add(u.address);
+ }
+ return [...set];
}
startRealtime() {
if (this.offline) return;
this.stopRealtime();
- // Frontier poll (next receive/change address only). While the socket is live
- // it pushes deposits instantly, so we poll just as a slow half-open-socket
- // safety net (~25s). When the socket is down, poll fast (~6s) so receives
- // still show quickly. The 3s tick is a cheap time check — it issues no
- // request unless one is due, which keeps REST volume (and 429s) low.
+ // Frontier poll (next receive/change + any pending-coin address). While the
+ // socket is live and nothing is pending it's just a slow half-open-socket
+ // safety net (~25s). When the socket is down, OR a coin is waiting to
+ // confirm, poll fast (~6s) so receives and confirmations show quickly. The
+ // 3s tick is a cheap time check — it issues no request unless one is due.
this._pollTimer = setInterval(() => {
if (this.offline) return;
- const due = this.live ? 25000 : 6000;
+ const pending = this.utxos.some((u) => !u.confirmed);
+ const due = this.live && !pending ? 25000 : 6000;
if (Date.now() - (this._lastPoll || 0) < due) return;
this._lastPoll = Date.now();
this.refreshLive();