diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 954a6403c..2c17ca76e 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -152,6 +152,8 @@ interface SpontaneousPayment { [Throws=NodeError] PaymentId send(u64 amount_msat, PublicKey node_id, SendingParameters? sending_parameters); [Throws=NodeError] + PaymentId send_with_custom_tlvs(u64 amount_msat, PublicKey node_id, SendingParameters? sending_parameters, sequence custom_tlvs); + [Throws=NodeError] void send_probes(u64 amount_msat, PublicKey node_id); }; @@ -182,6 +184,7 @@ enum NodeError { "OfferCreationFailed", "RefundCreationFailed", "PaymentSendingFailed", + "InvalidCustomTlvs", "ProbeSendingFailed", "ChannelCreationFailed", "ChannelClosingFailed", @@ -275,8 +278,8 @@ enum VssHeaderProviderError { interface Event { PaymentSuccessful(PaymentId? payment_id, PaymentHash payment_hash, PaymentPreimage? payment_preimage, u64? fee_paid_msat); PaymentFailed(PaymentId? payment_id, PaymentHash? payment_hash, PaymentFailureReason? reason); - PaymentReceived(PaymentId? payment_id, PaymentHash payment_hash, u64 amount_msat); - PaymentClaimable(PaymentId payment_id, PaymentHash payment_hash, u64 claimable_amount_msat, u32? claim_deadline); + PaymentReceived(PaymentId? payment_id, PaymentHash payment_hash, u64 amount_msat, sequence custom_records); + PaymentClaimable(PaymentId payment_id, PaymentHash payment_hash, u64 claimable_amount_msat, u32? claim_deadline, sequence custom_records); PaymentForwarded(ChannelId prev_channel_id, ChannelId next_channel_id, UserChannelId? prev_user_channel_id, UserChannelId? next_user_channel_id, u64? total_fee_earned_msat, u64? skimmed_fee_msat, boolean claim_from_onchain_tx, u64? outbound_amount_forwarded_msat); ChannelPending(ChannelId channel_id, UserChannelId user_channel_id, ChannelId former_temporary_channel_id, PublicKey counterparty_node_id, OutPoint funding_txo); ChannelReady(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id); @@ -362,6 +365,11 @@ dictionary SendingParameters { u8? max_channel_saturation_power_of_half; }; +dictionary CustomTlvRecord { + u64 kind; + sequence value; +}; + [Enum] interface MaxTotalRoutingFeeLimit { None (); diff --git a/src/error.rs b/src/error.rs index d1fd848b1..ec1182c87 100644 --- a/src/error.rs +++ b/src/error.rs @@ -34,6 +34,8 @@ pub enum Error { RefundCreationFailed, /// Sending a payment has failed. PaymentSendingFailed, + /// Sending of spontaneous payment with custom TLVs failed. + InvalidCustomTlvs, /// Sending a payment probe has failed. ProbeSendingFailed, /// A channel could not be opened. @@ -130,6 +132,7 @@ impl fmt::Display for Error { Self::OfferCreationFailed => write!(f, "Failed to create offer."), Self::RefundCreationFailed => write!(f, "Failed to create refund."), Self::PaymentSendingFailed => write!(f, "Failed to send the given payment."), + Self::InvalidCustomTlvs => write!(f, "Failed to construct payment with custom TLVs."), Self::ProbeSendingFailed => write!(f, "Failed to send the given payment probe."), Self::ChannelCreationFailed => write!(f, "Failed to create channel."), Self::ChannelClosingFailed => write!(f, "Failed to close channel."), diff --git a/src/event.rs b/src/event.rs index d760d3b58..179d9e7ee 100644 --- a/src/event.rs +++ b/src/event.rs @@ -5,7 +5,7 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::types::{DynStore, Sweeper, Wallet}; +use crate::types::{CustomTlvRecord, DynStore, Sweeper, Wallet}; use crate::{ hex_utils, BumpTransactionEventHandler, ChannelManager, Config, Error, Graph, PeerInfo, @@ -102,6 +102,8 @@ pub enum Event { payment_hash: PaymentHash, /// The value, in thousandths of a satoshi, that has been received. amount_msat: u64, + /// Custom TLV records received on the payment + custom_records: Vec, }, /// A payment has been forwarded. PaymentForwarded { @@ -168,6 +170,8 @@ pub enum Event { /// The block height at which this payment will be failed back and will no longer be /// eligible for claiming. claim_deadline: Option, + /// Custom TLV records attached to the payment + custom_records: Vec, }, /// A channel has been created and is pending confirmation on-chain. ChannelPending { @@ -224,6 +228,7 @@ impl_writeable_tlv_based_enum!(Event, (0, payment_hash, required), (1, payment_id, option), (2, amount_msat, required), + (3, custom_records, optional_vec), }, (3, ChannelReady) => { (0, channel_id, required), @@ -248,6 +253,7 @@ impl_writeable_tlv_based_enum!(Event, (2, payment_id, required), (4, claimable_amount_msat, required), (6, claim_deadline, option), + (8, custom_records, optional_vec), }, (7, PaymentForwarded) => { (0, prev_channel_id, required), @@ -542,7 +548,7 @@ where via_channel_id: _, via_user_channel_id: _, claim_deadline, - onion_fields: _, + onion_fields, counterparty_skimmed_fee_msat, } => { let payment_id = PaymentId(payment_hash.0); @@ -644,11 +650,17 @@ where "We would have registered the preimage if we knew" ); + let custom_records = onion_fields + .map(|cf| { + cf.custom_tlvs().into_iter().map(|tlv| tlv.into()).collect() + }) + .unwrap_or_default(); let event = Event::PaymentClaimable { payment_id, payment_hash, claimable_amount_msat: amount_msat, claim_deadline, + custom_records, }; match self.event_queue.add_event(event) { Ok(_) => return Ok(()), @@ -799,7 +811,7 @@ where receiver_node_id: _, htlcs: _, sender_intended_total_msat: _, - onion_fields: _, + onion_fields, } => { let payment_id = PaymentId(payment_hash.0); log_info!( @@ -875,6 +887,9 @@ where payment_id: Some(payment_id), payment_hash, amount_msat, + custom_records: onion_fields + .map(|cf| cf.custom_tlvs().into_iter().map(|tlv| tlv.into()).collect()) + .unwrap_or_default(), }; match self.event_queue.add_event(event) { Ok(_) => return Ok(()), diff --git a/src/lib.rs b/src/lib.rs index 1e30c61c0..363812292 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -140,7 +140,7 @@ use types::{ Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, Graph, KeysManager, OnionMessenger, PeerManager, Router, Scorer, Sweeper, Wallet, }; -pub use types::{ChannelDetails, PeerDetails, UserChannelId}; +pub use types::{ChannelDetails, CustomTlvRecord, PeerDetails, UserChannelId}; use logger::{log_error, log_info, log_trace, FilesystemLogger, Logger}; diff --git a/src/payment/spontaneous.rs b/src/payment/spontaneous.rs index 3be244bb5..8c03dbf0c 100644 --- a/src/payment/spontaneous.rs +++ b/src/payment/spontaneous.rs @@ -14,7 +14,7 @@ use crate::payment::store::{ PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, PaymentStore, }; use crate::payment::SendingParameters; -use crate::types::{ChannelManager, KeysManager}; +use crate::types::{ChannelManager, CustomTlvRecord, KeysManager}; use lightning::ln::channelmanager::{PaymentId, RecipientOnionFields, Retry, RetryableSendFailure}; use lightning::ln::{PaymentHash, PaymentPreimage}; @@ -58,6 +58,21 @@ impl SpontaneousPayment { /// node-wide parameters configured via [`Config::sending_parameters`] on a per-field basis. pub fn send( &self, amount_msat: u64, node_id: PublicKey, sending_parameters: Option, + ) -> Result { + self.send_inner(amount_msat, node_id, sending_parameters, None) + } + + /// Send a spontaneous payment including a list of custom TLVs. + pub fn send_with_custom_tlvs( + &self, amount_msat: u64, node_id: PublicKey, sending_parameters: Option, + custom_tlvs: Vec, + ) -> Result { + self.send_inner(amount_msat, node_id, sending_parameters, Some(custom_tlvs)) + } + + fn send_inner( + &self, amount_msat: u64, node_id: PublicKey, sending_parameters: Option, + custom_tlvs: Option>, ) -> Result { let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { @@ -97,7 +112,15 @@ impl SpontaneousPayment { .map(|s| route_params.payment_params.max_channel_saturation_power_of_half = s); }; - let recipient_fields = RecipientOnionFields::spontaneous_empty(); + let recipient_fields = match custom_tlvs { + Some(tlvs) => RecipientOnionFields::spontaneous_empty() + .with_custom_tlvs(tlvs.into_iter().map(|tlv| (tlv.kind, tlv.value)).collect()) + .map_err(|e| { + log_error!(self.logger, "Failed to send payment with custom TLVs: {:?}", e); + Error::InvalidCustomTlvs + })?, + None => RecipientOnionFields::spontaneous_empty(), + }; match self.channel_manager.send_spontaneous_payment_with_retry( Some(payment_preimage), diff --git a/src/types.rs b/src/types.rs index 9fae37e18..0e6d7d752 100644 --- a/src/types.rs +++ b/src/types.rs @@ -12,6 +12,7 @@ use crate::logger::FilesystemLogger; use crate::message_handler::NodeCustomMessageHandler; use lightning::chain::chainmonitor; +use lightning::impl_writeable_tlv_based; use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; use lightning::ln::msgs::RoutingMessageHandler; use lightning::ln::msgs::SocketAddress; @@ -348,3 +349,23 @@ pub struct PeerDetails { /// Indicates whether we currently have an active connection with the peer. pub is_connected: bool, } + +/// Custom TLV entry. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CustomTlvRecord { + /// Type number. + pub kind: u64, + /// Serialized value. + pub value: Vec, +} + +impl_writeable_tlv_based!(CustomTlvRecord, { + (0, kind, required), + (1, value, required), +}); + +impl From<&(u64, Vec)> for CustomTlvRecord { + fn from(tlv: &(u64, Vec)) -> Self { + CustomTlvRecord { kind: tlv.0, value: tlv.1.clone() } + } +} diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index c8c84dbed..9cb88597d 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -16,6 +16,7 @@ pub use crate::config::{ pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; pub use crate::payment::store::{LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus}; pub use crate::payment::{MaxTotalRoutingFeeLimit, QrPaymentResult, SendingParameters}; +pub use crate::types::CustomTlvRecord; pub use lightning::chain::channelmonitor::BalanceSource; pub use lightning::events::{ClosureReason, PaymentFailureReason}; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 26aff3d11..8a5a28f14 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -11,7 +11,10 @@ use ldk_node::config::{Config, EsploraSyncConfig}; use ldk_node::io::sqlite_store::SqliteStore; use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus}; -use ldk_node::{Builder, Event, LightningBalance, LogLevel, Node, NodeError, PendingSweepBalance}; +use ldk_node::{ + Builder, CustomTlvRecord, Event, LightningBalance, LogLevel, Node, NodeError, + PendingSweepBalance, +}; use lightning::ln::msgs::SocketAddress; use lightning::ln::{PaymentHash, PaymentPreimage}; @@ -751,14 +754,18 @@ pub(crate) fn do_channel_full_cycle( // Test spontaneous/keysend payments println!("\nA send_spontaneous_payment"); let keysend_amount_msat = 2500_000; - let keysend_payment_id = - node_a.spontaneous_payment().send(keysend_amount_msat, node_b.node_id(), None).unwrap(); + let custom_tlvs = vec![CustomTlvRecord { kind: 13377331, value: vec![1, 2, 3] }]; + let keysend_payment_id = node_a + .spontaneous_payment() + .send_with_custom_tlvs(keysend_amount_msat, node_b.node_id(), None, custom_tlvs.clone()) + .unwrap(); expect_event!(node_a, PaymentSuccessful); - let received_keysend_amount = match node_b.wait_next_event() { - ref e @ Event::PaymentReceived { amount_msat, .. } => { + let next_event = node_b.wait_next_event(); + let (received_keysend_amount, received_custom_records) = match next_event { + ref e @ Event::PaymentReceived { amount_msat, ref custom_records, .. } => { println!("{} got event {:?}", std::stringify!(node_b), e); node_b.event_handled(); - amount_msat + (amount_msat, custom_records) }, ref e => { panic!("{} got unexpected event!: {:?}", std::stringify!(node_b), e); @@ -772,6 +779,7 @@ pub(crate) fn do_channel_full_cycle( node_a.payment(&keysend_payment_id).unwrap().kind, PaymentKind::Spontaneous { .. } )); + assert_eq!(received_custom_records, &custom_tlvs); assert_eq!(node_b.payment(&keysend_payment_id).unwrap().status, PaymentStatus::Succeeded); assert_eq!(node_b.payment(&keysend_payment_id).unwrap().direction, PaymentDirection::Inbound); assert_eq!(node_b.payment(&keysend_payment_id).unwrap().amount_msat, Some(keysend_amount_msat));