From 9f06a2d677e579609ab70e4f8dc0742d112cda47 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 10 Jul 2026 22:55:56 -0700 Subject: payments: use the debited tip when crediting internal transfers credit() now credits the tip the sender was actually debited (passed in by the internal-transfer callers) instead of independently re-reading the invoice. This closes a time-of-check/time-of-use gap where an invoice's tip could be changed between the sender debit and the receiver credit, letting a payer be credited a tip they were never charged. External receipts (lightning/ecash) keep reading the invoice tip, since there is no paired coinos debit to diverge from. Also harden POST /invoice/:id: validate the tip is a sane non-negative integer and reject tip changes on an already-settled invoice. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/payments.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/payments.ts b/lib/payments.ts index c715af47..c256171c 100644 --- a/lib/payments.ts +++ b/lib/payments.ts @@ -273,6 +273,7 @@ export const credit = async ({ aid = undefined, payment_hash = undefined, created = undefined, + tip = undefined, }) => { amount = Number.parseInt(amount) || 0; @@ -288,8 +289,14 @@ export const credit = async ({ return; } - let { path, tip } = inv; - tip = Number.parseInt(tip) || 0; + let { path } = inv; + // Use the tip the sender was actually debited (passed in for internal + // transfers) rather than re-reading the invoice. The invoice's tip can be + // mutated between the sender debit and this credit (PUT /invoice), which + // otherwise lets a payer credit a tip they were never charged — minting + // balance. Fall back to the invoice tip only for external receipts + // (lightning/ecash) where there is no paired coinos debit to diverge from. + tip = tip !== undefined ? Number.parseInt(tip) || 0 : Number.parseInt(inv.tip) || 0; if (!memo) ({ memo } = inv); if (memo && memo.length > 5000) fail("memo too long"); @@ -890,7 +897,7 @@ export const sendInternal = async ({ const { hash } = invoice; const p = await debit({ hash, amount, memo, user: sender }); - await credit({ hash, amount, memo, ref: sender.id }); + await credit({ hash, amount, memo, ref: sender.id, tip: p.tip }); if (invoice.memo?.includes("9734")) { const { invoices } = await ln.listinvoices({ invstring: hash }); -- cgit v1.2.3