diff options
| author | Adam Soltys <[email protected]> | 2026-07-31 00:15:59 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-07-31 00:15:59 -0700 |
| commit | 6dc7f9f89a071e43de77bf6dd82cf8d4012ce8c3 (patch) | |
| tree | 342e92453a8554e18e5c3fbab5dfcac90e65b252 /src/features/sp-wallet.js | |
| parent | c264f12ded66f60ab137d05a6e2449f17c98ad68 (diff) | |
| download | coinosv3-6dc7f9f89a071e43de77bf6dd82cf8d4012ce8c3.tar.gz coinosv3-6dc7f9f89a071e43de77bf6dd82cf8d4012ce8c3.zip | |
Sync silent-payment scan across devices so the catch-up runs once
SP state (lastSpScan watermark + found spUtxos) was in the nostr snapshot
but, unlike Ark/gifts, its cache extension wasn't mergeAlways — so it only
applied via the newest full snapshot at load time, last-writer-wins. The
watermark could regress (a device behind on SP, or mid-rescan, publishing a
newer snapshot would reset another device's watermark), so every device
tended to re-grind the ~1M-tweak backlog independently.
Make it a commutative merge like Ark:
- mergeAlways: true, so it merges live and from older snapshots too.
- lastSpScan takes the MAX (monotonic high-water mark, never regresses) —
this is what makes the block catch-up run once across all devices.
- spUtxos is an additive union by outpoint; a coin we already track is never
mutated, and spend detection stays each device's own job (_refreshSpUtxos
drops backend-spent coins), so stale merged-in coins self-heal on refresh.
- Anti-entropy: republish our superset when we know more, guarded so it stops
once devices converge.
A manual rescan still works — it forces from=1 synchronously (no await before
capturing it), so a concurrent max-merge can't sabotage the re-scan.
Verified with a merge unit test driving the real extension (max/union/converge).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Diffstat (limited to 'src/features/sp-wallet.js')
| -rw-r--r-- | src/features/sp-wallet.js | 36 |
1 files changed, 35 insertions, 1 deletions
diff --git a/src/features/sp-wallet.js b/src/features/sp-wallet.js index 2ac3e70..f461d27 100644 --- a/src/features/sp-wallet.js +++ b/src/features/sp-wallet.js @@ -460,9 +460,43 @@ export function installSpWallet(wallet) { wallet.spUtxos = []; wallet.lastSpScan = 0; }); + // Ride the encrypted sync snapshot so the (expensive) SP block catch-up runs + // ONCE across all devices instead of on each. Like Ark, this is a commutative + // merge (mergeAlways) — applied even for snapshots older than local state — so + // whichever device scans furthest carries the rest. wallet.registerCacheExtension({ + mergeAlways: true, save: () => ({ spUtxos: wallet.spUtxos, lastSpScan: wallet.lastSpScan }), - load: (d) => { wallet.spUtxos = d.spUtxos || []; wallet.lastSpScan = d.lastSpScan || 0; }, + load: (d) => { + let grew = false; + // Watermark is a HIGH-water mark: adopt the furthest height any device + // reached, never regress. This is what stops every device re-grinding the + // ~1M-tweak backlog. (A manual rescan forces from=1 synchronously, so it + // still re-scans everything regardless of this.) + const remoteScan = d.lastSpScan || 0; + if (remoteScan > (wallet.lastSpScan || 0)) wallet.lastSpScan = remoteScan; + else if ((wallet.lastSpScan || 0) > remoteScan) grew = true; // our watermark is ahead + // Additive union of found coins by outpoint — bring in ones we lack, but + // NEVER mutate a coin we already track. Spend detection is each device's + // own job (_refreshSpUtxos drops coins the backend reports spent), so a + // stale coin merged in from another device self-heals on the next refresh + // rather than needing spent-state to propagate. + if (Array.isArray(d.spUtxos)) { + const have = new Set(wallet.spUtxos.map((u) => `${u.txid}:${u.vout}`)); + let added = false; + for (const ru of d.spUtxos) { + if (!ru || ru.txid == null || ru.vout == null) continue; + const id = `${ru.txid}:${ru.vout}`; + if (!have.has(id)) { wallet.spUtxos.push(ru); have.add(id); added = true; } + } + if (wallet.spUtxos.length > d.spUtxos.length) grew = true; // we hold coins it lacked + if (added) wallet._recomputeBalanceFromChains(); + } + // Anti-entropy: if we ended up knowing more than this snapshot did, push + // our superset back so the shared slot converges. Guarded, so once every + // device agrees it stops (matches the Ark merge). + if (grew) { try { wallet.saveCache(); } catch {} } + }, }); // Confirmed SP coins are spendable like any other coin — including to fund a // silent payment. Unconfirmed ones are excluded (the sender's tx could be |
