From a88420d129b4174d6d913ad2846ec2994f9bd5a9 Mon Sep 17 00:00:00 2001 From: talgya Date: Thu, 9 Jul 2026 10:22:06 -0700 Subject: [PATCH 1/2] 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 + )); } } From 356ce1684096c7641bf2c32277c2bd5a5cb98556 Mon Sep 17 00:00:00 2001 From: talgya Date: Sat, 11 Jul 2026 11:15:48 -0700 Subject: [PATCH 2/2] Don't panic when a same-mint melt settles without a preimage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a melt's bolt11 destination is a mint quote on the same mint, cdk-mintd settles it internally — no Lightning payment occurs and the melt result legitimately carries no payment_proof. The success path already tolerates a missing preimage (the event hash falls back to the invoice payment_hash), but the None arm carried a debug_assert!(false) that panics any debug build the moment two wallets sharing a mint pay each other. Downgrade to an explanatory log_info; fixes #91. --- orange-sdk/src/trusted_wallet/cashu/mod.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/orange-sdk/src/trusted_wallet/cashu/mod.rs b/orange-sdk/src/trusted_wallet/cashu/mod.rs index 9b11857..db1dd13 100644 --- a/orange-sdk/src/trusted_wallet/cashu/mod.rs +++ b/orange-sdk/src/trusted_wallet/cashu/mod.rs @@ -730,15 +730,17 @@ impl Cashu { }, }, None => { - debug_assert!( - false, - "Melt succeeded but no preimage for quote: {quote_id}" - ); - log_error!( + // Expected for same-mint payments: when the melt's + // bolt11 destination is a mint quote on this same + // mint, cdk-mintd settles internally — no Lightning + // payment occurs, so there is no preimage to return. + // The success path below already tolerates None + // (hash falls back to the invoice payment_hash). + log_info!( logger, - "Melt succeeded but no preimage for quote: {quote_id}" + "Melt for quote {quote_id} settled without a preimage (internal/same-mint settlement)" ); - None // Placeholder, should not happen + None }, };