diff options
| author | Adam Soltys <[email protected]> | 2026-09-21 11:51:16 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-21 11:51:16 -0700 |
| commit | 85c7256c86e6640f5424c8a1412d0323e4396aed (patch) | |
| tree | 7760ade5d92704bd916d0e4c400a1e024fc0dc48 /src/ark/manager.js | |
| parent | 4befa5c5e22ace56415e24321ceb4d9589b27fb3 (diff) | |
| download | coinosv3-85c7256c86e6640f5424c8a1412d0323e4396aed.tar.gz coinosv3-85c7256c86e6640f5424c8a1412d0323e4396aed.zip | |
Ark: an invoice the server hands back for a hash it already holds is not ours.
StartLightningReceive on a payment hash with an unpaid invoice returns that
invoice — an earlier sale's amount and memo under a fresh QR (a 0.60 CAD
bill showed an 8,467-sat invoice on 2026-09-21). The returned invoice must
now carry the amount asked for and a comfortable expiry, or the index is
treated as spoken for and skipped. The counter is also floored above every
index an ln-recv action of ours already names, so a restored or merged
state can't rewind it.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Tmfg8k5f3TbNJS34qiqHAp
Diffstat (limited to 'src/ark/manager.js')
| -rw-r--r-- | src/ark/manager.js | 26 |
1 files changed, 20 insertions, 6 deletions
diff --git a/src/ark/manager.js b/src/ark/manager.js index 18c69b7..0440263 100644 --- a/src/ark/manager.js +++ b/src/ark/manager.js @@ -1343,7 +1343,12 @@ export class ArkManager { // 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; + // A restored or merged state can also carry a counter BELOW indices this + // wallet already used: never start under one an action of ours names. + for (const a of this.state.actions || []) { + if (a.type === 'ln-recv' && Number.isInteger(a.preimageIndex) && a.preimageIndex >= (this.state.nextLnRecvIndex || 0)) this.state.nextLnRecvIndex = a.preimageIndex + 1; + } + let idx, paymentHash, invoice, decInv; for (let tries = 0; ; tries++) { idx = this.state.nextLnRecvIndex || 0; this.state.nextLnRecvIndex = idx + 1; @@ -1353,7 +1358,6 @@ export class ArkManager { paymentHash, amountSat, minCltvDelta, mailboxPubkey: this._mailboxKey().pubkey, description, }); - break; } catch (e) { // "already been paid" for a settled one; "UNIQUE constraint failed: // invoices.payment_hash" for one merely issued before — both mean @@ -1361,13 +1365,23 @@ export class ArkManager { const used = /already been paid|already exists|already in use|duplicate|UNIQUE constraint|payment_hash/i.test(e?.message || ''); if (!used || tries >= 200) throw e; this._save(); // remember the skipped index even if the next try fails + continue; + } + decInv = decodeBolt11(invoice); + if (decInv.paymentHash !== hex.encode(paymentHash)) { + throw new Error('server invoice payment hash mismatch'); } + // The server answers a hash it already holds an unpaid invoice for + // with THAT invoice — an earlier sale's amount and memo under a new + // QR (seen live 2026-09-21: a 0.60 CAD bill showed an 8,467-sat + // invoice). Anything but a fresh invoice for this amount, with a + // comfortable expiry, means the index is spoken for. + const fresh = decInv.amountSat === amountSat && (!decInv.expiresAt || decInv.expiresAt > Date.now() + 10 * 60_000); + if (fresh) break; + if (tries >= 200) throw new Error('could not get a fresh invoice from the server'); + this._save(); } const keyIndex = this.state.nextKeyIndex++; - const decInv = decodeBolt11(invoice); - if (decInv.paymentHash !== hex.encode(paymentHash)) { - throw new Error('server invoice payment hash mismatch'); - } const action = { id: `lnrecv-${Date.now()}`, type: 'ln-recv', step: 'awaiting', paymentHash: hex.encode(paymentHash), preimageIndex: idx, keyIndex, |
