From 51847c5e05ad70e936fd374e01e62e0e4b5e3c72 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 14 Jun 2026 12:34:12 -0700 Subject: finalize: don't crash on non-integer fee delta (tbRefund NaN) Port of master 8aba0bc2, adapted to this stack's tbRefund. When xpay's response shape is unexpected (or a zero-amount invoice's decoded amount_msat is undefined) the computed p.fee becomes NaN, and the refund delta crashes the balance write, leaving finalize partial (user loses up to maxfee). Validate the computed fee is a finite integer; if not, keep the originally-reserved maxfee (zero refund, no balance corruption) and warn. Same guard on the refund delta before tbRefund. Co-Authored-By: Claude Opus 4.8 --- lib/payments.ts | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/payments.ts b/lib/payments.ts index bb2a055d..e4d4fd0f 100644 --- a/lib/payments.ts +++ b/lib/payments.ts @@ -1890,15 +1890,35 @@ const finalize = async (r, p) => { nwcNotify(p); const maxfee = p.fee; + // Compute the actual fee xpay paid (sent − invoice amount, both msat). + // Robust against zero-amount invoices (decoded amount_msat is undefined, fall + // back to invoice_amount_msat then r.amount_msat) and against CLN occasionally + // returning unexpected response shapes — if we can't compute a finite integer + // fee, keep p.fee at the originally-reserved maxfee so the subsequent refund + // delta is 0 instead of NaN (which would crash the tbRefund balance write and + // leave finalize in a partial state). const { amount_msat, invoice_amount_msat } = await ln.decode(p.hash); - p.fee = Math.round((r.amount_sent_msat - (amount_msat || invoice_amount_msat)) / 1000); + const invMsat = Number(amount_msat ?? invoice_amount_msat ?? r.amount_msat); + const sentMsat = Number(r.amount_sent_msat); + const computedFee = Math.round((sentMsat - invMsat) / 1000); + if (!Number.isFinite(computedFee)) { + warn("finalize: non-finite fee compute, keeping reserved maxfee", p.id, + "sent_msat=", r.amount_sent_msat, "inv_msat=", amount_msat ?? invoice_amount_msat ?? r.amount_msat); + } + p.fee = Number.isFinite(computedFee) ? computedFee : maxfee; p.ref = preimage; if (!(await g(`payment:${p.id}`)).ref) { await s(`payment:${p.id}`, p); - l("refunding fee", maxfee, p.fee, maxfee - p.fee, p.ref); - await tbRefund(p.uid, maxfee - p.fee); + const refund = maxfee - p.fee; + if (Number.isFinite(refund) && Number.isInteger(refund)) { + l("refunding fee", maxfee, p.fee, refund, p.ref); + await tbRefund(p.uid, refund); + } else { + warn("finalize: skipping fee refund (non-integer delta)", p.id, + "maxfee=", maxfee, "p.fee=", p.fee, "refund=", refund); + } } return p; -- cgit v1.2.3