aboutsummaryrefslogtreecommitdiffstats
path: root/names
diff options
context:
space:
mode:
Diffstat (limited to 'names')
-rw-r--r--names/server.js45
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();
}