Surface failed and pending payments across backends - #108
benthecarman wants to merge 2 commits into
Conversation
…ctions ldk-node records a synchronously-failed send as a Failed outbound PaymentDetails before returning the error (bolt11 send_internal's SendingFailed arm; bolt12 and spontaneous likewise), and pay() only writes tx metadata when the send returns Ok — so a failed send lands in the no-metadata branch of list_transactions and was hidden by the status != Completed filter, leaving no trace in the transaction history. Surface outbound attempts regardless of status; non-completed inbound records (issued-but-unpaid invoices) stay hidden. The no-metadata branch's debug assertion assumed outbound records always carry metadata; that only holds for successful sends, so it fires today in any debug build that lists transactions after a synchronously-failed send. Replace it with the trusted loop's pattern (log_warn plus a _test-utils-gated assert), scoped to Succeeded — and even Succeeded can legitimately lack metadata after a crash between ldk-node's persist and the metadata write, hence warn-and-assert. Surfacing outbound records also exposed one internal leg: when try_mpp_bolt11's lightning portion fails synchronously after the trusted leg is in flight, ldk-node has recorded a failed outbound payment for it that would list as a second, standalone transaction. Nothing is in flight after a synchronous failure, so the MPP error path now removes that record; the attempt stays surfaced through the trusted leg, as that error path already intends. Trusted-backend records are unchanged: failed Spark sends either already surface through the metadata branch (keyed by the idempotency uuid, which never filtered on status) or never reach storage, and the CDK only records melts that reached Paid. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zApnExfnDXm1Kxu8pf9P9
Keep submitted trusted payments visible while pending and after a confirmed failure, even when the backend has no transaction record. Merge local records with backend history using existing payment IDs, leaving uncertain send outcomes pending until they are resolved. Resolve trusted sends whose outcome was unknown instead of leaving them pending forever. Cashu finalizes interrupted melts through the CDK saga log at startup and whenever a melt ends without a definite result, and asks the mint about submitted payments the CDK has no saga for. Melts in this process are excluded so the two paths cannot race on the same quote. Every melt outcome, including a reconciled one, reports to the rebalance watcher, so a rebalance waits for the real result instead of being released on an ambiguous error. Melt an unpaid quote again after a transient error instead of returning the old payment ID without doing anything; the CDK and the mint reject a quote that is actually in flight. Reject a duplicate MPP invoice before the trusted leg goes out, since that leg cannot be recalled, and find the lightning MPP leg among recent outbound records by hash and direction rather than deriving an LDK payment ID from the hash. Only the newest page of the payment list is read: a synchronously failed leg was inserted moments earlier, and the duplicate check is best effort. Preserve pending and completed LDK records when an MPP leg is rejected as a duplicate payment. Keep the payment store cheap on the paths that run often. History listings and rebalance checks pass the backend list through untouched when nothing is recorded locally, records carry the backend's own quote ID so reconciliation looks a quote up directly instead of scanning them all, and an outcome is reported once even if two paths observe it. Prune local records once the backend lists the payment, skip corrupt records instead of failing wallet start, and document that a backend may return an in-flight payment's ID; recording metadata for such an ID must not assert that the entry is new. Errors before submission do not create history entries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Thanks @benthecarman — agreed, this is the right shape, and closing #89 in favour of it is the right call. Surfacing failed and pending sends consistently across all three backends, reconciling uncertain melts through the saga log, and refusing the MPP trusted leg for a duplicate invoice closes the asymmetry we'd been uneasy about; carrying #89's Lightning commit forward on top is fine by us. We pulled
Two notes from the diff. The first we think is worth a small change; the second is data. 1. Failed trusted sends are never pruned from 2. Data, no ask — two things we measured rather than reasoned about:
For the record, we checked the page assumption behind We'll bump our pin when #107 lands and take this on top when it merges. Our staging capture is scripted — point us at a branch if another run is useful. |
|
@hash-money can you do a test with these changes + all the bitreq fixes? This should slightly effect start up time but make cashu payments faster |
|
Will do. Scope I'll run, so you can correct it: |
|
Done — Cashu payments (desktop, same wallet-sdk/orange-sdk code as the app, local SQLite store, live cdk-mintd on Mutinynet). Counterparty = a second wallet on our LSP with a JIT channel; receives are its LN payment into the Cashu wallet's mint quote, sends are the Cashu wallet melting to pay its LN invoice. Wall-clock from the HTTP call to the terminal event:
No measurable difference on a local store — both are dominated by the Lightning leg and the mint round-trips ( Cold start on the phone (Galaxy A12, VSS-backed store, same wallet, interleaved). First VSS request → balance ready:
Same shape as the table above from yesterday (14.5/21.0 vs 17.0/25.3/25.7): roughly +5 s on average with ±7 s run-to-run spread, all of it in the ~100 sequential VSS reads during the LDK node build (#90), which go over hyper/reqwest — so the bitreq flush fix cannot reach it. The consistent new item is the startup Cashu receive on the phone (counterparty pays the phone's mint quote; timed from the phone log): The phone has Lightning inbound, so orange-sdk routed the 1,000-sat receives to Lightning, not Cashu (Trusted stayed at 2,404; that's the tier router working as designed) — so I could not get a Cashu receive on the phone tonight; the Cashu numbers above are the desktop ones. What the phone did give is the Lightning receive latency #107 targets, HTLC
Direction is slower by ~1.5–2 s on this device with n=2, which is inside what a VSS-persist-gated receive (each commitment step is a synchronous remote persist here) can swing; I'd treat it as "no improvement seen", not a regression claim. So: startup on VSS is ~5 s slower on average in our sample (the VSS read count went 94→103, and the startup melt reconciliation is on the critical path); Cashu payment latency unchanged within noise on our setup. Happy to rerun with a different bitreq state if I've misread "all the bitreq fixes". |
Summary
Keep failed and pending sends visible in the transaction history across Lightning, Spark, and Cashu, and resolve sends whose outcome was unknown instead of leaving them pending forever.
Stacked on #107 and targets its branch until it merges. The first commit is #89's; this PR supersedes #89 and carries it forward on top of #107.
Changes
Surface failed and pending outbound Lightning payments in
list_transactions. ldk-node records a synchronously failed send as aFailedoutbound record before returning the error, and it was hidden by the completed-only filter. Outbound attempts now always surface; unpaid inbound invoices stay hidden. A failed internal MPP leg no longer lists as a separate payment.Persist submitted trusted payments. Spark and Cashu sends stay visible while pending and after a confirmed failure, even when the backend has no record. Backend terminal records take precedence and the local copy is pruned once the backend lists the payment. History listings and rebalance checks pass the backend list through untouched when nothing is recorded locally.
Reconcile uncertain Cashu melts. Interrupted melts are finalized through the CDK saga log at startup and whenever a melt ends without a definite result; submitted payments the CDK has no saga for are checked against the mint. Every outcome, including a reconciled one, reports to the rebalance watcher, so a rebalance waits for the real result. An unpaid quote is melted again after a transient error; the CDK and the mint reject a quote that is actually in flight.
Harden the MPP path. A duplicate invoice is rejected before the trusted leg goes out, since that leg cannot be recalled. The Lightning leg is found among recent outbound records by hash and direction, reading only the newest page of the payment list, rather than by deriving an LDK payment ID from the hash.
Tests
Unit tests for the payment store (restart survival, backend precedence, pruning, pass-through, corrupt records) and the error classifiers. Integration tests: a failed Lightning send lists once as failed; a failed MPP leg does not list separately; a repeated MPP invoice keeps the completed payment; a failed Cashu send is surfaced. Both full integration suites pass on the rebased branch.
🤖 Generated with Claude Code