diff options
| author | Adam Soltys <[email protected]> | 2026-05-20 22:29:35 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-05-20 22:29:35 -0700 |
| commit | fe767002dd59f000d54fd09c9d617d69b9a5c894 (patch) | |
| tree | 0eec59481f66c122a2c1ef56966d829317650af6 /lib | |
| parent | 51107ed4b4079238ed1f657ec7231943f3c08696 (diff) | |
| download | coinos-server-fe767002dd59f000d54fd09c9d617d69b9a5c894.tar.gz coinos-server-fe767002dd59f000d54fd09c9d617d69b9a5c894.zip | |
tighten LEAKED DEBIT guard: don't false-alarm on successful-via-fallback paths
The first deployment surfaced two LEAKED DEBIT lines that were actually
clean finalizes via the new failed_parts listpays-fallback path — exactly
the path that recovers payments that would have been stuck under the old
code (joho33's case). The leak guard's `outcome !== "finalized"` check
was too narrow: any path that finalizes the payment, regardless of how
it got there, is a success.
Bucket outcomes explicitly:
- finalizedOutcomes — clean successes (finalize ran, ref set)
- safeNonFinalized — reverse ran, OR debit left in pending for check()
to reconcile (these aren't leaks)
- anything else — actually stranded (finalize-threw-*, unknown) → err
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/payments.ts | 30 |
1 files changed, 23 insertions, 7 deletions
diff --git a/lib/payments.ts b/lib/payments.ts index af45ebc3..5e073715 100644 --- a/lib/payments.ts +++ b/lib/payments.ts @@ -779,14 +779,30 @@ export const sendLightning = async ({ throw e; } - // Leak guard: if we got here without a clean finalize, the payment is - // confirmed=true (from debit's default) but has no ref/preimage — the - // exact stuck state we want to avoid. Log loudly so the next occurrence - // shows up in app logs immediately. - if (outcome !== "finalized") { - err("LEAKED DEBIT in sendLightning", p.id, "outcome=", outcome, "user=", user?.username, "amount=", amount, "fee=", fee); - } else { + // Leak guard: outcomes that leave a confirmed=true debit with no ref AND + // no entry in the pending set are the joho33-style stuck state. Pending-* + // outcomes are fine — check() will reconcile. Finalize-via-* outcomes + // are clean successes via a fallback path. The truly stranded cases are + // the finalize-threw-* family. + const finalizedOutcomes = new Set([ + "finalized", + "finalized-via-listpays-after-failed_parts", + "finalized-after-xpay-throw", + ]); + const safeNonFinalized = new Set([ + "reversed-via-failed_parts", + "reversed-after-xpay-throw", + "pending-after-failed_parts", + "pending-after-xpay-throw", + "verify-failed-after-failed_parts", + "verify-failed-after-xpay-throw", + ]); + if (finalizedOutcomes.has(outcome)) { l("sendLightning outcome", p.id, "=", outcome); + } else if (safeNonFinalized.has(outcome)) { + warn("sendLightning outcome", p.id, "=", outcome); + } else { + err("LEAKED DEBIT in sendLightning", p.id, "outcome=", outcome, "user=", user?.username, "amount=", amount, "fee=", fee); } return p; |
