aboutsummaryrefslogtreecommitdiffstats
path: root/lib/payments.ts
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-11-02 12:55:05 -0700
committerAdam Soltys <[email protected]>2026-11-02 12:55:05 -0700
commiteee65266b7843764b7544a259cba9ec94a67a392 (patch)
tree025d7fc70a4cc08284488d10b957ddd21b664aac /lib/payments.ts
parent8fbc6ea19c9f4959799f3d72b32e2b8661596ea8 (diff)
downloadcoinos-server-eee65266b7843764b7544a259cba9ec94a67a392.tar.gz
coinos-server-eee65266b7843764b7544a259cba9ec94a67a392.zip
payments: cap the routing fee a lightning send may reserve
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6VN9xvnX1cgvV6hrUV
Diffstat (limited to 'lib/payments.ts')
-rw-r--r--lib/payments.ts11
1 files changed, 11 insertions, 0 deletions
diff --git a/lib/payments.ts b/lib/payments.ts
index 46682670..407e5eac 100644
--- a/lib/payments.ts
+++ b/lib/payments.ts
@@ -830,6 +830,15 @@ export const sendOnchain = async (params) => {
}
};
+// Ceiling on the routing fee a send may reserve: 5% of the amount or 1000
+// sats, whichever is larger. An invoice can carry route hints with arbitrary
+// fees, and whoever controls those hints keeps the difference, so a caller-
+// named fee must not be trusted past what real routes cost.
+const checkRoutingFee = (amount, fee) => {
+ const max = Math.max(1000, Math.ceil(amount * 0.05));
+ if (fee > max) fail(`Routing fee of ⚡️${fee} exceeds the ⚡️${max} limit`);
+};
+
export const sendKeysend = async ({
hash,
amount,
@@ -841,6 +850,7 @@ export const sendKeysend = async ({
maxTotal = undefined,
}) => {
fee = Math.max(Number.parseInt(fee || amount * 0.005), 5);
+ checkRoutingFee(amount, fee);
let p = await gf(`payment:${hash}`);
if (p) fail("duplicate keysend");
@@ -1052,6 +1062,7 @@ export const sendLightning = async ({
if (channels.some((c) => c.peer_id === payee)) minfee = 0;
fee = minfee;
}
+ checkRoutingFee(amt, fee);
const { pays } = await ln.listpays(pr);
if (pays.find((p) => p.status === "complete"))