diff options
| author | Adam Soltys <[email protected]> | 2026-09-10 07:28:54 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-10 07:28:54 -0700 |
| commit | f2de9b1330dfad5990f8e0ea6130b91096a60e54 (patch) | |
| tree | ec87386a91fa126d9e53877da2e38d884c312cdf /src/features | |
| parent | af4810e2ec8e454e4bb616c685e907eaee04c874 (diff) | |
| download | coinosv3-f2de9b1330dfad5990f8e0ea6130b91096a60e54.tar.gz coinosv3-f2de9b1330dfad5990f8e0ea6130b91096a60e54.zip | |
Sync: stop resurrecting stale pending txs from other devices' snapshots.
A 5,000-sat send built on 2026-09-06 that the network never took sat in the
history as a pending withdrawal for four days on every device. The local
scan prunes such rows (explorer 404 / absent from address history), but
the boot-time cross-device history union re-adopted it from a stale device
slot's core snapshot, and the device then republished it — a phantom that
no explorer knows, immortal across devices.
The union now adopts a remote PENDING row only while it is under a day old
(it has to prove itself to the local scan); confirmed rows still union as
before, and the device's own rows are untouched.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Nw1Dijik1tLVKRM1WkU7xE
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/sync.js | 14 |
1 files changed, 12 insertions, 2 deletions
diff --git a/src/features/sync.js b/src/features/sync.js index a800ef8..415717f 100644 --- a/src/features/sync.js +++ b/src/features/sync.js @@ -223,9 +223,19 @@ export function installSyncWallet(wallet, { outbox = syncOutbox() } = {}) { // 2026-09-04) while a fat core sat shadowed in an older device slot. // Safe because history is append-mostly and every SUCCESSFUL local // scan still prunes rows its addresses no longer report. + // …but a PENDING row from another device's snapshot is only adopted + // while it is fresh. A tx the wallet built that the network never took + // (or that was replaced) is pruned by the local scan, yet a stale device + // slot keeps publishing it — and this union used to resurrect it on + // every boot, then republish it: a phantom "withdrawal" that no explorer + // knows, immortal across devices. Confirmed rows are history; pending + // ones must prove themselves to the local scan within a day. + const FRESH_MS = 24 * 3600_000; + const adoptable = (t) => t.confirmed || Date.now() - (t.firstSeen || 0) < FRESH_MS; const byId = new Map(); - for (const list of [this.txs || [], localTxs, ...fulls.map((s) => s.state.txs || [])]) { - for (const t of list) if (t && t.txid && !byId.has(t.txid)) byId.set(t.txid, t); + for (const t of [...(this.txs || []), ...localTxs]) if (t && t.txid && !byId.has(t.txid)) byId.set(t.txid, t); + for (const s of fulls) { + for (const t of s.state.txs || []) if (t && t.txid && adoptable(t) && !byId.has(t.txid)) byId.set(t.txid, t); } if (byId.size > (this.txs || []).length) { this.txs = [...byId.values()]; |
