aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-14 12:34:12 -0700
committerAdam Soltys <[email protected]>2026-06-17 07:47:47 -0700
commit51847c5e05ad70e936fd374e01e62e0e4b5e3c72 (patch)
tree83d6c25a90a94e2469e1fae2f7d837cce765a838 /lib
parente55165307096093d769893475a7481c781457f94 (diff)
downloadcoinos-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.ts26
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;