From 0761d901ed1f6468a5497e87efb2d21b1e222a02 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Mon, 21 Sep 2026 10:26:38 -0700 Subject: Ark: a Lightning invoice steps past a preimage index another device already spent. The ASP answered "invoice has already been paid" for a point-of-sale invoice: the receive counter on the phone was behind the one the laptop had used. createLnInvoice now retries on that answer with the next index (the counter only moves up; the key index is allocated once an invoice exists), so the Receive tab, NWC make_invoice and the point of sale all recover instead of failing. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Tmfg8k5f3TbNJS34qiqHAp --- src/ark/manager.js | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) (limited to 'src/ark') diff --git a/src/ark/manager.js b/src/ark/manager.js index 1381095..3477638 100644 --- a/src/ark/manager.js +++ b/src/ark/manager.js @@ -1338,14 +1338,29 @@ export class ArkManager { if (this.info.maxUserInvoiceCltvDelta && minCltvDelta > this.info.maxUserInvoiceCltvDelta) { throw new Error('server max invoice CLTV delta is too low'); } - const idx = this.state.nextLnRecvIndex || 0; - this.state.nextLnRecvIndex = idx + 1; + // The preimage comes from a counter. Another device can have spent an + // index this one hasn't heard about yet (the merge keeps the higher + // count, but only once the state has travelled), and the server then + // refuses the hash as already paid. Step past any such index rather + // than fail the sale — the counter only ever moves up. + let idx, paymentHash, invoice; + for (let tries = 0; ; tries++) { + idx = this.state.nextLnRecvIndex || 0; + this.state.nextLnRecvIndex = idx + 1; + paymentHash = sha256(this._lnPreimage(idx)); + try { + invoice = await startLightningReceive(this.arkUrl, { + paymentHash, amountSat, minCltvDelta, + mailboxPubkey: this._mailboxKey().pubkey, description, + }); + break; + } catch (e) { + const used = /already been paid|already exists|already in use|duplicate/i.test(e?.message || ''); + if (!used || tries >= 50) throw e; + this._save(); // remember the skipped index even if the next try fails + } + } const keyIndex = this.state.nextKeyIndex++; - const paymentHash = sha256(this._lnPreimage(idx)); - const invoice = await startLightningReceive(this.arkUrl, { - paymentHash, amountSat, minCltvDelta, - mailboxPubkey: this._mailboxKey().pubkey, description, - }); const decInv = decodeBolt11(invoice); if (decInv.paymentHash !== hex.encode(paymentHash)) { throw new Error('server invoice payment hash mismatch'); -- cgit v1.2.3