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 Jan 23, 2024
1 parent 7d3ca96 commit ba1fd79
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 49 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 @@ -186,6 +188,7 @@ dictionary PaymentDetails {
PaymentDirection direction;
PaymentStatus status;
u64? max_total_lsp_fee_limit_msat;
u64? max_proportional_lsp_fee_limit_ppm_msat;
};

dictionary OutPoint {
Expand Down
18 changes: 16 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_lsp_fee_limit_msat =
info.max_total_lsp_fee_limit_msat.unwrap_or(0);
let max_total_lsp_fee_limit_msat = if let Some(max_total_lsp_fee_limit_msat) =
info.max_total_lsp_fee_limit_msat
{
max_total_lsp_fee_limit_msat
} else if let Some(max_proportional_lsp_fee_limit_ppm_msat) =
info.max_proportional_lsp_fee_limit_ppm_msat
{
// If it's a variable amount payment, compute the actual total opening fee.
compute_opening_fee(amount_msat, 0, max_proportional_lsp_fee_limit_ppm_msat)
.unwrap_or(0)
} else {
0
};

if counterparty_skimmed_fee_msat > max_total_lsp_fee_limit_msat {
log_info!(
self.logger,
Expand Down Expand Up @@ -497,6 +510,7 @@ where
direction: PaymentDirection::Inbound,
status: PaymentStatus::Succeeded,
max_total_lsp_fee_limit_msat: None,
max_proportional_lsp_fee_limit_ppm_msat: None,
};

match self.payment_store.insert(payment) {
Expand Down
62 changes: 49 additions & 13 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1198,6 +1198,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
direction: PaymentDirection::Outbound,
status: PaymentStatus::Pending,
max_total_lsp_fee_limit_msat: None,
max_proportional_lsp_fee_limit_ppm_msat: None,
};
self.payment_store.insert(payment)?;

Expand All @@ -1218,6 +1219,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
direction: PaymentDirection::Outbound,
status: PaymentStatus::Failed,
max_total_lsp_fee_limit_msat: None,
max_proportional_lsp_fee_limit_ppm_msat: None,
};

self.payment_store.insert(payment)?;
Expand Down Expand Up @@ -1306,6 +1308,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
direction: PaymentDirection::Outbound,
status: PaymentStatus::Pending,
max_total_lsp_fee_limit_msat: None,
max_proportional_lsp_fee_limit_ppm_msat: None,
};
self.payment_store.insert(payment)?;

Expand All @@ -1327,6 +1330,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
direction: PaymentDirection::Outbound,
status: PaymentStatus::Failed,
max_total_lsp_fee_limit_msat: None,
max_proportional_lsp_fee_limit_ppm_msat: None,
};
self.payment_store.insert(payment)?;

Expand Down Expand Up @@ -1382,6 +1386,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
direction: PaymentDirection::Outbound,
amount_msat: Some(amount_msat),
max_total_lsp_fee_limit_msat: None,
max_proportional_lsp_fee_limit_ppm_msat: None,
};
self.payment_store.insert(payment)?;

Expand All @@ -1403,6 +1408,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
direction: PaymentDirection::Outbound,
amount_msat: Some(amount_msat),
max_total_lsp_fee_limit_msat: None,
max_proportional_lsp_fee_limit_ppm_msat: None,
};

self.payment_store.insert(payment)?;
Expand Down Expand Up @@ -1577,6 +1583,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
direction: PaymentDirection::Inbound,
status: PaymentStatus::Pending,
max_total_lsp_fee_limit_msat: None,
max_proportional_lsp_fee_limit_ppm_msat: None,
};

self.payment_store.insert(payment)?;
Expand All @@ -1603,12 +1610,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 @@ -1638,18 +1671,20 @@ 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());
Expand All @@ -1660,7 +1695,8 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
amount_msat,
direction: PaymentDirection::Inbound,
status: PaymentStatus::Pending,
max_total_lsp_fee_limit_msat: Some(lsp_opening_fee),
max_total_lsp_fee_limit_msat: lsp_total_opening_fee,
max_proportional_lsp_fee_limit_ppm_msat: lsp_prop_opening_fee,
};

self.payment_store.insert(payment)?;
Expand Down
100 changes: 66 additions & 34 deletions src/liquidity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,54 +191,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 @@ -291,7 +323,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
12 changes: 12 additions & 0 deletions src/payment_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,23 @@ pub struct PaymentDetails {
///
/// [`LdkChannelConfig::accept_underpaying_htlcs`]: lightning::util::config::ChannelConfig::accept_underpaying_htlcs
pub max_total_lsp_fee_limit_msat: Option<u64>,
/// The maximal proportional fee, in parts-per-million millisatoshi, we allow any configured
/// LSP withhold from us when forwarding the payment.
///
/// This is usually only `Some` for payments received via a JIT-channel, in which case the first
/// inbound payment will pay for the LSP's channel opening fees.
///
/// See [`LdkChannelConfig::accept_underpaying_htlcs`] for more information.
///
/// [`LdkChannelConfig::accept_underpaying_htlcs`]: lightning::util::config::ChannelConfig::accept_underpaying_htlcs
pub max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
}

impl_writeable_tlv_based!(PaymentDetails, {
(0, hash, required),
(1, max_total_lsp_fee_limit_msat, option),
(2, preimage, required),
(3, max_proportional_lsp_fee_limit_ppm_msat, option),
(4, secret, required),
(6, amount_msat, required),
(8, direction, required),
Expand Down Expand Up @@ -265,6 +276,7 @@ mod tests {
direction: PaymentDirection::Inbound,
status: PaymentStatus::Pending,
max_total_lsp_fee_limit_msat: None,
max_proportional_lsp_fee_limit_ppm_msat: None,
};

assert_eq!(Ok(false), payment_store.insert(payment.clone()));
Expand Down

0 comments on commit ba1fd79

Please sign in to comment.