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 15, 2024
1 parent bab64dd commit 3248f0d
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 22 deletions.
3 changes: 3 additions & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,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 @@ -174,6 +176,7 @@ enum PaymentStatus {

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

dictionary PaymentDetails {
Expand Down
15 changes: 13 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,18 @@ 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 = info
.lsp_fee_limits
.and_then(|l| {
l.max_total_opening_fee_msat.or_else(|| {
l.max_proportional_opening_fee_ppm_msat.and_then(|max_prop_fee| {
// If it's a variable amount payment, compute the actual fee.
compute_opening_fee(amount_msat, 0, max_prop_fee)
})
})
})
.unwrap_or(0);

if counterparty_skimmed_fee_msat > max_total_opening_fee_msat {
log_info!(
self.logger,
Expand Down
75 changes: 55 additions & 20 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1614,12 +1614,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 @@ -1649,29 +1675,38 @@ 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_total_opening_fee) = tokio::task::block_in_place(move || {
runtime.block_on(async move {
if let Some(amount_msat) = amount_msat {
liquidity_source
.lsps2_receive_to_jit_channel(
amount_msat,
description,
expiry_secs,
max_total_lsp_fee_limit_msat,
)
.await
.map(|(invoice, total_fee)| (invoice, Some(total_fee)))
} else {
// TODO: will be implemented in the next commit
Err(Error::LiquidityRequestFailed)
}
})
})?;
let (invoice, lsp_total_opening_fee, lsp_prop_opening_fee) =
tokio::task::block_in_place(move || {
runtime.block_on(async move {
if let Some(amount_msat) = amount_msat {
liquidity_source
.lsps2_receive_to_jit_channel(
amount_msat,
description,
expiry_secs,
max_total_lsp_fee_limit_msat,
)
.await
.map(|(invoice, total_fee)| (invoice, Some(total_fee), None))
} else {
liquidity_source
.lsps2_receive_variable_amount_to_jit_channel(
description,
expiry_secs,
max_proportional_lsp_fee_limit_ppm_msat,
)
.await
.map(|(invoice, prop_fee)| (invoice, None, Some(prop_fee)))
}
})
})?;

// 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: lsp_total_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
43 changes: 43 additions & 0 deletions src/liquidity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,49 @@ where
Ok((invoice, min_total_fee_msat))
}

pub(crate) async fn lsps2_receive_variable_amount_to_jit_channel(
&self, description: &str, expiry_secs: u32,
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
) -> Result<(Bolt11Invoice, u64), Error> {
let fee_response = self.lsps2_request_opening_fee_params().await?;

let (min_prop_fee_ppm_msat, min_opening_params) = fee_response
.opening_fee_params_menu
.into_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 {}ppm msat in proportional LSP fees",
min_prop_fee_ppm_msat
);

let buy_response = self.lsps2_send_buy_request(None, min_opening_params).await?;
let invoice =
self.lsps2_create_jit_invoice(buy_response, None, description, expiry_secs)?;

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

async fn lsps2_request_opening_fee_params(&self) -> Result<LSPS2FeeResponse, Error> {
let lsps2_service = self.lsps2_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;

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 3248f0d

Please sign in to comment.