From eee65266b7843764b7544a259cba9ec94a67a392 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Mon, 2 Nov 2026 12:55:05 -0700 Subject: payments: cap the routing fee a lightning send may reserve Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VMan6VN9xvnX1cgvV6hrUV --- lib/payments.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'lib') 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")) -- cgit v1.2.3