aboutsummaryrefslogtreecommitdiffstats
path: root/lib/payments.ts
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-07-10 22:55:56 -0700
committerAdam Soltys <[email protected]>2026-07-10 22:55:56 -0700
commit9f06a2d677e579609ab70e4f8dc0742d112cda47 (patch)
treec7c6f8d015e9fa2db2d54c883cdcdff9e7edc213 /lib/payments.ts
parentc5bcf51d6adb1c7718f08ff11a9652762c51669d (diff)
downloadcoinos-server-9f06a2d677e579609ab70e4f8dc0742d112cda47.tar.gz
coinos-server-9f06a2d677e579609ab70e4f8dc0742d112cda47.zip
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) <[email protected]>
Diffstat (limited to 'lib/payments.ts')
-rw-r--r--lib/payments.ts13
1 files changed, 10 insertions, 3 deletions
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 });