fix: surface LDK Node payment failure reasons to user

This commit is contained in:
Evan Kaloudis
2026-04-14 17:10:48 -04:00
parent b4b3aa4c45
commit 87f0ff4716
3 changed files with 52 additions and 17 deletions
+34 -17
View File
@@ -20,6 +20,7 @@ import type {
ClosedChannelDetails,
PaymentDetails,
LdkNodeEvent,
PaymentFailureReason,
ClosureReason,
Lsps1OrderResponse,
Lsps1OrderStatus
@@ -1783,6 +1784,7 @@ export default class LdkNode {
/**
* Poll listPayments until the given payment succeeds or fails.
* Captures failure reason from events for better error messages.
* Returns the completed payment or throws on failure/timeout.
*/
private awaitPaymentCompletion = async (
@@ -1791,29 +1793,44 @@ export default class LdkNode {
const maxAttempts = 60;
const delayMs = 1000;
let payment = null;
let failureReason: PaymentFailureReason | undefined;
for (let i = 0; i < maxAttempts; i++) {
const payments = await LdkNodeInjection.payments.listPayments();
payment = payments.find((p) => p.id === paymentId);
if (payment?.status === 'succeeded') {
break;
// Subscribe to events to capture the failure reason
const unsubscribe = this.subscribeToEvents((event: LdkNodeEvent) => {
if (
event.type === 'paymentFailed' &&
event.paymentId === paymentId
) {
failureReason = event.reason;
}
if (payment?.status === 'failed') {
throw new Error(localeString('error.paymentFailed'));
});
try {
for (let i = 0; i < maxAttempts; i++) {
const payments = await LdkNodeInjection.payments.listPayments();
payment = payments.find((p) => p.id === paymentId);
if (payment?.status === 'succeeded') {
break;
}
if (payment?.status === 'failed') {
throw new Error(failureReason || 'PAYMENT_FAILED_UNKNOWN');
}
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
if (payment?.status !== 'succeeded') {
throw new Error(localeString('error.paymentTimedOut'));
}
if (payment?.status !== 'succeeded') {
throw new Error(localeString('error.paymentTimedOut'));
return {
hash: payment?.kind.hash || paymentId,
preimage: payment?.kind.preimage || ''
};
} finally {
unsubscribe();
}
return {
hash: payment?.kind.hash || paymentId,
preimage: payment?.kind.preimage || ''
};
};
/**
+8
View File
@@ -1753,6 +1753,14 @@
"error.failureReasonIncorrectPaymentDetails": "Payment failed: Payment details incorrect (unknown payment hash, invalid amount or invalid final CLTV delta).",
"error.failureReasonIncorrectPaymentDetailsKeysend": "The receiving node might not accept keysend payments.",
"error.failureReasonInsufficientBalance": "Insufficient local balance",
"error.ldk.recipientRejected": "Payment rejected by recipient",
"error.ldk.retriesExhausted": "All routing attempts exhausted",
"error.ldk.routeNotFound": "No route found to destination",
"error.ldk.paymentExpired": "Payment expired before completion",
"error.ldk.unknownRequiredFeatures": "Invoice requires unsupported features",
"error.ldk.invoiceRequestExpired": "Invoice request expired",
"error.ldk.invoiceRequestRejected": "Invoice request was rejected",
"error.ldk.blindedPathCreationFailed": "Failed to create blinded payment path",
"error.invalidMacaroon": "Invalid macaroon. Please check that you've entered the correct macaroon for this node.",
"error.invalidResponse": "Received invalid response data from the server",
"error.paymentFailed": "Payment failed",
+10
View File
@@ -18,6 +18,16 @@ const userFriendlyErrors: any = {
'error.failureReasonIncorrectPaymentDetails',
FAILURE_REASON_INSUFFICIENT_BALANCE:
'error.failureReasonInsufficientBalance',
// LDK Node payment failure reasons
recipientRejected: 'error.ldk.recipientRejected',
retriesExhausted: 'error.ldk.retriesExhausted',
routeNotFound: 'error.ldk.routeNotFound',
paymentExpired: 'error.ldk.paymentExpired',
unknownRequiredFeatures: 'error.ldk.unknownRequiredFeatures',
invoiceRequestExpired: 'error.ldk.invoiceRequestExpired',
invoiceRequestRejected: 'error.ldk.invoiceRequestRejected',
blindedPathCreationFailed: 'error.ldk.blindedPathCreationFailed',
PAYMENT_FAILED_UNKNOWN: 'error.paymentFailed',
Error: 'general.error'
};