diff options
| author | Adam Soltys <[email protected]> | 2026-08-28 21:08:33 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-28 21:08:33 -0700 |
| commit | 065d50b3ef553f540c6471c60abd804abed61b28 (patch) | |
| tree | 6f0661549ad23464d8c95b171c1da9e87d90668c /src | |
| parent | f13acc1bf4ffa3d0e2733dc53041bbc61659a5af (diff) | |
| download | coinosv3-065d50b3ef553f540c6471c60abd804abed61b28.tar.gz coinosv3-065d50b3ef553f540c6471c60abd804abed61b28.zip | |
One payment, one line: the settle path is idempotent (the mailbox push and the status poll raced past each other's step checks — a toque got two history entries), movements carry their payment hash, and the sync merge folds same-preimage lightning records into the earliest telling
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019eA8JRq9Wr7z2fRQk99HLi
Diffstat (limited to 'src')
| -rw-r--r-- | src/ark/manager.js | 10 | ||||
| -rw-r--r-- | src/features/ark.js | 14 |
2 files changed, 21 insertions, 3 deletions
diff --git a/src/ark/manager.js b/src/ark/manager.js index a2c69d8..cfb7436 100644 --- a/src/ark/manager.js +++ b/src/ark/manager.js @@ -990,16 +990,22 @@ export class ArkManager { } _settleLnPay(action, preimageHex) { + // The mailbox push and the status poll both land here, and their step + // checks happen BEFORE awaits — settle once, however many callers race + // (a hat purchase once wrote its history line twice this way). + if (action.step === 'done') return; for (const id of action.htlcVtxoIds || [action.htlcVtxoId]) { const htlc = this._vtxo(id); if (htlc) htlc.state = 'spent'; } action.preimage = preimageHex; action.step = 'done'; - this._movement({ + const dup = this.state.movements.some((m) => + m.type === 'ln-send' && (m.paymentHash === action.paymentHash || (m.preimage && m.preimage === preimageHex))); + if (!dup) this._movement({ type: 'ln-send', amountSat: action.amountSat, status: 'complete', detail: action.feeSat ? `fee ${action.feeSat} sat` : undefined, - invoice: action.invoice, preimage: preimageHex, + invoice: action.invoice, preimage: preimageHex, paymentHash: action.paymentHash, }); this._save(); } diff --git a/src/features/ark.js b/src/features/ark.js index 3e614c4..15b7687 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -175,7 +175,19 @@ export function mergeArkStates(a, b) { return [...x, ...y.filter((i) => !ids.has(i.id))]; }; out.actions = unionById(a.actions, b.actions); - out.movements = unionById(a.movements, b.movements).sort((m, n) => (m.ts || 0) - (n.ts || 0)); + // Two devices (or the push and the poll, pre-guard) can each record the + // SAME lightning payment under different movement ids — same preimage is + // the same payment, keep the earliest telling of it. + const seenPay = new Set(); + out.movements = unionById(a.movements, b.movements) + .sort((m, n) => (m.ts || 0) - (n.ts || 0)) + .filter((m) => { + if (!m.type || !m.type.startsWith('ln-') || m.status !== 'complete') return true; + const key = m.type + ':' + (m.paymentHash || m.preimage || m.invoice || m.id); + if (seenPay.has(key)) return false; + seenPay.add(key); + return true; + }); const gifts = new Map((a.gifts || []).map((g) => [g.id, { ...g }])); for (const rg of b.gifts || []) { const lg = gifts.get(rg.id); |
