diff --git a/core/archipelago/src/api/rpc/lnd/payments.rs b/core/archipelago/src/api/rpc/lnd/payments.rs index 276281bc..eba3f182 100644 --- a/core/archipelago/src/api/rpc/lnd/payments.rs +++ b/core/archipelago/src/api/rpc/lnd/payments.rs @@ -62,6 +62,14 @@ impl RpcHandler { .get("message") .and_then(|v| v.as_str()) .unwrap_or("Unknown error"); + // Invoices are short-lived; retrying the same one can never + // succeed, so tell the user the way out instead of just the fact. + if msg.contains("invoice expired") { + return Err(anyhow::anyhow!( + "Payment failed: this invoice has expired ({}). Ask the recipient for a fresh invoice and try again.", + msg.trim_start_matches("invoice expired. ") + )); + } return Err(anyhow::anyhow!("Payment failed: {}", msg)); } diff --git a/core/archipelago/src/api/rpc/middleware.rs b/core/archipelago/src/api/rpc/middleware.rs index 72b9e68d..a20a184f 100644 --- a/core/archipelago/src/api/rpc/middleware.rs +++ b/core/archipelago/src/api/rpc/middleware.rs @@ -79,6 +79,13 @@ pub(super) fn sanitize_error_message(msg: &str) -> String { // them in the first place (ecash send, 2026-07-22). "Insufficient balance", "Insufficient funds", + // Lightning payment failures carry LND's reason ("invoice expired. + // Valid until …", "no route", …) — the user can act on every one of + // them, and masking sent the operator to journalctl (invoice-expired + // send, 2026-07-23). + "Payment failed", + "Invalid payment request", + "Missing 'payment_request'", "Your Lightning node is still finishing", "Bitcoin address", "No router", @@ -158,6 +165,25 @@ mod sanitize_tests { } } + #[test] + fn lightning_payment_errors_pass_through() { + // LND's payment-failure reasons are written for the payer — masking + // "invoice expired" as "Check server logs" left a user retrying a + // dead invoice (framework-pt, 2026-07-23). + for msg in [ + "Payment failed: this invoice has expired (Valid until 2026-07-23 07:41:42 +0000 UTC). Ask the recipient for a fresh invoice and try again.", + "Payment failed: unable to find a path to destination", + "Invalid payment request: must be a Lightning invoice (lnbc...)", + "Missing 'payment_request' parameter", + ] { + assert_ne!( + sanitize_error_message(msg), + "Operation failed. Check server logs for details.", + "masked: {msg}" + ); + } + } + #[test] fn internal_errors_stay_generic() { assert_eq!(