Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SendingParameters struct for customizable payments #336

Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ class LibraryTest {

val invoice = node2.bolt11Payment().receive(2500000u, "asdf", 9217u)

node1.bolt11Payment().send(invoice)
node1.bolt11Payment().send(invoice, null)
slanesuke marked this conversation as resolved.
Show resolved Hide resolved

val paymentSuccessfulEvent = node1.waitNextEvent()
println("Got event: $paymentSuccessfulEvent")
Expand Down
14 changes: 11 additions & 3 deletions bindings/ldk_node.udl
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, could we either split this in one commit per field, or add all fields at once? In any case this split of "initial" and "new" fields doesn't make too much sense, given that they are not new. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that during the commit history cleanup yesterday, I accidentally included some code in the wrong commits. I’m still working on my skills with interactive rebasing 😅. But, I’ve restructured the commits so that they should make more sense now. I squashed all the fields into one commit and then implemented SendingParameters in each method in the following commits.

Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dictionary Config {
u64 probing_liquidity_limit_multiplier;
LogLevel log_level;
AnchorChannelsConfig? anchor_channels_config;
SendingParameters? sending_parameters_config;
};

dictionary AnchorChannelsConfig {
Expand Down Expand Up @@ -93,9 +94,9 @@ interface Node {

interface Bolt11Payment {
[Throws=NodeError]
PaymentId send([ByRef]Bolt11Invoice invoice);
PaymentId send([ByRef]Bolt11Invoice invoice, SendingParameters? sending_parameters);
[Throws=NodeError]
PaymentId send_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat);
PaymentId send_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat, SendingParameters? sending_parameters);
[Throws=NodeError]
void send_probes([ByRef]Bolt11Invoice invoice);
[Throws=NodeError]
Expand Down Expand Up @@ -135,7 +136,7 @@ interface Bolt12Payment {

interface SpontaneousPayment {
[Throws=NodeError]
PaymentId send(u64 amount_msat, PublicKey node_id);
PaymentId send(u64 amount_msat, PublicKey node_id, SendingParameters? sending_parameters);
[Throws=NodeError]
void send_probes(u64 amount_msat, PublicKey node_id);
};
Expand Down Expand Up @@ -319,6 +320,13 @@ dictionary PaymentDetails {
u64 latest_update_timestamp;
};

dictionary SendingParameters {
u64? max_total_routing_fee_msat;
u32? max_total_cltv_expiry_delta;
u8? max_path_count;
u8? max_channel_saturation_power_of_half;
};

[NonExhaustive]
enum Network {
"Bitcoin",
Expand Down
10 changes: 10 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::time::Duration;

use crate::payment::SendingParameters;
slanesuke marked this conversation as resolved.
Show resolved Hide resolved

use lightning::ln::msgs::SocketAddress;
use lightning::util::config::UserConfig;
use lightning::util::logger::Level as LogLevel;
Expand Down Expand Up @@ -86,6 +88,7 @@ pub(crate) const WALLET_KEYS_SEED_LEN: usize = 64;
/// | `probing_liquidity_limit_multiplier` | 3 |
/// | `log_level` | Debug |
/// | `anchor_channels_config` | Some(..) |
/// | `sending_parameters_config` | None |
slanesuke marked this conversation as resolved.
Show resolved Hide resolved
///
/// See [`AnchorChannelsConfig`] for more information on its respective default values.
///
Expand Down Expand Up @@ -147,6 +150,12 @@ pub struct Config {
/// closure. We *will* however still try to get the Anchor spending transactions confirmed
/// on-chain with the funds available.
pub anchor_channels_config: Option<AnchorChannelsConfig>,

/// Configuration options for payment routing and pathfinding.
///
/// Setting the `SendingParameters` provides flexibility to customize how payments are routed,
/// including setting limits on routing fees, CLTV expiry, and channel utilization.
pub sending_parameters_config: Option<SendingParameters>,
}

impl Default for Config {
Expand All @@ -164,6 +173,7 @@ impl Default for Config {
probing_liquidity_limit_multiplier: DEFAULT_PROBING_LIQUIDITY_LIMIT_MULTIPLIER,
log_level: DEFAULT_LOG_LEVEL,
anchor_channels_config: Some(AnchorChannelsConfig::default()),
sending_parameters_config: None,
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
//! node.event_handled();
//!
//! let invoice = Bolt11Invoice::from_str("INVOICE_STR").unwrap();
//! node.bolt11_payment().send(&invoice).unwrap();
//! node.bolt11_payment().send(&invoice, None).unwrap();
//!
//! node.stop().unwrap();
//! }
Expand Down
90 changes: 86 additions & 4 deletions src/payment/bolt11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::payment::store::{
LSPFeeLimits, PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind,
PaymentStatus, PaymentStore,
};
use crate::payment::SendingParameters;
use crate::peer_store::{PeerInfo, PeerStore};
use crate::types::{ChannelManager, KeysManager};

Expand Down Expand Up @@ -69,13 +70,20 @@ impl Bolt11Payment {
}

/// Send a payment given an invoice.
pub fn send(&self, invoice: &Bolt11Invoice) -> Result<PaymentId, Error> {
///
/// If [`SendingParameters`] are provided they will override the node's default routing parameters
/// on a per-field basis. Each field in `SendingParameters` that is set replaces the corresponding
/// default value. Fields that are not set fall back to the node's configured defaults. If no
/// `SendingParameters` are provided, the method fully relies on these defaults.
pub fn send(
&self, invoice: &Bolt11Invoice, sending_parameters: Option<SendingParameters>,
) -> Result<PaymentId, Error> {
let rt_lock = self.runtime.read().unwrap();
if rt_lock.is_none() {
return Err(Error::NotRunning);
}

let (payment_hash, recipient_onion, route_params) = payment::payment_parameters_from_invoice(&invoice).map_err(|_| {
let (payment_hash, recipient_onion, mut route_params) = payment::payment_parameters_from_invoice(&invoice).map_err(|_| {
log_error!(self.logger, "Failed to send payment due to the given invoice being \"zero-amount\". Please use send_using_amount instead.");
Error::InvalidInvoice
})?;
Expand All @@ -90,6 +98,40 @@ impl Bolt11Payment {
}
}

if let Some(user_set_params) = sending_parameters {
if let Some(mut default_params) =
self.config.sending_parameters_config.as_ref().cloned()
slanesuke marked this conversation as resolved.
Show resolved Hide resolved
{
default_params.max_total_routing_fee_msat = user_set_params
.max_total_routing_fee_msat
.or(default_params.max_total_routing_fee_msat);
default_params.max_total_cltv_expiry_delta = user_set_params
.max_total_cltv_expiry_delta
.or(default_params.max_total_cltv_expiry_delta);
default_params.max_path_count =
user_set_params.max_path_count.or(default_params.max_path_count);
default_params.max_channel_saturation_power_of_half = user_set_params
.max_channel_saturation_power_of_half
.or(default_params.max_channel_saturation_power_of_half);

route_params.max_total_routing_fee_msat = default_params.max_total_routing_fee_msat;
route_params.payment_params.max_total_cltv_expiry_delta =
default_params.max_total_cltv_expiry_delta.unwrap_or_default();
route_params.payment_params.max_path_count =
default_params.max_path_count.unwrap_or_default();
route_params.payment_params.max_channel_saturation_power_of_half =
default_params.max_channel_saturation_power_of_half.unwrap_or_default();
}
} else if let Some(default_params) = &self.config.sending_parameters_config {
route_params.max_total_routing_fee_msat = default_params.max_total_routing_fee_msat;
route_params.payment_params.max_total_cltv_expiry_delta =
default_params.max_total_cltv_expiry_delta.unwrap_or_default();
route_params.payment_params.max_path_count =
default_params.max_path_count.unwrap_or_default();
route_params.payment_params.max_channel_saturation_power_of_half =
default_params.max_channel_saturation_power_of_half.unwrap_or_default();
}

let payment_secret = Some(*invoice.payment_secret());
let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT);

Expand Down Expand Up @@ -148,14 +190,20 @@ impl Bolt11Payment {
}
}

/// Send a payment given an invoice and an amount in millisatoshi.
/// Send a payment given an invoice and an amount in millisatoshis.
///
/// This will fail if the amount given is less than the value required by the given invoice.
///
/// This can be used to pay a so-called "zero-amount" invoice, i.e., an invoice that leaves the
/// amount paid to be determined by the user.
///
/// If [`SendingParameters`] are provided they will override the node's default routing parameters
/// on a per-field basis. Each field in `SendingParameters` that is set replaces the corresponding
/// default value. Fields that are not set fall back to the node's configured defaults. If no
/// `SendingParameters` are provided, the method fully relies on these defaults.
pub fn send_using_amount(
&self, invoice: &Bolt11Invoice, amount_msat: u64,
sending_parameters: Option<SendingParameters>,
) -> Result<PaymentId, Error> {
let rt_lock = self.runtime.read().unwrap();
if rt_lock.is_none() {
Expand Down Expand Up @@ -196,9 +244,43 @@ impl Bolt11Payment {
.with_bolt11_features(features.clone())
.map_err(|_| Error::InvalidInvoice)?;
}
let route_params =
let mut route_params =
RouteParameters::from_payment_params_and_value(payment_params, amount_msat);

if let Some(user_set_params) = sending_parameters {
if let Some(mut default_params) =
self.config.sending_parameters_config.as_ref().cloned()
{
default_params.max_total_routing_fee_msat = user_set_params
.max_total_routing_fee_msat
.or(default_params.max_total_routing_fee_msat);
default_params.max_total_cltv_expiry_delta = user_set_params
.max_total_cltv_expiry_delta
.or(default_params.max_total_cltv_expiry_delta);
default_params.max_path_count =
user_set_params.max_path_count.or(default_params.max_path_count);
default_params.max_channel_saturation_power_of_half = user_set_params
.max_channel_saturation_power_of_half
.or(default_params.max_channel_saturation_power_of_half);

route_params.max_total_routing_fee_msat = default_params.max_total_routing_fee_msat;
route_params.payment_params.max_total_cltv_expiry_delta =
default_params.max_total_cltv_expiry_delta.unwrap_or_default();
route_params.payment_params.max_path_count =
default_params.max_path_count.unwrap_or_default();
route_params.payment_params.max_channel_saturation_power_of_half =
default_params.max_channel_saturation_power_of_half.unwrap_or_default();
}
} else if let Some(default_params) = &self.config.sending_parameters_config {
route_params.max_total_routing_fee_msat = default_params.max_total_routing_fee_msat;
route_params.payment_params.max_total_cltv_expiry_delta =
default_params.max_total_cltv_expiry_delta.unwrap_or_default();
route_params.payment_params.max_path_count =
default_params.max_path_count.unwrap_or_default();
route_params.payment_params.max_channel_saturation_power_of_half =
default_params.max_channel_saturation_power_of_half.unwrap_or_default();
}

let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT);
let recipient_fields = RecipientOnionFields::secret_only(*payment_secret);

Expand Down
49 changes: 49 additions & 0 deletions src/payment/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,52 @@ pub use onchain::OnchainPayment;
pub use spontaneous::SpontaneousPayment;
pub use store::{LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus};
pub use unified_qr::{QrPaymentResult, UnifiedQrPayment};

/// Represents information used to route a payment.
#[derive(Clone, Debug, PartialEq)]
pub struct SendingParameters {
/// The maximum total fees, in millisatoshi, that may accrue during route finding.
///
/// This limit also applies to the total fees that may arise while retrying failed payment
/// paths.
///
/// Note that values below a few sats may result in some paths being spuriously ignored.
pub max_total_routing_fee_msat: Option<u64>,

/// The maximum total CLTV delta we accept for the route.
///
/// Defaults to [`DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA`].
///
/// [`DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA`]: lightning::routing::router::DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA
pub max_total_cltv_expiry_delta: Option<u32>,

/// The maximum number of paths that may be used by (MPP) payments.
///
/// Defaults to [`DEFAULT_MAX_PATH_COUNT`].
///
/// [`DEFAULT_MAX_PATH_COUNT`]: lightning::routing::router::DEFAULT_MAX_PATH_COUNT
pub max_path_count: Option<u8>,

/// Selects the maximum share of a channel's total capacity which will be sent over a channel,
/// as a power of 1/2.
///
/// A higher value prefers to send the payment using more MPP parts whereas
/// a lower value prefers to send larger MPP parts, potentially saturating channels and
/// increasing failure probability for those paths.
///
/// Note that this restriction will be relaxed during pathfinding after paths which meet this
/// restriction have been found. While paths which meet this criteria will be searched for, it
/// is ultimately up to the scorer to select them over other paths.
///
/// Examples:
///
/// | Value | Max Proportion of Channel Capacity Used |
/// |-------|-----------------------------------------|
/// | 0 | Up to 100% of the channel’s capacity |
/// | 1 | Up to 50% of the channel’s capacity |
/// | 2 | Up to 25% of the channel’s capacity |
/// | 3 | Up to 12.5% of the channel’s capacity |
///
/// Default value: 2
pub max_channel_saturation_power_of_half: Option<u8>,
}
49 changes: 46 additions & 3 deletions src/payment/spontaneous.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::logger::{log_error, log_info, FilesystemLogger, Logger};
use crate::payment::store::{
PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, PaymentStore,
};
use crate::payment::SendingParameters;
use crate::types::{ChannelManager, KeysManager};

use lightning::ln::channelmanager::{PaymentId, RecipientOnionFields, Retry, RetryableSendFailure};
Expand Down Expand Up @@ -41,8 +42,15 @@ impl SpontaneousPayment {
Self { runtime, channel_manager, keys_manager, payment_store, config, logger }
}

/// Send a spontaneous, aka. "keysend", payment
pub fn send(&self, amount_msat: u64, node_id: PublicKey) -> Result<PaymentId, Error> {
/// Send a spontaneous aka. "keysend", payment.
///
/// If [`SendingParameters`] are provided they will override the node's default routing parameters
/// on a per-field basis. Each field in `SendingParameters` that is set replaces the corresponding
/// default value. Fields that are not set fall back to the node's configured defaults. If no
/// `SendingParameters` are provided, the method fully relies on these defaults.
pub fn send(
&self, amount_msat: u64, node_id: PublicKey, sending_parameters: Option<SendingParameters>,
) -> Result<PaymentId, Error> {
let rt_lock = self.runtime.read().unwrap();
if rt_lock.is_none() {
return Err(Error::NotRunning);
Expand All @@ -61,10 +69,45 @@ impl SpontaneousPayment {
}
}

let route_params = RouteParameters::from_payment_params_and_value(
let mut route_params = RouteParameters::from_payment_params_and_value(
PaymentParameters::from_node_id(node_id, self.config.default_cltv_expiry_delta),
amount_msat,
);

if let Some(user_set_params) = sending_parameters {
if let Some(mut default_params) =
self.config.sending_parameters_config.as_ref().cloned()
{
default_params.max_total_routing_fee_msat = user_set_params
.max_total_routing_fee_msat
.or(default_params.max_total_routing_fee_msat);
default_params.max_total_cltv_expiry_delta = user_set_params
.max_total_cltv_expiry_delta
.or(default_params.max_total_cltv_expiry_delta);
default_params.max_path_count =
user_set_params.max_path_count.or(default_params.max_path_count);
default_params.max_channel_saturation_power_of_half = user_set_params
.max_channel_saturation_power_of_half
.or(default_params.max_channel_saturation_power_of_half);

route_params.max_total_routing_fee_msat = default_params.max_total_routing_fee_msat;
route_params.payment_params.max_total_cltv_expiry_delta =
default_params.max_total_cltv_expiry_delta.unwrap_or_default();
route_params.payment_params.max_path_count =
default_params.max_path_count.unwrap_or_default();
route_params.payment_params.max_channel_saturation_power_of_half =
default_params.max_channel_saturation_power_of_half.unwrap_or_default();
}
} else if let Some(default_params) = &self.config.sending_parameters_config {
route_params.max_total_routing_fee_msat = default_params.max_total_routing_fee_msat;
route_params.payment_params.max_total_cltv_expiry_delta =
default_params.max_total_cltv_expiry_delta.unwrap_or_default();
slanesuke marked this conversation as resolved.
Show resolved Hide resolved
route_params.payment_params.max_path_count =
default_params.max_path_count.unwrap_or_default();
route_params.payment_params.max_channel_saturation_power_of_half =
default_params.max_channel_saturation_power_of_half.unwrap_or_default();
}

let recipient_fields = RecipientOnionFields::spontaneous_empty();

match self.channel_manager.send_spontaneous_payment_with_retry(
Expand Down
2 changes: 1 addition & 1 deletion src/payment/unified_qr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ impl UnifiedQrPayment {
}

if let Some(invoice) = uri_network_checked.extras.bolt11_invoice {
match self.bolt11_invoice.send(&invoice) {
match self.bolt11_invoice.send(&invoice, None) {
Ok(payment_id) => return Ok(QrPaymentResult::Bolt11 { payment_id }),
Err(e) => log_error!(self.logger, "Failed to send BOLT11 invoice: {:?}. This is part of a unified QR code payment. Falling back to the on-chain transaction.", e),
}
Expand Down
2 changes: 1 addition & 1 deletion src/uniffi_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo};
pub use crate::payment::store::{LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus};
pub use crate::payment::QrPaymentResult;
pub use crate::payment::{QrPaymentResult, SendingParameters};

pub use lightning::events::{ClosureReason, PaymentFailureReason};
pub use lightning::ln::{ChannelId, PaymentHash, PaymentPreimage, PaymentSecret};
Expand Down
Loading
Loading