aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-18 11:15:27 -0700
committerAdam Soltys <[email protected]>2026-06-18 11:15:27 -0700
commitd2762384156e3310ca08d7c295dea0e3856f8c9d (patch)
treea25c56a6af40fff8374768a70a9fe0ba983a3619
parent3a60e3a49a031c8efea067b3c142b086bb6f5847 (diff)
downloadcoinos-server-d2762384156e3310ca08d7c295dea0e3856f8c9d.tar.gz
coinos-server-d2762384156e3310ca08d7c295dea0e3856f8c9d.zip
payments: normalize bolt11 to lowercase so failed uppercase sends get reconciled
BOLT11 is case-insensitive, but an uppercase-pasted invoice was stored as the payment hash and the pending-set entry verbatim. check()'s startsWith("ln") test is case-sensitive, so it treated such pending entries as non-lightning and evicted them without reversing — a failed send would never be auto-refunded. - sendLightning: lowercase the invoice up front so the stored hash, the pending entry, and xpay all use the same form. - check(): lowercase before the startsWith("ln") test (defensive; also rescues any already-stored uppercase entries). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
-rw-r--r--lib/payments.ts13
1 files changed, 12 insertions, 1 deletions
diff --git a/lib/payments.ts b/lib/payments.ts
index 16eb7213..c715af47 100644
--- a/lib/payments.ts
+++ b/lib/payments.ts
@@ -697,6 +697,14 @@ export const sendLightning = async ({
}) => {
let p;
+ // BOLT11 is case-insensitive, but downstream code is not: the stored payment
+ // hash, the `pending` reconciler entry, and check()'s startsWith("ln") test
+ // all assume lowercase. An uppercase-pasted invoice gets stored uppercase,
+ // then check() treats its pending entry as non-lightning and evicts it
+ // without reversing — a failed send that never gets auto-refunded. Normalize
+ // once, up front, so every downstream use sees the same lowercase form.
+ pr = pr.replace(/\s/g, "").toLowerCase();
+
if (typeof amount !== "undefined") {
amount = Number.parseInt(amount);
if (amount < 0 || amount > SATS || Number.isNaN(amount)) {
@@ -1072,7 +1080,10 @@ export const check = async () => {
const payments = await db.sMembers("pending");
for (const pr of payments) {
- if (!pr.startsWith("ln")) {
+ // Case-insensitive: an uppercase-pasted invoice ("LNBC…") is still a
+ // lightning payment. Matching case-sensitively here used to evict such
+ // entries without reversing, stranding failed sends with no auto-refund.
+ if (!pr.toLowerCase().startsWith("ln")) {
await db.sRem("pending", pr);
continue;
}