diff options
| author | Adam Soltys <[email protected]> | 2026-06-14 12:34:12 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-17 07:47:47 -0700 |
| commit | 51847c5e05ad70e936fd374e01e62e0e4b5e3c72 (patch) | |
| tree | 83d6c25a90a94e2469e1fae2f7d837cce765a838 /lib | |
| parent | e55165307096093d769893475a7481c781457f94 (diff) | |
| download | coinos-server-51847c5e05ad70e936fd374e01e62e0e4b5e3c72.tar.gz coinos-server-51847c5e05ad70e936fd374e01e62e0e4b5e3c72.zip | |
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 <[email protected]>
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/payments.ts | 26 |
1 files changed, 23 insertions, 3 deletions
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; |
