From 0e368dab0943067c060ad45578a4c4cbdc3434a6 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 17 May 2026 23:22:51 -0700 Subject: mirror payment records to arc + atomic /confirm lock to prevent double-credits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follows up the Apr 29 double-credit incident. Two parallel callers of /confirm (walletnotify, catchUp, bulk sweeps) could each read p.confirmed=false and each run the multi() that increments balance — because main-db pointers were sometimes missing while arc still had them, gf()'s arc fallback returned stale state. Three pieces here: - routes/payments.ts: SET NX EX 60 lock around the /confirm credit block keyed by `confirmlock:${txid}:${vout}`. Concurrent callers short-circuit cleanly. The lock is released on success and on every early-return. - lib/payments.ts credit() + routes/payments.ts /confirm: mirror the payment/invoice records and the `payment::` / `payment:` pointer keys into arc as soon as they're written. gf() will then find the prior credit via arc fallback even if the main-db copy is wiped by a future data-loss event. - routes/payments.ts hide `revertedDuplicate` payments from the user's payment list — matches the same filter added to lib/nwc.ts list_transactions so reverted records never surface in either rendering path. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/payments.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/payments.ts b/lib/payments.ts index 9d658294..5d2bf0e6 100644 --- a/lib/payments.ts +++ b/lib/payments.ts @@ -1,6 +1,6 @@ import config from "$config"; import api from "$lib/api"; -import { db, g, ga, gf, s } from "$lib/db"; +import { archive, db, g, ga, gf, s, sa } from "$lib/db"; import { generate } from "$lib/invoices"; import ln from "$lib/ln"; import { err, l, warn } from "$lib/logging"; @@ -298,8 +298,13 @@ export const credit = async ({ p.confirmed = false; balanceKey = "pending"; await s(`payment:${txid}:${vout}`, id); + // Mirror the txid:vout pointer into arc so future bulk /confirm sweeps find + // the prior credit via gf() fallback and don't double-credit. See + // feedback_apr29_double_credit_incident.md for the incident this prevents. + await sa(`payment:${txid}:${vout}`, id); } else { await s(`payment:${hash}`, id); + await sa(`payment:${hash}`, id); } const m = await db.multi(); @@ -323,6 +328,10 @@ export const credit = async ({ .set(`${aid || uid}:payments:last`, p.created) .exec(); + // Mirror the payment record + invoice into arc for the same protection. + await sa(`payment:${p.id}`, p); + await sa(`invoice:${inv.id}`, inv); + if (inv.items?.length) { formatReceipt(inv.items, inv.currency); p.items = inv.items; -- cgit v1.2.3