From 065d50b3ef553f540c6471c60abd804abed61b28 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 28 Aug 2026 21:08:33 -0700 Subject: 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 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019eA8JRq9Wr7z2fRQk99HLi --- src/ark/manager.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'src/ark') 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(); } -- cgit v1.2.3