Skip to content

Commit 6644bd0

Browse files
committed
Allow for variable amount payments
1 parent c76811e commit 6644bd0

4 files changed

Lines changed: 143 additions & 49 deletions

File tree

src/event.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer};
2626
use bitcoin::blockdata::locktime::absolute::LockTime;
2727
use bitcoin::secp256k1::PublicKey;
2828
use bitcoin::OutPoint;
29+
use lightning_liquidity::lsps2::utils::compute_opening_fee;
2930
use rand::{thread_rng, Rng};
3031
use std::collections::VecDeque;
3132
use std::ops::Deref;
@@ -381,8 +382,20 @@ where
381382
return;
382383
}
383384

384-
let max_total_lsp_fee_limit_msat =
385-
info.max_total_lsp_fee_limit_msat.unwrap_or(0);
385+
let max_total_lsp_fee_limit_msat = if let Some(max_total_lsp_fee_limit_msat) =
386+
info.max_total_lsp_fee_limit_msat
387+
{
388+
max_total_lsp_fee_limit_msat
389+
} else if let Some(max_proportional_lsp_fee_limit_ppm_msat) =
390+
info.max_proportional_lsp_fee_limit_ppm_msat
391+
{
392+
// If it's a variable amount payment, compute the actual total opening fee.
393+
compute_opening_fee(amount_msat, 0, max_proportional_lsp_fee_limit_ppm_msat)
394+
.unwrap_or(0)
395+
} else {
396+
0
397+
};
398+
386399
if counterparty_skimmed_fee_msat > max_total_lsp_fee_limit_msat {
387400
log_info!(
388401
self.logger,
@@ -497,6 +510,7 @@ where
497510
direction: PaymentDirection::Inbound,
498511
status: PaymentStatus::Succeeded,
499512
max_total_lsp_fee_limit_msat: None,
513+
max_proportional_lsp_fee_limit_ppm_msat: None,
500514
};
501515

502516
match self.payment_store.insert(payment) {

src/lib.rs

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,6 +1198,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
11981198
direction: PaymentDirection::Outbound,
11991199
status: PaymentStatus::Pending,
12001200
max_total_lsp_fee_limit_msat: None,
1201+
max_proportional_lsp_fee_limit_ppm_msat: None,
12011202
};
12021203
self.payment_store.insert(payment)?;
12031204

@@ -1218,6 +1219,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
12181219
direction: PaymentDirection::Outbound,
12191220
status: PaymentStatus::Failed,
12201221
max_total_lsp_fee_limit_msat: None,
1222+
max_proportional_lsp_fee_limit_ppm_msat: None,
12211223
};
12221224

12231225
self.payment_store.insert(payment)?;
@@ -1306,6 +1308,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
13061308
direction: PaymentDirection::Outbound,
13071309
status: PaymentStatus::Pending,
13081310
max_total_lsp_fee_limit_msat: None,
1311+
max_proportional_lsp_fee_limit_ppm_msat: None,
13091312
};
13101313
self.payment_store.insert(payment)?;
13111314

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

@@ -1382,6 +1386,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
13821386
direction: PaymentDirection::Outbound,
13831387
amount_msat: Some(amount_msat),
13841388
max_total_lsp_fee_limit_msat: None,
1389+
max_proportional_lsp_fee_limit_ppm_msat: None,
13851390
};
13861391
self.payment_store.insert(payment)?;
13871392

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

14081414
self.payment_store.insert(payment)?;
@@ -1577,6 +1583,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
15771583
direction: PaymentDirection::Inbound,
15781584
status: PaymentStatus::Pending,
15791585
max_total_lsp_fee_limit_msat: None,
1586+
max_proportional_lsp_fee_limit_ppm_msat: None,
15801587
};
15811588

15821589
self.payment_store.insert(payment)?;
@@ -1603,12 +1610,38 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
16031610
description,
16041611
expiry_secs,
16051612
max_total_lsp_fee_limit_msat,
1613+
None,
1614+
)
1615+
}
1616+
1617+
/// Returns a payable invoice that can be used to request a variable amount payment (also known
1618+
/// as "zero-amount" invoice) and receive it via a newly created just-in-time (JIT) channel.
1619+
///
1620+
/// When the returned invoice is paid, the configured [LSPS2]-compliant LSP will open a channel
1621+
/// to us, supplying just-in-time inbound liquidity.
1622+
///
1623+
/// If set, `max_proportional_lsp_fee_limit_ppm_msat` will limit how much proportional fee, in
1624+
/// parts-per-million millisatoshis, we allow the LSP to take for opening the channel to us.
1625+
/// We'll use its cheapest offer otherwise.
1626+
///
1627+
/// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md
1628+
pub fn receive_variable_amount_payment_via_jit_channel(
1629+
&self, description: &str, expiry_secs: u32,
1630+
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
1631+
) -> Result<Bolt11Invoice, Error> {
1632+
self.receive_payment_via_jit_channel_inner(
1633+
None,
1634+
description,
1635+
expiry_secs,
1636+
None,
1637+
max_proportional_lsp_fee_limit_ppm_msat,
16061638
)
16071639
}
16081640

16091641
fn receive_payment_via_jit_channel_inner(
16101642
&self, amount_msat: Option<u64>, description: &str, expiry_secs: u32,
16111643
max_total_lsp_fee_limit_msat: Option<u64>,
1644+
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
16121645
) -> Result<Bolt11Invoice, Error> {
16131646
let liquidity_source =
16141647
self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;
@@ -1638,18 +1671,20 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
16381671
log_info!(self.logger, "Connected to LSP {}@{}. ", peer_info.node_id, peer_info.address);
16391672

16401673
let liquidity_source = Arc::clone(&liquidity_source);
1641-
let (invoice, lsp_opening_fee) = tokio::task::block_in_place(move || {
1642-
runtime.block_on(async move {
1643-
liquidity_source
1644-
.lsps2_receive_to_jit_channel(
1645-
amount_msat,
1646-
description,
1647-
expiry_secs,
1648-
max_total_lsp_fee_limit_msat,
1649-
)
1650-
.await
1651-
})
1652-
})?;
1674+
let (invoice, lsp_total_opening_fee, lsp_prop_opening_fee) =
1675+
tokio::task::block_in_place(move || {
1676+
runtime.block_on(async move {
1677+
liquidity_source
1678+
.lsps2_receive_to_jit_channel(
1679+
amount_msat,
1680+
description,
1681+
expiry_secs,
1682+
max_total_lsp_fee_limit_msat,
1683+
max_proportional_lsp_fee_limit_ppm_msat,
1684+
)
1685+
.await
1686+
})
1687+
})?;
16531688

16541689
// Register payment in payment store.
16551690
let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array());
@@ -1660,7 +1695,8 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
16601695
amount_msat,
16611696
direction: PaymentDirection::Inbound,
16621697
status: PaymentStatus::Pending,
1663-
max_total_lsp_fee_limit_msat: Some(lsp_opening_fee),
1698+
max_total_lsp_fee_limit_msat: lsp_total_opening_fee,
1699+
max_proportional_lsp_fee_limit_ppm_msat: lsp_prop_opening_fee,
16641700
};
16651701

16661702
self.payment_store.insert(payment)?;

src/liquidity.rs

Lines changed: 66 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -191,54 +191,86 @@ where
191191
pub(crate) async fn lsps2_receive_to_jit_channel(
192192
&self, amount_msat: Option<u64>, description: &str, expiry_secs: u32,
193193
max_total_lsp_fee_limit_msat: Option<u64>,
194-
) -> Result<(Bolt11Invoice, u64), Error> {
194+
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
195+
) -> Result<(Bolt11Invoice, Option<u64>, Option<u64>), Error> {
195196
let lsps2_service = self.lsps2_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;
196197

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

199-
if let Some(amount_msat) = amount_msat {
200+
let (min_total_fee_msat, min_prop_fee_ppm_msat, min_opening_params) = if let Some(
201+
amount_msat,
202+
) = amount_msat
203+
{
204+
// `MPP+fixed-invoice` mode
200205
if amount_msat < fee_response.min_payment_size_msat
201206
|| amount_msat > fee_response.max_payment_size_msat
202207
{
203208
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);
204209
return Err(Error::LiquidityRequestFailed);
205210
}
206-
}
207211

208-
// If it's variable amount, we pick the cheapest opening fee with a dummy value.
209-
let fee_computation_amount = amount_msat.unwrap_or(1_000_000);
210-
let (min_opening_fee_msat, min_opening_params) = fee_response
211-
.opening_fee_params_menu
212-
.iter()
213-
.flat_map(|params| {
214-
if let Some(fee) = compute_opening_fee(
215-
fee_computation_amount,
216-
params.min_fee_msat,
217-
params.proportional as u64,
218-
) {
219-
Some((fee, params))
220-
} else {
221-
None
212+
let (min_total_fee_msat, min_params) = fee_response
213+
.opening_fee_params_menu
214+
.iter()
215+
.flat_map(|params| {
216+
if let Some(fee) = compute_opening_fee(
217+
amount_msat,
218+
params.min_fee_msat,
219+
params.proportional as u64,
220+
) {
221+
Some((fee, params))
222+
} else {
223+
None
224+
}
225+
})
226+
.min_by_key(|p| p.0)
227+
.ok_or_else(|| {
228+
log_error!(self.logger, "Failed to handle response from liquidity service",);
229+
Error::LiquidityRequestFailed
230+
})?;
231+
232+
if let Some(max_total_lsp_fee_limit_msat) = max_total_lsp_fee_limit_msat {
233+
if min_total_fee_msat > max_total_lsp_fee_limit_msat {
234+
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);
235+
return Err(Error::LiquidityFeeTooHigh);
222236
}
223-
})
224-
.min_by_key(|p| p.0)
225-
.ok_or_else(|| {
226-
log_error!(self.logger, "Failed to handle response from liquidity service",);
227-
Error::LiquidityRequestFailed
228-
})?;
237+
}
229238

230-
if let Some(max_total_lsp_fee_limit_msat) = max_total_lsp_fee_limit_msat {
231-
if min_opening_fee_msat > max_total_lsp_fee_limit_msat {
232-
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);
233-
return Err(Error::LiquidityFeeTooHigh);
239+
log_debug!(
240+
self.logger,
241+
"Choosing cheapest liquidity offer, will pay {}msat in total LSP fees",
242+
min_total_fee_msat
243+
);
244+
245+
(Some(min_total_fee_msat), None, min_params)
246+
} else {
247+
// `no-MPP+var-invoice` mode
248+
let (min_prop_fee_ppm_msat, min_params) = fee_response
249+
.opening_fee_params_menu
250+
.iter()
251+
.map(|params| (params.proportional as u64, params))
252+
.min_by_key(|p| p.0)
253+
.ok_or_else(|| {
254+
log_error!(self.logger, "Failed to handle response from liquidity service",);
255+
Error::LiquidityRequestFailed
256+
})?;
257+
258+
if let Some(max_proportional_lsp_fee_limit_ppm_msat) =
259+
max_proportional_lsp_fee_limit_ppm_msat
260+
{
261+
if min_prop_fee_ppm_msat > max_proportional_lsp_fee_limit_ppm_msat {
262+
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);
263+
return Err(Error::LiquidityFeeTooHigh);
264+
}
234265
}
235-
}
236266

237-
log_debug!(
238-
self.logger,
239-
"Choosing cheapest liquidity offer, will pay {}msat in LSP fees",
240-
min_opening_fee_msat
241-
);
267+
log_debug!(
268+
self.logger,
269+
"Choosing cheapest liquidity offer, will pay {}ppm msat in proportional LSP fees",
270+
min_prop_fee_ppm_msat
271+
);
272+
(None, Some(min_prop_fee_ppm_msat), min_params)
273+
};
242274

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

@@ -291,7 +323,7 @@ where
291323
})?;
292324

293325
log_info!(self.logger, "JIT-channel invoice created: {}", invoice);
294-
Ok((invoice, min_opening_fee_msat))
326+
Ok((invoice, min_total_fee_msat, min_prop_fee_ppm_msat))
295327
}
296328

297329
async fn request_opening_fee_params(&self) -> Result<LSPS2FeeResponse, Error> {

src/payment_store.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,23 @@ pub struct PaymentDetails {
4040
///
4141
/// [`LdkChannelConfig::accept_underpaying_htlcs`]: lightning::util::config::ChannelConfig::accept_underpaying_htlcs
4242
pub max_total_lsp_fee_limit_msat: Option<u64>,
43+
/// The maximal proportional fee, in parts-per-million millisatoshi, we allow any configured
44+
/// LSP withhold from us when forwarding the payment.
45+
///
46+
/// This is usually only `Some` for payments received via a JIT-channel, in which case the first
47+
/// inbound payment will pay for the LSP's channel opening fees.
48+
///
49+
/// See [`LdkChannelConfig::accept_underpaying_htlcs`] for more information.
50+
///
51+
/// [`LdkChannelConfig::accept_underpaying_htlcs`]: lightning::util::config::ChannelConfig::accept_underpaying_htlcs
52+
pub max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
4353
}
4454

4555
impl_writeable_tlv_based!(PaymentDetails, {
4656
(0, hash, required),
4757
(1, max_total_lsp_fee_limit_msat, option),
4858
(2, preimage, required),
59+
(3, max_proportional_lsp_fee_limit_ppm_msat, option),
4960
(4, secret, required),
5061
(6, amount_msat, required),
5162
(8, direction, required),
@@ -265,6 +276,7 @@ mod tests {
265276
direction: PaymentDirection::Inbound,
266277
status: PaymentStatus::Pending,
267278
max_total_lsp_fee_limit_msat: None,
279+
max_proportional_lsp_fee_limit_ppm_msat: None,
268280
};
269281

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

0 commit comments

Comments
 (0)