Skip to content

Commit 3c05b17

Browse files
committed
Allow for variable amount payments
1 parent 49f497f commit 3c05b17

File tree

5 files changed

+122
-23
lines changed

5 files changed

+122
-23
lines changed

bindings/ldk_node.udl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ interface LDKNode {
8989
Bolt11Invoice receive_variable_amount_payment([ByRef]string description, u32 expiry_secs);
9090
[Throws=NodeError]
9191
Bolt11Invoice receive_payment_via_jit_channel(u64 amount_msat, [ByRef]string description, u32 expiry_secs, u64? max_lsp_fee_limit_msat);
92+
[Throws=NodeError]
93+
Bolt11Invoice receive_variable_amount_payment_via_jit_channel([ByRef]string description, u32 expiry_secs, u64? max_proportional_lsp_fee_limit_ppm_msat);
9294
PaymentDetails? payment([ByRef]PaymentHash payment_hash);
9395
[Throws=NodeError]
9496
void remove_payment([ByRef]PaymentHash payment_hash);
@@ -176,6 +178,7 @@ enum PaymentStatus {
176178

177179
dictionary LSPFeeLimits {
178180
u64? max_total_opening_fee_msat;
181+
u64? max_proportional_opening_fee_ppm_msat;
179182
};
180183

181184
dictionary PaymentDetails {

src/event.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,16 @@ use lightning::util::errors::APIError;
2323
use lightning::util::persist::KVStore;
2424
use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer};
2525

26+
use lightning_liquidity::lsps2::utils::compute_opening_fee;
27+
2628
use bitcoin::blockdata::locktime::absolute::LockTime;
2729
use bitcoin::secp256k1::PublicKey;
2830
use bitcoin::OutPoint;
31+
32+
use rand::{thread_rng, Rng};
33+
2934
use core::future::Future;
3035
use core::task::{Poll, Waker};
31-
use rand::{thread_rng, Rng};
3236
use std::collections::VecDeque;
3337
use std::ops::Deref;
3438
use std::sync::{Arc, Condvar, Mutex, RwLock};
@@ -418,8 +422,18 @@ where
418422
return;
419423
}
420424

421-
let max_total_opening_fee_msat =
422-
info.lsp_fee_limits.and_then(|l| l.max_total_opening_fee_msat).unwrap_or(0);
425+
let max_total_opening_fee_msat = info
426+
.lsp_fee_limits
427+
.and_then(|l| {
428+
l.max_total_opening_fee_msat.or_else(|| {
429+
l.max_proportional_opening_fee_ppm_msat.and_then(|max_prop_fee| {
430+
// If it's a variable amount payment, compute the actual fee.
431+
compute_opening_fee(amount_msat, 0, max_prop_fee)
432+
})
433+
})
434+
})
435+
.unwrap_or(0);
436+
423437
if counterparty_skimmed_fee_msat > max_total_opening_fee_msat {
424438
log_info!(
425439
self.logger,

src/lib.rs

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1623,12 +1623,38 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
16231623
description,
16241624
expiry_secs,
16251625
max_total_lsp_fee_limit_msat,
1626+
None,
1627+
)
1628+
}
1629+
1630+
/// Returns a payable invoice that can be used to request a variable amount payment (also known
1631+
/// as "zero-amount" invoice) and receive it via a newly created just-in-time (JIT) channel.
1632+
///
1633+
/// When the returned invoice is paid, the configured [LSPS2]-compliant LSP will open a channel
1634+
/// to us, supplying just-in-time inbound liquidity.
1635+
///
1636+
/// If set, `max_proportional_lsp_fee_limit_ppm_msat` will limit how much proportional fee, in
1637+
/// parts-per-million millisatoshis, we allow the LSP to take for opening the channel to us.
1638+
/// We'll use its cheapest offer otherwise.
1639+
///
1640+
/// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md
1641+
pub fn receive_variable_amount_payment_via_jit_channel(
1642+
&self, description: &str, expiry_secs: u32,
1643+
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
1644+
) -> Result<Bolt11Invoice, Error> {
1645+
self.receive_payment_via_jit_channel_inner(
1646+
None,
1647+
description,
1648+
expiry_secs,
1649+
None,
1650+
max_proportional_lsp_fee_limit_ppm_msat,
16261651
)
16271652
}
16281653

16291654
fn receive_payment_via_jit_channel_inner(
16301655
&self, amount_msat: Option<u64>, description: &str, expiry_secs: u32,
16311656
max_total_lsp_fee_limit_msat: Option<u64>,
1657+
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
16321658
) -> Result<Bolt11Invoice, Error> {
16331659
let liquidity_source =
16341660
self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;
@@ -1658,29 +1684,38 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
16581684
log_info!(self.logger, "Connected to LSP {}@{}. ", peer_info.node_id, peer_info.address);
16591685

16601686
let liquidity_source = Arc::clone(&liquidity_source);
1661-
let (invoice, lsp_total_opening_fee) = tokio::task::block_in_place(move || {
1662-
runtime.block_on(async move {
1663-
if let Some(amount_msat) = amount_msat {
1664-
liquidity_source
1665-
.lsps2_receive_to_jit_channel(
1666-
amount_msat,
1667-
description,
1668-
expiry_secs,
1669-
max_total_lsp_fee_limit_msat,
1670-
)
1671-
.await
1672-
.map(|(invoice, total_fee)| (invoice, Some(total_fee)))
1673-
} else {
1674-
// TODO: will be implemented in the next commit
1675-
Err(Error::LiquidityRequestFailed)
1676-
}
1677-
})
1678-
})?;
1687+
let (invoice, lsp_total_opening_fee, lsp_prop_opening_fee) =
1688+
tokio::task::block_in_place(move || {
1689+
runtime.block_on(async move {
1690+
if let Some(amount_msat) = amount_msat {
1691+
liquidity_source
1692+
.lsps2_receive_to_jit_channel(
1693+
amount_msat,
1694+
description,
1695+
expiry_secs,
1696+
max_total_lsp_fee_limit_msat,
1697+
)
1698+
.await
1699+
.map(|(invoice, total_fee)| (invoice, Some(total_fee), None))
1700+
} else {
1701+
liquidity_source
1702+
.lsps2_receive_variable_amount_to_jit_channel(
1703+
description,
1704+
expiry_secs,
1705+
max_proportional_lsp_fee_limit_ppm_msat,
1706+
)
1707+
.await
1708+
.map(|(invoice, prop_fee)| (invoice, None, Some(prop_fee)))
1709+
}
1710+
})
1711+
})?;
16791712

16801713
// Register payment in payment store.
16811714
let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array());
1682-
let lsp_fee_limits =
1683-
Some(LSPFeeLimits { max_total_opening_fee_msat: lsp_total_opening_fee });
1715+
let lsp_fee_limits = Some(LSPFeeLimits {
1716+
max_total_opening_fee_msat: lsp_total_opening_fee,
1717+
max_proportional_opening_fee_ppm_msat: lsp_prop_opening_fee,
1718+
});
16841719
let payment = PaymentDetails {
16851720
hash: payment_hash,
16861721
preimage: None,

src/liquidity.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,49 @@ where
249249
Ok((invoice, min_total_fee_msat))
250250
}
251251

252+
pub(crate) async fn lsps2_receive_variable_amount_to_jit_channel(
253+
&self, description: &str, expiry_secs: u32,
254+
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
255+
) -> Result<(Bolt11Invoice, u64), Error> {
256+
let fee_response = self.lsps2_request_opening_fee_params().await?;
257+
258+
let (min_prop_fee_ppm_msat, min_opening_params) = fee_response
259+
.opening_fee_params_menu
260+
.into_iter()
261+
.map(|params| (params.proportional as u64, params))
262+
.min_by_key(|p| p.0)
263+
.ok_or_else(|| {
264+
log_error!(self.logger, "Failed to handle response from liquidity service",);
265+
Error::LiquidityRequestFailed
266+
})?;
267+
268+
if let Some(max_proportional_lsp_fee_limit_ppm_msat) =
269+
max_proportional_lsp_fee_limit_ppm_msat
270+
{
271+
if min_prop_fee_ppm_msat > max_proportional_lsp_fee_limit_ppm_msat {
272+
log_error!(self.logger,
273+
"Failed to request inbound JIT channel as LSP's requested proportional opening fee of {} ppm msat exceeds our fee limit of {} ppm msat",
274+
min_prop_fee_ppm_msat,
275+
max_proportional_lsp_fee_limit_ppm_msat
276+
);
277+
return Err(Error::LiquidityFeeTooHigh);
278+
}
279+
}
280+
281+
log_debug!(
282+
self.logger,
283+
"Choosing cheapest liquidity offer, will pay {}ppm msat in proportional LSP fees",
284+
min_prop_fee_ppm_msat
285+
);
286+
287+
let buy_response = self.lsps2_send_buy_request(None, min_opening_params).await?;
288+
let invoice =
289+
self.lsps2_create_jit_invoice(buy_response, None, description, expiry_secs)?;
290+
291+
log_info!(self.logger, "JIT-channel invoice created: {}", invoice);
292+
Ok((invoice, min_prop_fee_ppm_msat))
293+
}
294+
252295
async fn lsps2_request_opening_fee_params(&self) -> Result<LSPS2FeeResponse, Error> {
253296
let lsps2_service = self.lsps2_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;
254297

src/payment_store.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,14 @@ pub struct LSPFeeLimits {
9292
/// The maximal total amount we allow any configured LSP withhold from us when forwarding the
9393
/// payment.
9494
pub max_total_opening_fee_msat: Option<u64>,
95+
/// The maximal proportional fee, in parts-per-million millisatoshi, we allow any configured
96+
/// LSP withhold from us when forwarding the payment.
97+
pub max_proportional_opening_fee_ppm_msat: Option<u64>,
9598
}
9699

97100
impl_writeable_tlv_based!(LSPFeeLimits, {
98101
(0, max_total_opening_fee_msat, option),
102+
(2, max_proportional_opening_fee_ppm_msat, option),
99103
});
100104

101105
#[derive(Clone, Debug, PartialEq, Eq)]

0 commit comments

Comments
 (0)