Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions e2e-tests/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use ldk_server_client::ldk_server_grpc::api::{
};
use ldk_server_client::ldk_server_grpc::events::event_envelope::Event;
use ldk_server_client::ldk_server_grpc::events::{
ChannelClosureInitiator, ChannelState, ChannelStateChangeReasonKind,
ChannelClosureInitiator, ChannelState, ChannelStateChangeReasonKind, PaymentFailureReason,
};
use ldk_server_client::ldk_server_grpc::types::{
bolt11_invoice_description, Bolt11InvoiceDescription,
Expand Down Expand Up @@ -1515,9 +1515,17 @@ async fn test_hodl_invoice_fail() {
// Fail the payment on B using CLI
run_cli(&server_b, &["bolt11-fail-for-hash", &payment_hash_hex]);

// Verify PaymentFailed on A
// Verify PaymentFailed on A and its failure reason.
let event_a = wait_for_event(&mut events_a, |e| matches!(e, Event::PaymentFailed(_))).await;
assert!(matches!(&event_a.event, Some(Event::PaymentFailed(_))));
match &event_a.event {
Some(Event::PaymentFailed(payment_failed)) => {
assert_eq!(
payment_failed.reason,
Some(PaymentFailureReason::RecipientRejected as i32)
);
},
other => panic!("expected PaymentFailed event, got {other:?}"),
}
}

#[tokio::test]
Expand Down
88 changes: 88 additions & 0 deletions ldk-server-grpc/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,13 @@ pub struct PaymentFailed {
/// The payment details for the payment in event.
#[prost(message, optional, tag = "1")]
pub payment: ::core::option::Option<super::types::Payment>,
/// The reason the payment failed, if known.
///
/// This is only available on the emitted event; `GetPaymentDetails` cannot
/// recover it as LDK Node does not currently persist the failure reason in
/// `PaymentDetails`.
#[prost(enumeration = "PaymentFailureReason", optional, tag = "2")]
pub reason: ::core::option::Option<i32>,
}
/// PaymentClaimable indicates a payment has arrived and is waiting to be manually claimed or failed.
/// This event is only emitted for payments created via `Bolt11ReceiveForHash`.
Expand Down Expand Up @@ -275,6 +282,87 @@ impl ChannelClosureInitiator {
}
}
}
/// PaymentFailureReason mirrors LDK's `lightning::events::PaymentFailureReason`,
/// indicating why a sent payment failed.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum PaymentFailureReason {
Unspecified = 0,
/// The intended recipient rejected our payment.
RecipientRejected = 1,
/// The user chose to abandon this payment by calling `abandon_payment`.
UserAbandoned = 2,
/// We exhausted all of our retry attempts while trying to send the payment,
/// or we exhausted the configured retry timeout.
RetriesExhausted = 3,
/// Either the BOLT12 invoice was expired by the time we received it or the
/// payment expired while retrying.
PaymentExpired = 4,
/// We failed to find a route while sending or retrying the payment.
RouteNotFound = 5,
/// An unexpected error occurred, generally indicating a problem with the router.
UnexpectedError = 6,
/// An invoice was received that required unknown features.
UnknownRequiredFeatures = 7,
/// A BOLT12 invoice was not received in a reasonable amount of time.
InvoiceRequestExpired = 8,
/// An invoice request for the payment was rejected by the recipient.
InvoiceRequestRejected = 9,
/// Failed to create a blinded path back to ourselves.
BlindedPathCreationFailed = 10,
}
impl PaymentFailureReason {
/// String value of the enum field names used in the ProtoBuf definition.
///
/// The values are not transformed in any way and thus are considered stable
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
pub fn as_str_name(&self) -> &'static str {
match self {
PaymentFailureReason::Unspecified => "PAYMENT_FAILURE_REASON_UNSPECIFIED",
PaymentFailureReason::RecipientRejected => "PAYMENT_FAILURE_REASON_RECIPIENT_REJECTED",
PaymentFailureReason::UserAbandoned => "PAYMENT_FAILURE_REASON_USER_ABANDONED",
PaymentFailureReason::RetriesExhausted => "PAYMENT_FAILURE_REASON_RETRIES_EXHAUSTED",
PaymentFailureReason::PaymentExpired => "PAYMENT_FAILURE_REASON_PAYMENT_EXPIRED",
PaymentFailureReason::RouteNotFound => "PAYMENT_FAILURE_REASON_ROUTE_NOT_FOUND",
PaymentFailureReason::UnexpectedError => "PAYMENT_FAILURE_REASON_UNEXPECTED_ERROR",
PaymentFailureReason::UnknownRequiredFeatures => {
"PAYMENT_FAILURE_REASON_UNKNOWN_REQUIRED_FEATURES"
},
PaymentFailureReason::InvoiceRequestExpired => {
"PAYMENT_FAILURE_REASON_INVOICE_REQUEST_EXPIRED"
},
PaymentFailureReason::InvoiceRequestRejected => {
"PAYMENT_FAILURE_REASON_INVOICE_REQUEST_REJECTED"
},
PaymentFailureReason::BlindedPathCreationFailed => {
"PAYMENT_FAILURE_REASON_BLINDED_PATH_CREATION_FAILED"
},
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"PAYMENT_FAILURE_REASON_UNSPECIFIED" => Some(Self::Unspecified),
"PAYMENT_FAILURE_REASON_RECIPIENT_REJECTED" => Some(Self::RecipientRejected),
"PAYMENT_FAILURE_REASON_USER_ABANDONED" => Some(Self::UserAbandoned),
"PAYMENT_FAILURE_REASON_RETRIES_EXHAUSTED" => Some(Self::RetriesExhausted),
"PAYMENT_FAILURE_REASON_PAYMENT_EXPIRED" => Some(Self::PaymentExpired),
"PAYMENT_FAILURE_REASON_ROUTE_NOT_FOUND" => Some(Self::RouteNotFound),
"PAYMENT_FAILURE_REASON_UNEXPECTED_ERROR" => Some(Self::UnexpectedError),
"PAYMENT_FAILURE_REASON_UNKNOWN_REQUIRED_FEATURES" => {
Some(Self::UnknownRequiredFeatures)
},
"PAYMENT_FAILURE_REASON_INVOICE_REQUEST_EXPIRED" => Some(Self::InvoiceRequestExpired),
"PAYMENT_FAILURE_REASON_INVOICE_REQUEST_REJECTED" => Some(Self::InvoiceRequestRejected),
"PAYMENT_FAILURE_REASON_BLINDED_PATH_CREATION_FAILED" => {
Some(Self::BlindedPathCreationFailed)
},
_ => None,
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
Expand Down
34 changes: 34 additions & 0 deletions ldk-server-grpc/src/proto/events.proto
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,34 @@ enum ChannelClosureInitiator {
CHANNEL_CLOSURE_INITIATOR_UNKNOWN = 3;
}

// PaymentFailureReason mirrors LDK's `lightning::events::PaymentFailureReason`,
// indicating why a sent payment failed.
enum PaymentFailureReason {
PAYMENT_FAILURE_REASON_UNSPECIFIED = 0;
// The intended recipient rejected our payment.
PAYMENT_FAILURE_REASON_RECIPIENT_REJECTED = 1;
// The user chose to abandon this payment by calling `abandon_payment`.
PAYMENT_FAILURE_REASON_USER_ABANDONED = 2;
// We exhausted all of our retry attempts while trying to send the payment,
// or we exhausted the configured retry timeout.
PAYMENT_FAILURE_REASON_RETRIES_EXHAUSTED = 3;
// Either the BOLT12 invoice was expired by the time we received it or the
// payment expired while retrying.
PAYMENT_FAILURE_REASON_PAYMENT_EXPIRED = 4;
// We failed to find a route while sending or retrying the payment.
PAYMENT_FAILURE_REASON_ROUTE_NOT_FOUND = 5;
// An unexpected error occurred, generally indicating a problem with the router.
PAYMENT_FAILURE_REASON_UNEXPECTED_ERROR = 6;
// An invoice was received that required unknown features.
PAYMENT_FAILURE_REASON_UNKNOWN_REQUIRED_FEATURES = 7;
// A BOLT12 invoice was not received in a reasonable amount of time.
PAYMENT_FAILURE_REASON_INVOICE_REQUEST_EXPIRED = 8;
// An invoice request for the payment was rejected by the recipient.
PAYMENT_FAILURE_REASON_INVOICE_REQUEST_REJECTED = 9;
// Failed to create a blinded path back to ourselves.
PAYMENT_FAILURE_REASON_BLINDED_PATH_CREATION_FAILED = 10;
}

enum ChannelStateChangeReasonKind {
CHANNEL_STATE_CHANGE_REASON_KIND_UNSPECIFIED = 0;
CHANNEL_STATE_CHANGE_REASON_KIND_COUNTERPARTY_FORCE_CLOSED = 1;
Expand Down Expand Up @@ -110,6 +138,12 @@ message PaymentSuccessful {
message PaymentFailed {
// The payment details for the payment in event.
types.Payment payment = 1;
// The reason the payment failed, if known.
//
// This is only available on the emitted event; `GetPaymentDetails` cannot
// recover it as LDK Node does not currently persist the failure reason in
// `PaymentDetails`.
optional PaymentFailureReason reason = 2;
}

// PaymentClaimable indicates a payment has arrived and is waiting to be manually claimed or failed.
Expand Down
31 changes: 28 additions & 3 deletions ldk-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use hyper::server::conn::http2;
use hyper_util::rt::{TokioExecutor, TokioIo};
use ldk_node::bitcoin::Network;
use ldk_node::config::{Config, ElectrumSyncConfig, EsploraSyncConfig};
use ldk_node::lightning::events::ClosureReason;
use ldk_node::lightning::events::{ClosureReason, PaymentFailureReason};
use ldk_node::lightning::ln::channelmanager::PaymentId;
use ldk_node::lightning::ln::types::ChannelId;
use ldk_node::{Builder, CustomTlvRecord, Event, Node};
Expand Down Expand Up @@ -534,12 +534,14 @@ fn main() {
metrics.update_all_balances(&event_node);
}
},
Event::PaymentFailed {payment_id, ..} => {
Event::PaymentFailed {payment_id, reason, ..} => {
let payment_id = payment_id.expect("PaymentId expected for ldk-server >=0.1");
let proto_reason = reason.as_ref().map(payment_failure_reason_to_proto);

send_event_and_upsert_payment(&payment_id,
|payment_ref| event_envelope::Event::PaymentFailed(events::PaymentFailed {
move |payment_ref| event_envelope::Event::PaymentFailed(events::PaymentFailed {
payment: Some(payment_ref.clone()),
reason: proto_reason.map(|r| r as i32),
}),
&event_node,
&event_sender,
Expand Down Expand Up @@ -766,6 +768,29 @@ fn closure_initiator_from_reason(
}
}

fn payment_failure_reason_to_proto(reason: &PaymentFailureReason) -> events::PaymentFailureReason {
match reason {
PaymentFailureReason::RecipientRejected => events::PaymentFailureReason::RecipientRejected,
PaymentFailureReason::UserAbandoned => events::PaymentFailureReason::UserAbandoned,
PaymentFailureReason::RetriesExhausted => events::PaymentFailureReason::RetriesExhausted,
PaymentFailureReason::PaymentExpired => events::PaymentFailureReason::PaymentExpired,
PaymentFailureReason::RouteNotFound => events::PaymentFailureReason::RouteNotFound,
PaymentFailureReason::UnexpectedError => events::PaymentFailureReason::UnexpectedError,
PaymentFailureReason::UnknownRequiredFeatures => {
events::PaymentFailureReason::UnknownRequiredFeatures
},
PaymentFailureReason::InvoiceRequestExpired => {
events::PaymentFailureReason::InvoiceRequestExpired
},
PaymentFailureReason::InvoiceRequestRejected => {
events::PaymentFailureReason::InvoiceRequestRejected
},
PaymentFailureReason::BlindedPathCreationFailed => {
events::PaymentFailureReason::BlindedPathCreationFailed
},
}
}

fn closure_reason_to_proto(reason: &ClosureReason) -> events::ChannelStateChangeReason {
events::ChannelStateChangeReason {
kind: closure_reason_kind(reason).into(),
Expand Down