diff options
Diffstat (limited to 'names/server.js')
| -rw-r--r-- | names/server.js | 45 |
1 files changed, 37 insertions, 8 deletions
diff --git a/names/server.js b/names/server.js index 2e2634c..f8fa378 100644 --- a/names/server.js +++ b/names/server.js @@ -382,7 +382,29 @@ function recordLnReceipt(name, sat, hash) { const HOLD_KEYS = new Set(['a7fc7e2cbc16949c411a982fe6cecec0c1674fb4dabbdce5702757de90386d67', '42a0887a748f535f0796a55601bb3d2a3bfa537b13869e9185fba26222a0a005']); +// One payment, one delivery, whoever asks: the settle loop, the retry tick +// and the operator's /admin/forward all come through here. A sale being +// delivered right now refuses a second caller (it stays queued), and a sale +// delivered once is never delivered again — the record is saved before +// anything else happens. +const delivering = new Set(); async function forward(name, sat, hash, pubkey) { + if (hash && state.delivered?.[hash]) { log(`forward: ${hash.slice(0, 12)} was already delivered — not again`); return; } + if (hash && delivering.has(hash)) throw new Error('already being delivered'); + if (hash) delivering.add(hash); + try { + await forwardOnce(name, sat, hash, pubkey); + if (hash) { + const all = state.delivered ||= {}; + all[hash] = Date.now(); + const cutoff = Date.now() - 60 * 86400_000; + for (const [h, t] of Object.entries(all)) if (t < cutoff) delete all[h]; + persist(); + } + } finally { if (hash) delivering.delete(hash); } +} + +async function forwardOnce(name, sat, hash, pubkey) { let rec = state.names[name]; if (HOLD_KEYS.has(pubkey) || HOLD_KEYS.has(rec?.pubkey)) { throw Object.assign(new Error(`${name} is held for review (double-payout claims, 2026-10-27)`), { permanent: true }); @@ -699,16 +721,21 @@ async function settleLoop() { } // a released name's custom offer still knows its owner's key const pubkey = state.names[name]?.pubkey || (offerId && state.offers?.[offerId]?.pubkey) || undefined; + // on the queue BEFORE the first try: a restart mid-delivery used to lose + // the payment (the settle index had moved on, nothing was queued); the + // queue's retry now finds it, and forward()'s records keep that from + // paying twice + const p = { name, sat, hash: inv.payment_hash, pubkey, ts: Date.now() }; + (state.pending ||= []).push(p); + persist(); try { await forward(name, sat, inv.payment_hash, pubkey); + state.pending = (state.pending || []).filter((x) => x !== p); } catch (e) { - const p = { name, sat, hash: inv.payment_hash, pubkey, ts: Date.now() }; - if (queueFailedForward(p, e)) { - log(`forward failed for ${name} (${sat} sat): ${e.message} — queued`); - (state.pending ||= []).push(p); - } - persist(); + if (queueFailedForward(p, e)) log(`forward failed for ${name} (${sat} sat): ${e.message} — queued`); + else state.pending = (state.pending || []).filter((x) => x !== p); } + persist(); } } // -- float self-top-up ------------------------------------------------------- @@ -906,7 +933,7 @@ async function retryTick() { if ((await smallCoins().catch(() => [])).length > CONSOLIDATE_AT) { await consolidateFloat().catch((e) => log('float consolidate failed: ' + e.message)); } - const q = state.pending || []; + const q = [...(state.pending || [])]; if (!q.length) return; const still = []; for (const p of q) { @@ -923,7 +950,9 @@ async function retryTick() { if (queueFailedForward(p, e)) still.push(p); } } - state.pending = still; + // (only the ones this tick looked at: the settle loop may have queued more + // while it ran, and a plain `= still` dropped those) + state.pending = (state.pending || []).filter((p) => !q.includes(p) || still.includes(p)); persist(); } |
