From a88420d129b4174d6d913ad2846ec2994f9bd5a9 Mon Sep 17 00:00:00 2001 From: talgya Date: Thu, 9 Jul 2026 10:22:06 -0700 Subject: [PATCH] Surface failed and pending outbound payments in list_transactions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both mapping paths hid every non-Completed payment. The comment justified hiding issued-but-unpaid inbound invoices, but the condition also swallowed failed and in-flight *outbound* payments — a failed send leaves no record in the wallet's transaction list at all, and a UI built on list_transactions cannot show the user what happened. Hide only non-completed inbound records (unpaid invoices/quotes stay noise); outbound attempts always surface. Applies to both the self-custodial path (should_surface_lightning_payment_without_metadata gains the direction) and the trusted-payment fallback branch. Observed downstream (emergent-money/graduated-wallet#328): a failed BOLT11 send was absent from history while the UI already renders a 'failed' status for records it receives. --- orange-sdk/src/lib.rs | 72 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 10 deletions(-) diff --git a/orange-sdk/src/lib.rs b/orange-sdk/src/lib.rs index 176fe9f..a38443b 100644 --- a/orange-sdk/src/lib.rs +++ b/orange-sdk/src/lib.rs @@ -525,15 +525,24 @@ impl From for WalletError { } } -fn should_surface_lightning_payment_without_metadata(status: TxStatus, kind: &PaymentKind) -> bool { - status == TxStatus::Completed || matches!(kind, PaymentKind::Onchain { .. }) +fn should_surface_lightning_payment_without_metadata( + status: TxStatus, kind: &PaymentKind, direction: PaymentDirection, +) -> bool { + // Hide only non-completed *inbound* Lightning records (issued-but-unpaid + // invoices are noise). Outbound attempts always surface: a failed or + // still-pending send the user made must appear in their history — + // otherwise a failed payment leaves no record at all. + status == TxStatus::Completed + || matches!(kind, PaymentKind::Onchain { .. }) + || direction == PaymentDirection::Outbound } fn lightning_payment_without_metadata_to_transaction( payment: &PaymentDetails, fee: Option, ) -> Option { let status = payment.status.into(); - if !should_surface_lightning_payment_without_metadata(status, &payment.kind) { + if !should_surface_lightning_payment_without_metadata(status, &payment.kind, payment.direction) + { return None; } @@ -919,9 +928,10 @@ impl Wallet { ); } - if payment.status != TxStatus::Completed { - // We don't bother to surface pending inbound transactions (i.e. issued but - // unpaid invoices) in our transaction list. + if !payment.outbound && payment.status != TxStatus::Completed { + // We don't bother to surface pending/expired inbound records + // (i.e. issued but unpaid quotes) in our transaction list. + // Outbound attempts always surface, including failures. continue; } @@ -1775,7 +1785,11 @@ mod tests { status: ConfirmationStatus::Unconfirmed, }; - assert!(should_surface_lightning_payment_without_metadata(TxStatus::Pending, &kind)); + assert!(should_surface_lightning_payment_without_metadata( + TxStatus::Pending, + &kind, + PaymentDirection::Inbound + )); } #[test] @@ -1805,16 +1819,54 @@ mod tests { } #[test] - fn pending_non_onchain_lightning_payments_without_metadata_are_hidden() { + fn pending_inbound_non_onchain_lightning_payments_without_metadata_are_hidden() { let kind = PaymentKind::Spontaneous { hash: PaymentHash([42; 32]), preimage: None }; - assert!(!should_surface_lightning_payment_without_metadata(TxStatus::Pending, &kind)); + assert!(!should_surface_lightning_payment_without_metadata( + TxStatus::Pending, + &kind, + PaymentDirection::Inbound + )); } #[test] fn completed_lightning_payments_without_metadata_are_listed() { let kind = PaymentKind::Spontaneous { hash: PaymentHash([42; 32]), preimage: None }; - assert!(should_surface_lightning_payment_without_metadata(TxStatus::Completed, &kind)); + assert!(should_surface_lightning_payment_without_metadata( + TxStatus::Completed, + &kind, + PaymentDirection::Inbound + )); + } + + #[test] + fn failed_and_pending_outbound_payments_are_listed() { + // A failed or in-flight send the user made must appear in their + // history — a failed payment that leaves no record erodes trust + // in the send flow (the wallet UI can't show what it never sees). + let kind = PaymentKind::Bolt11 { + hash: PaymentHash([42; 32]), + preimage: None, + secret: None, + counterparty_skimmed_fee_msat: None, + }; + + assert!(should_surface_lightning_payment_without_metadata( + TxStatus::Failed, + &kind, + PaymentDirection::Outbound + )); + assert!(should_surface_lightning_payment_without_metadata( + TxStatus::Pending, + &kind, + PaymentDirection::Outbound + )); + // Inbound failures (expired unpaid invoices) stay hidden. + assert!(!should_surface_lightning_payment_without_metadata( + TxStatus::Failed, + &kind, + PaymentDirection::Inbound + )); } }