fix(lightning): never report a slow in-flight payment as failed

Slow multi-hop payments (>15s routing) surfaced as "Payment failed"
while LND settled them in the background: the shared LND REST client's
15s total timeout aborted the synchronous /v1/channels/transactions
wait, and every UI path treated that abort as a definitive failure. The
payment then succeeded anyway and only appeared in history on the next
background poll.

Backend: lnd.payinvoice now decodes the invoice up front for its payment
hash, pays on a dedicated 120s client, and answers status:"pending" with
the hash (never an error) when the wait elapses after the payment was
handed to LND — only a pre-connect failure is still a hard error. New
lnd.paymentstatus RPC reports succeeded/failed/in_flight (with humanized
failure reasons) from /v1/payments.

Frontend: new rpcClient.payLightningInvoice() pays then polls
lnd.paymentstatus to a real terminal state (3s interval, up to 2 min);
all five call sites (send modal, scan modal, web5 unified send, peer-file
purchase, app-launcher payments) migrated. Failure is only shown when LND
itself declares FAILED; a still-settling payment shows an in-flight state
and success fires the transaction refresh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-07-27 05:33:02 -04:00
co-authored by Claude Fable 5
parent aa272bfcf4
commit 9cd507269c
8 changed files with 264 additions and 36 deletions
+15 -5
View File
@@ -620,11 +620,21 @@ async function send() {
ecashToken.value = res.token
} else if (method === 'lightning') {
if (!dest.value.trim()) { error.value = t('web5.pasteInvoice'); return }
const res = await rpcClient.call<{ payment_hash: string }>({
method: 'lnd.payinvoice',
params: { payment_request: dest.value.trim() },
})
successInfo.value = { amount: paidAmount, methodLabel: 'Paid over Lightning', hash: res.payment_hash }
// Waits out slow multi-hop routing and only reports failure when LND
// itself declares the payment failed — never on a timeout.
const res = await rpcClient.payLightningInvoice({ payment_request: dest.value.trim() })
if (res.status === 'failed') {
error.value = res.failure_reason || t('web5.sendFailed')
return
}
successInfo.value = {
amount: paidAmount,
methodLabel: res.status === 'pending' ? 'Payment in flight' : 'Paid over Lightning',
hash: res.payment_hash || undefined,
...(res.status === 'pending'
? { note: 'This payment is taking longer than usual to settle. It will appear in your transactions once it completes.' }
: {}),
}
} else {
if (!dest.value.trim()) { error.value = t('web5.enterBitcoinAddress'); return }
const res = await rpcClient.call<{ txid: string }>({