From 71fa56f0bad19d7ea7758ab6aef2ffccd46e3c5b Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 5 Jul 2026 21:09:14 -0700 Subject: Dedupe ark receive movements The mailbox poll and live stream can process the same message concurrently (validation awaits in between): the vtxo itself deduped but both copies pushed a receive movement, so a claimed gift showed twice in History. The movement now only records when _addVtxo actually added the vtxo, and init() prunes duplicates the old race left behind. Co-Authored-By: Claude Fable 5 --- src/ark/manager.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) (limited to 'src/ark') diff --git a/src/ark/manager.js b/src/ark/manager.js index cfc37a9..86d496a 100644 --- a/src/ark/manager.js +++ b/src/ark/manager.js @@ -79,6 +79,15 @@ export class ArkManager { // ---- lifecycle ---- async init() { this.state = this.storage.load() || EMPTY_STATE(); + // drop duplicate receive movements (same vtxo) left by the former + // poll/stream race; the vtxo set itself was always deduped + const seen = new Set(); + this.state.movements = this.state.movements.filter((m) => { + if (m.type !== 'receive' || !m.vtxoId) return true; + if (seen.has(m.vtxoId)) return false; + seen.add(m.vtxoId); + return true; + }); this.info = await getArkInfo(this.arkUrl); const hs = await handshake(this.arkUrl).catch(() => null); this.psa = hs?.psa; @@ -192,7 +201,10 @@ export class ArkManager { } throw e; // network errors etc: retry next sync } - this._addVtxo(v, v._raw.bytes, 0); + // _addVtxo dedupes by id — the poll and the live stream can process the + // same message concurrently (validation awaits in between), and only the + // copy that actually added the vtxo may record the receive movement. + if (!this._addVtxo(v, v._raw.bytes, 0)) continue; this._movement({ type: 'receive', amountSat: v.amountSat, status: 'complete', vtxoId: v.id }); changed = true; } -- cgit v1.2.3