fix(lightning): actionable expired-invoice error + payment failures reach the user

- payments.rs: an expired invoice can never succeed on retry, so the
  error now says so and tells the payer the way out (ask the recipient
  for a fresh invoice) instead of echoing LND's raw reason.
- middleware.rs: the error sanitizer masked every Lightning payment
  failure as 'Operation failed. Check server logs' — but LND's reasons
  ('invoice expired', 'unable to find a path') are written for the
  payer and are all actionable. 'Payment failed', 'Invalid payment
  request' and 'Missing payment_request' now pass through, with a
  regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-07-23 04:51:57 -04:00
parent b991c18e73
commit db6003aef6
2 changed files with 34 additions and 0 deletions

View File

@ -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));
}

View File

@ -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!(