Skip to content

Commit

Permalink
Allow for variable amount payments
Browse files Browse the repository at this point in the history
  • Loading branch information
tnull committed Feb 2, 2024
1 parent 211c454 commit 5eec4d1
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 50 deletions.
3 changes: 3 additions & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ interface LDKNode {
Bolt11Invoice receive_variable_amount_payment([ByRef]string description, u32 expiry_secs);
[Throws=NodeError]
Bolt11Invoice receive_payment_via_jit_channel(u64 amount_msat, [ByRef]string description, u32 expiry_secs, u64? max_lsp_fee_limit_msat);
[Throws=NodeError]
Bolt11Invoice receive_variable_amount_payment_via_jit_channel([ByRef]string description, u32 expiry_secs, u64? max_proportional_lsp_fee_limit_ppm_msat);
PaymentDetails? payment([ByRef]PaymentHash payment_hash);
[Throws=NodeError]
void remove_payment([ByRef]PaymentHash payment_hash);
Expand Down Expand Up @@ -173,6 +175,7 @@ enum PaymentStatus {

dictionary LSPFeeLimits {
u64? max_total_opening_fee_msat;
u64? max_proportional_opening_fee_ppm_msat;
};

dictionary PaymentDetails {
Expand Down
17 changes: 15 additions & 2 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer};
use bitcoin::blockdata::locktime::absolute::LockTime;
use bitcoin::secp256k1::PublicKey;
use bitcoin::OutPoint;
use lightning_liquidity::lsps2::utils::compute_opening_fee;
use rand::{thread_rng, Rng};
use std::collections::VecDeque;
use std::ops::Deref;
Expand Down Expand Up @@ -381,8 +382,20 @@ where
return;
}

let max_total_opening_fee_msat =
info.lsp_fee_limits.and_then(|l| l.max_total_opening_fee_msat).unwrap_or(0);
let max_total_opening_fee_msat = if let Some(max_total_opening_fee_msat) =
info.lsp_fee_limits.and_then(|l| l.max_total_opening_fee_msat)
{
max_total_opening_fee_msat
} else if let Some(max_proportional_opening_fee_ppm_msat) =
info.lsp_fee_limits.and_then(|l| l.max_proportional_opening_fee_ppm_msat)
{
// If it's a variable amount payment, compute the actual total opening fee.
compute_opening_fee(amount_msat, 0, max_proportional_opening_fee_ppm_msat)
.unwrap_or(0)
} else {
0
};

if counterparty_skimmed_fee_msat > max_total_opening_fee_msat {
log_info!(
self.logger,
Expand Down
58 changes: 44 additions & 14 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1604,12 +1604,38 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
description,
expiry_secs,
max_total_lsp_fee_limit_msat,
None,
)
}

/// Returns a payable invoice that can be used to request a variable amount payment (also known
/// as "zero-amount" invoice) and receive it via a newly created just-in-time (JIT) channel.
///
/// When the returned invoice is paid, the configured [LSPS2]-compliant LSP will open a channel
/// to us, supplying just-in-time inbound liquidity.
///
/// If set, `max_proportional_lsp_fee_limit_ppm_msat` will limit how much proportional fee, in
/// parts-per-million millisatoshis, we allow the LSP to take for opening the channel to us.
/// We'll use its cheapest offer otherwise.
///
/// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md
pub fn receive_variable_amount_payment_via_jit_channel(
&self, description: &str, expiry_secs: u32,
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
) -> Result<Bolt11Invoice, Error> {
self.receive_payment_via_jit_channel_inner(
None,
description,
expiry_secs,
None,
max_proportional_lsp_fee_limit_ppm_msat,
)
}

fn receive_payment_via_jit_channel_inner(
&self, amount_msat: Option<u64>, description: &str, expiry_secs: u32,
max_total_lsp_fee_limit_msat: Option<u64>,
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
) -> Result<Bolt11Invoice, Error> {
let liquidity_source =
self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;
Expand Down Expand Up @@ -1639,23 +1665,27 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
log_info!(self.logger, "Connected to LSP {}@{}. ", peer_info.node_id, peer_info.address);

let liquidity_source = Arc::clone(&liquidity_source);
let (invoice, lsp_opening_fee) = tokio::task::block_in_place(move || {
runtime.block_on(async move {
liquidity_source
.lsps2_receive_to_jit_channel(
amount_msat,
description,
expiry_secs,
max_total_lsp_fee_limit_msat,
)
.await
})
})?;
let (invoice, lsp_total_opening_fee, lsp_prop_opening_fee) =
tokio::task::block_in_place(move || {
runtime.block_on(async move {
liquidity_source
.lsps2_receive_to_jit_channel(
amount_msat,
description,
expiry_secs,
max_total_lsp_fee_limit_msat,
max_proportional_lsp_fee_limit_ppm_msat,
)
.await
})
})?;

// Register payment in payment store.
let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array());
let lsp_fee_limits =
Some(LSPFeeLimits { max_total_opening_fee_msat: Some(lsp_opening_fee) });
let lsp_fee_limits = Some(LSPFeeLimits {
max_total_opening_fee_msat: lsp_total_opening_fee,
max_proportional_opening_fee_ppm_msat: lsp_prop_opening_fee,
});
let payment = PaymentDetails {
hash: payment_hash,
preimage: None,
Expand Down
100 changes: 66 additions & 34 deletions src/liquidity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,54 +198,86 @@ where
pub(crate) async fn lsps2_receive_to_jit_channel(
&self, amount_msat: Option<u64>, description: &str, expiry_secs: u32,
max_total_lsp_fee_limit_msat: Option<u64>,
) -> Result<(Bolt11Invoice, u64), Error> {
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
) -> Result<(Bolt11Invoice, Option<u64>, Option<u64>), Error> {
let lsps2_service = self.lsps2_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;

let fee_response = self.request_opening_fee_params().await?;

if let Some(amount_msat) = amount_msat {
let (min_total_fee_msat, min_prop_fee_ppm_msat, min_opening_params) = if let Some(
amount_msat,
) = amount_msat
{
// `MPP+fixed-invoice` mode
if amount_msat < fee_response.min_payment_size_msat
|| amount_msat > fee_response.max_payment_size_msat
{
log_error!(self.logger, "Failed to request inbound JIT channel as the payment of {}msat doesn't meet LSP limits (min: {}msat, max: {}msat)", amount_msat, fee_response.min_payment_size_msat, fee_response.max_payment_size_msat);
return Err(Error::LiquidityRequestFailed);
}
}

// If it's variable amount, we pick the cheapest opening fee with a dummy value.
let fee_computation_amount = amount_msat.unwrap_or(1_000_000);
let (min_opening_fee_msat, min_opening_params) = fee_response
.opening_fee_params_menu
.iter()
.flat_map(|params| {
if let Some(fee) = compute_opening_fee(
fee_computation_amount,
params.min_fee_msat,
params.proportional as u64,
) {
Some((fee, params))
} else {
None
let (min_total_fee_msat, min_params) = fee_response
.opening_fee_params_menu
.iter()
.flat_map(|params| {
if let Some(fee) = compute_opening_fee(
amount_msat,
params.min_fee_msat,
params.proportional as u64,
) {
Some((fee, params))
} else {
None
}
})
.min_by_key(|p| p.0)
.ok_or_else(|| {
log_error!(self.logger, "Failed to handle response from liquidity service",);
Error::LiquidityRequestFailed
})?;

if let Some(max_total_lsp_fee_limit_msat) = max_total_lsp_fee_limit_msat {
if min_total_fee_msat > max_total_lsp_fee_limit_msat {
log_error!(self.logger, "Failed to request inbound JIT channel as LSP's requested total opening fee of {}msat exceeds our fee limit of {}msat", min_total_fee_msat, max_total_lsp_fee_limit_msat);
return Err(Error::LiquidityFeeTooHigh);
}
})
.min_by_key(|p| p.0)
.ok_or_else(|| {
log_error!(self.logger, "Failed to handle response from liquidity service",);
Error::LiquidityRequestFailed
})?;
}

if let Some(max_total_lsp_fee_limit_msat) = max_total_lsp_fee_limit_msat {
if min_opening_fee_msat > max_total_lsp_fee_limit_msat {
log_error!(self.logger, "Failed to request inbound JIT channel as LSP's requested opening fee of {}msat exceeds our fee limit of {}msat", min_opening_fee_msat, max_total_lsp_fee_limit_msat);
return Err(Error::LiquidityFeeTooHigh);
log_debug!(
self.logger,
"Choosing cheapest liquidity offer, will pay {}msat in total LSP fees",
min_total_fee_msat
);

(Some(min_total_fee_msat), None, min_params)
} else {
// `no-MPP+var-invoice` mode
let (min_prop_fee_ppm_msat, min_params) = fee_response
.opening_fee_params_menu
.iter()
.map(|params| (params.proportional as u64, params))
.min_by_key(|p| p.0)
.ok_or_else(|| {
log_error!(self.logger, "Failed to handle response from liquidity service",);
Error::LiquidityRequestFailed
})?;

if let Some(max_proportional_lsp_fee_limit_ppm_msat) =
max_proportional_lsp_fee_limit_ppm_msat
{
if min_prop_fee_ppm_msat > max_proportional_lsp_fee_limit_ppm_msat {
log_error!(self.logger, "Failed to request inbound JIT channel as LSP's requested proportional opening fee of {} ppm msat exceeds our fee limit of {} ppm msat", min_prop_fee_ppm_msat, max_proportional_lsp_fee_limit_ppm_msat);
return Err(Error::LiquidityFeeTooHigh);
}
}
}

log_debug!(
self.logger,
"Choosing cheapest liquidity offer, will pay {}msat in LSP fees",
min_opening_fee_msat
);
log_debug!(
self.logger,
"Choosing cheapest liquidity offer, will pay {}ppm msat in proportional LSP fees",
min_prop_fee_ppm_msat
);
(None, Some(min_prop_fee_ppm_msat), min_params)
};

let buy_response = self.send_buy_request(amount_msat, min_opening_params.clone()).await?;

Expand Down Expand Up @@ -298,7 +330,7 @@ where
})?;

log_info!(self.logger, "JIT-channel invoice created: {}", invoice);
Ok((invoice, min_opening_fee_msat))
Ok((invoice, min_total_fee_msat, min_prop_fee_ppm_msat))
}

async fn request_opening_fee_params(&self) -> Result<LSPS2FeeResponse, Error> {
Expand Down
4 changes: 4 additions & 0 deletions src/payment_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,14 @@ pub struct LSPFeeLimits {
/// The maximal total amount we allow any configured LSP withhold from us when forwarding the
/// payment.
pub max_total_opening_fee_msat: Option<u64>,
/// The maximal proportional fee, in parts-per-million millisatoshi, we allow any configured
/// LSP withhold from us when forwarding the payment.
pub max_proportional_opening_fee_ppm_msat: Option<u64>,
}

impl_writeable_tlv_based!(LSPFeeLimits, {
(0, max_total_opening_fee_msat, option),
(2, max_proportional_opening_fee_ppm_msat, option),
});

#[derive(Clone, Debug, PartialEq, Eq)]
Expand Down

0 comments on commit 5eec4d1

Please sign in to comment.