From 6992af9bc03822dea71b59604f03d34a91511264 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 17 May 2026 23:38:53 -0700 Subject: sendLightning / sendKeysend: only reverse when CLN reports the payment failed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The catch blocks for ln.xpay and ln.keysend reversed the user's debit whenever the RPC call threw, regardless of the actual payment state in CLN. If the RPC errored transiently while the payment was still pending, the user's balance got refunded — and then the lightning payment could still complete, leaving the user with both the refund and the outgoing sats. Both call sites now mirror the safe check() logic: const failed = !pays.length || pays.every((p) => p.status === "failed"); if (completed) finalize ... else if (failed) reverse ... // else (pending) leave the debit; check() reconciles when CLN settles. If listpays itself errors, log via warn() and skip the reverse — the debit stays in the pending set and check() will retry. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/payments.ts | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/payments.ts b/lib/payments.ts index 156fd9f9..8b1d6366 100644 --- a/lib/payments.ts +++ b/lib/payments.ts @@ -972,7 +972,16 @@ export const sendKeysend = async ({ extratlvs, }); } catch (e) { - reverse(p); + try { + const { pays } = await ln.listpays({ payment_hash: hash }); + const failed = !pays.length || pays.every((p) => p.status === "failed"); + if (failed) await reverse(p); + // else: pending or completed — leave the debit; check() loop will + // reconcile. Reversing on pending would refund while CLN may still + // settle the keysend. + } catch (verifyErr: any) { + warn("listpays verification failed (keysend)", verifyErr?.message ?? String(verifyErr)); + } throw e; } }; @@ -1035,7 +1044,22 @@ export const sendLightning = async ({ user, pr, amount, fee = undefined, memo = } } catch (e) { err("failed to pay", pr.substr(-8)); - reverse(p); + try { + const { pays } = await ln.listpays(pr); + const completed = pays.find((p) => p.status === "complete"); + const failed = !pays.length || pays.every((p) => p.status === "failed"); + if (completed) { + warn("payment completed despite error, finalizing", p.id); + try { await finalize(completed, p); } catch (_) {} + } else if (failed) { + await reverse(p); + } + // else: pending — leave the debit in place; check() loop will reconcile + // once CLN confirms outcome. Reversing while still in flight would + // refund the user even though the payment may yet complete. + } catch (verifyErr: any) { + warn("listpays verification failed", verifyErr?.message ?? String(verifyErr)); + } throw e; } -- cgit v1.2.3