aboutsummaryrefslogtreecommitdiffstats
path: root/src/ark/manager.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-21 11:51:16 -0700
committerAdam Soltys <[email protected]>2026-09-21 11:51:16 -0700
commit85c7256c86e6640f5424c8a1412d0323e4396aed (patch)
tree7760ade5d92704bd916d0e4c400a1e024fc0dc48 /src/ark/manager.js
parent4befa5c5e22ace56415e24321ceb4d9589b27fb3 (diff)
downloadcoinosv3-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.js26
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,