From cb0f1962238534ce0fa2261373a018bf34a868f5 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 21 May 2026 07:35:42 -0700 Subject: sendLightning: silence success-case xpay log, re-read db before leak guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two cleanups based on a day of production data: 1. Only log the xpay response shape when something is abnormal (failed_parts > 0 or missing preimage). The success case (200+/day) was pure noise — answers Adam's "do we still need these xpay warnings". 2. Re-read the payment record from db before the leak guard fires. Several "LEAKED DEBIT" lines today were finalize() throwing on a late line (likely db.incrBy for the fee refund) AFTER db.set had already persisted p.ref. The payment is functionally settled in those cases — only flag as a real leak if ref is genuinely missing from the db record. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/payments.ts | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) (limited to 'lib') diff --git a/lib/payments.ts b/lib/payments.ts index 5e073715..e8fec540 100644 --- a/lib/payments.ts +++ b/lib/payments.ts @@ -707,16 +707,18 @@ export const sendLightning = async ({ layers: ["prefer-kappa"], }); - // Dump the full xpay response shape so we can see what CLN actually - // returned — especially failed_parts, missing preimage, partial sends. - warn("xpay returned", p.id, JSON.stringify({ - status: r?.status, - failed_parts: r?.failed_parts, - parts: r?.parts, - has_preimage: !!(r?.payment_preimage ?? r?.preimage), - amount_sent_msat: r?.amount_sent_msat, - amount_msat: r?.amount_msat, - })); + // Only log the xpay response shape when there's something interesting — + // failed_parts > 0 or a missing preimage. The success case is noise. + if (r?.failed_parts || !(r?.payment_preimage ?? r?.preimage)) { + warn("xpay returned (abnormal)", p.id, JSON.stringify({ + status: r?.status, + failed_parts: r?.failed_parts, + parts: r?.parts, + has_preimage: !!(r?.payment_preimage ?? r?.preimage), + amount_sent_msat: r?.amount_sent_msat, + amount_msat: r?.amount_msat, + })); + } if (r.failed_parts) { // xpay didn't throw but reported failed parts. The old code path @@ -779,11 +781,11 @@ export const sendLightning = async ({ throw e; } - // Leak guard: outcomes that leave a confirmed=true debit with no ref AND - // no entry in the pending set are the joho33-style stuck state. Pending-* - // outcomes are fine — check() will reconcile. Finalize-via-* outcomes - // are clean successes via a fallback path. The truly stranded cases are - // the finalize-threw-* family. + // Leak guard: re-read the db record before deciding. If the payment has + // ref set in db, it's settled regardless of which code path threw — + // finalize() may have set ref via db.set and then thrown on a later line + // (e.g. balance.incrBy for the fee refund), but the payment itself is + // recorded as complete. Only treat as a real leak if ref is not in db. const finalizedOutcomes = new Set([ "finalized", "finalized-via-listpays-after-failed_parts", @@ -797,8 +799,10 @@ export const sendLightning = async ({ "verify-failed-after-failed_parts", "verify-failed-after-xpay-throw", ]); - if (finalizedOutcomes.has(outcome)) { - l("sendLightning outcome", p.id, "=", outcome); + const persisted = await g(`payment:${p.id}`); + const refInDb = !!persisted?.ref; + if (refInDb || finalizedOutcomes.has(outcome)) { + l("sendLightning outcome", p.id, "=", outcome, refInDb && !finalizedOutcomes.has(outcome) ? "(ref set in db despite throw)" : ""); } else if (safeNonFinalized.has(outcome)) { warn("sendLightning outcome", p.id, "=", outcome); } else { -- cgit v1.2.3