Skip to content

Commit 95c88e5

Browse files
committed
Move LSPS2 client logic into liquidity/client/lsps2.rs
1 parent 36390fe commit 95c88e5

3 files changed

Lines changed: 329 additions & 294 deletions

File tree

src/liquidity/client/lsps2.rs

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
// This file is Copyright its original authors, visible in version control history.
2+
//
3+
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5+
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
6+
// accordance with one or both of these licenses.
7+
8+
use std::collections::HashMap;
9+
use std::ops::Deref;
10+
use std::sync::Mutex;
11+
use std::time::Duration;
12+
13+
use bitcoin::secp256k1::{PublicKey, Secp256k1};
14+
use lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA;
15+
use lightning::ln::msgs::SocketAddress;
16+
use lightning::routing::router::{RouteHint, RouteHintHop};
17+
use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, InvoiceBuilder, RoutingFees};
18+
use lightning_liquidity::lsps0::ser::LSPSRequestId;
19+
use lightning_liquidity::lsps2::client::LSPS2ClientConfig as LdkLSPS2ClientConfig;
20+
use lightning_liquidity::lsps2::msgs::LSPS2OpeningFeeParams;
21+
use lightning_liquidity::lsps2::utils::compute_opening_fee;
22+
use lightning_types::payment::PaymentHash;
23+
use tokio::sync::oneshot;
24+
25+
use crate::logger::{log_debug, log_error, log_info, LdkLogger};
26+
use crate::Error;
27+
28+
use super::super::{LiquiditySource, LIQUIDITY_REQUEST_TIMEOUT_SECS};
29+
30+
pub(crate) struct LSPS2Client {
31+
pub(crate) lsp_node_id: PublicKey,
32+
pub(crate) lsp_address: SocketAddress,
33+
pub(crate) token: Option<String>,
34+
pub(crate) ldk_client_config: LdkLSPS2ClientConfig,
35+
pub(crate) pending_fee_requests:
36+
Mutex<HashMap<LSPSRequestId, oneshot::Sender<LSPS2FeeResponse>>>,
37+
pub(crate) pending_buy_requests:
38+
Mutex<HashMap<LSPSRequestId, oneshot::Sender<LSPS2BuyResponse>>>,
39+
}
40+
41+
#[derive(Debug, Clone)]
42+
pub(crate) struct LSPS2ClientConfig {
43+
pub node_id: PublicKey,
44+
pub address: SocketAddress,
45+
pub token: Option<String>,
46+
}
47+
48+
#[derive(Debug, Clone)]
49+
pub(crate) struct LSPS2FeeResponse {
50+
pub(crate) opening_fee_params_menu: Vec<LSPS2OpeningFeeParams>,
51+
}
52+
53+
#[derive(Debug, Clone)]
54+
pub(crate) struct LSPS2BuyResponse {
55+
pub(crate) intercept_scid: u64,
56+
pub(crate) cltv_expiry_delta: u32,
57+
}
58+
59+
impl<L: Deref> LiquiditySource<L>
60+
where
61+
L::Target: LdkLogger,
62+
{
63+
pub(crate) fn get_lsps2_lsp_details(&self) -> Option<(PublicKey, SocketAddress)> {
64+
self.lsps2_client.as_ref().map(|s| (s.lsp_node_id, s.lsp_address.clone()))
65+
}
66+
67+
pub(crate) async fn lsps2_receive_to_jit_channel(
68+
&self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32,
69+
max_total_lsp_fee_limit_msat: Option<u64>, payment_hash: Option<PaymentHash>,
70+
) -> Result<(Bolt11Invoice, u64), Error> {
71+
let fee_response = self.lsps2_request_opening_fee_params().await?;
72+
73+
let (min_total_fee_msat, min_opening_params) = fee_response
74+
.opening_fee_params_menu
75+
.into_iter()
76+
.filter_map(|params| {
77+
if amount_msat < params.min_payment_size_msat
78+
|| amount_msat > params.max_payment_size_msat
79+
{
80+
log_debug!(self.logger,
81+
"Skipping LSP-offered JIT parameters as the payment of {}msat doesn't meet LSP limits (min: {}msat, max: {}msat)",
82+
amount_msat,
83+
params.min_payment_size_msat,
84+
params.max_payment_size_msat
85+
);
86+
None
87+
} else {
88+
compute_opening_fee(amount_msat, params.min_fee_msat, params.proportional as u64)
89+
.map(|fee| (fee, params))
90+
}
91+
})
92+
.min_by_key(|p| p.0)
93+
.ok_or_else(|| {
94+
log_error!(self.logger, "Failed to handle response from liquidity service",);
95+
Error::LiquidityRequestFailed
96+
})?;
97+
98+
if let Some(max_total_lsp_fee_limit_msat) = max_total_lsp_fee_limit_msat {
99+
if min_total_fee_msat > max_total_lsp_fee_limit_msat {
100+
log_error!(self.logger,
101+
"Failed to request inbound JIT channel as LSP's requested total opening fee of {}msat exceeds our fee limit of {}msat",
102+
min_total_fee_msat, max_total_lsp_fee_limit_msat
103+
);
104+
return Err(Error::LiquidityFeeTooHigh);
105+
}
106+
}
107+
108+
log_debug!(
109+
self.logger,
110+
"Choosing cheapest liquidity offer, will pay {}msat in total LSP fees",
111+
min_total_fee_msat
112+
);
113+
114+
let buy_response =
115+
self.lsps2_send_buy_request(Some(amount_msat), min_opening_params).await?;
116+
let invoice = self.lsps2_create_jit_invoice(
117+
buy_response,
118+
Some(amount_msat),
119+
description,
120+
expiry_secs,
121+
payment_hash,
122+
)?;
123+
124+
log_info!(self.logger, "JIT-channel invoice created: {}", invoice);
125+
Ok((invoice, min_total_fee_msat))
126+
}
127+
128+
pub(crate) async fn lsps2_receive_variable_amount_to_jit_channel(
129+
&self, description: &Bolt11InvoiceDescription, expiry_secs: u32,
130+
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>, payment_hash: Option<PaymentHash>,
131+
) -> Result<(Bolt11Invoice, u64), Error> {
132+
let fee_response = self.lsps2_request_opening_fee_params().await?;
133+
134+
let (min_prop_fee_ppm_msat, min_opening_params) = fee_response
135+
.opening_fee_params_menu
136+
.into_iter()
137+
.map(|params| (params.proportional as u64, params))
138+
.min_by_key(|p| p.0)
139+
.ok_or_else(|| {
140+
log_error!(self.logger, "Failed to handle response from liquidity service",);
141+
Error::LiquidityRequestFailed
142+
})?;
143+
144+
if let Some(max_proportional_lsp_fee_limit_ppm_msat) =
145+
max_proportional_lsp_fee_limit_ppm_msat
146+
{
147+
if min_prop_fee_ppm_msat > max_proportional_lsp_fee_limit_ppm_msat {
148+
log_error!(self.logger,
149+
"Failed to request inbound JIT channel as LSP's requested proportional opening fee of {} ppm msat exceeds our fee limit of {} ppm msat",
150+
min_prop_fee_ppm_msat,
151+
max_proportional_lsp_fee_limit_ppm_msat
152+
);
153+
return Err(Error::LiquidityFeeTooHigh);
154+
}
155+
}
156+
157+
log_debug!(
158+
self.logger,
159+
"Choosing cheapest liquidity offer, will pay {}ppm msat in proportional LSP fees",
160+
min_prop_fee_ppm_msat
161+
);
162+
163+
let buy_response = self.lsps2_send_buy_request(None, min_opening_params).await?;
164+
let invoice = self.lsps2_create_jit_invoice(
165+
buy_response,
166+
None,
167+
description,
168+
expiry_secs,
169+
payment_hash,
170+
)?;
171+
172+
log_info!(self.logger, "JIT-channel invoice created: {}", invoice);
173+
Ok((invoice, min_prop_fee_ppm_msat))
174+
}
175+
176+
async fn lsps2_request_opening_fee_params(&self) -> Result<LSPS2FeeResponse, Error> {
177+
let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;
178+
179+
let client_handler = self.liquidity_manager.lsps2_client_handler().ok_or_else(|| {
180+
log_error!(self.logger, "Liquidity client was not configured.",);
181+
Error::LiquiditySourceUnavailable
182+
})?;
183+
184+
let (fee_request_sender, fee_request_receiver) = oneshot::channel();
185+
{
186+
let mut pending_fee_requests_lock =
187+
lsps2_client.pending_fee_requests.lock().expect("lock");
188+
let request_id = client_handler
189+
.request_opening_params(lsps2_client.lsp_node_id, lsps2_client.token.clone());
190+
pending_fee_requests_lock.insert(request_id, fee_request_sender);
191+
}
192+
193+
tokio::time::timeout(
194+
Duration::from_secs(LIQUIDITY_REQUEST_TIMEOUT_SECS),
195+
fee_request_receiver,
196+
)
197+
.await
198+
.map_err(|e| {
199+
log_error!(self.logger, "Liquidity request timed out: {}", e);
200+
Error::LiquidityRequestFailed
201+
})?
202+
.map_err(|e| {
203+
log_error!(self.logger, "Failed to handle response from liquidity service: {}", e);
204+
Error::LiquidityRequestFailed
205+
})
206+
}
207+
208+
async fn lsps2_send_buy_request(
209+
&self, amount_msat: Option<u64>, opening_fee_params: LSPS2OpeningFeeParams,
210+
) -> Result<LSPS2BuyResponse, Error> {
211+
let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;
212+
213+
let client_handler = self.liquidity_manager.lsps2_client_handler().ok_or_else(|| {
214+
log_error!(self.logger, "Liquidity client was not configured.",);
215+
Error::LiquiditySourceUnavailable
216+
})?;
217+
218+
let (buy_request_sender, buy_request_receiver) = oneshot::channel();
219+
{
220+
let mut pending_buy_requests_lock =
221+
lsps2_client.pending_buy_requests.lock().expect("lock");
222+
let request_id = client_handler
223+
.select_opening_params(lsps2_client.lsp_node_id, amount_msat, opening_fee_params)
224+
.map_err(|e| {
225+
log_error!(
226+
self.logger,
227+
"Failed to send buy request to liquidity service: {:?}",
228+
e
229+
);
230+
Error::LiquidityRequestFailed
231+
})?;
232+
pending_buy_requests_lock.insert(request_id, buy_request_sender);
233+
}
234+
235+
let buy_response = tokio::time::timeout(
236+
Duration::from_secs(LIQUIDITY_REQUEST_TIMEOUT_SECS),
237+
buy_request_receiver,
238+
)
239+
.await
240+
.map_err(|e| {
241+
log_error!(self.logger, "Liquidity request timed out: {}", e);
242+
Error::LiquidityRequestFailed
243+
})?
244+
.map_err(|e| {
245+
log_error!(self.logger, "Failed to handle response from liquidity service: {:?}", e);
246+
Error::LiquidityRequestFailed
247+
})?;
248+
249+
Ok(buy_response)
250+
}
251+
252+
fn lsps2_create_jit_invoice(
253+
&self, buy_response: LSPS2BuyResponse, amount_msat: Option<u64>,
254+
description: &Bolt11InvoiceDescription, expiry_secs: u32,
255+
payment_hash: Option<PaymentHash>,
256+
) -> Result<Bolt11Invoice, Error> {
257+
let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;
258+
259+
// LSPS2 requires min_final_cltv_expiry_delta to be at least 2 more than usual.
260+
let min_final_cltv_expiry_delta = MIN_FINAL_CLTV_EXPIRY_DELTA + 2;
261+
let (payment_hash, payment_secret) = match payment_hash {
262+
Some(payment_hash) => {
263+
let payment_secret = self
264+
.channel_manager
265+
.create_inbound_payment_for_hash(
266+
payment_hash,
267+
None,
268+
expiry_secs,
269+
Some(min_final_cltv_expiry_delta),
270+
)
271+
.map_err(|e| {
272+
log_error!(self.logger, "Failed to register inbound payment: {:?}", e);
273+
Error::InvoiceCreationFailed
274+
})?;
275+
(payment_hash, payment_secret)
276+
},
277+
None => self
278+
.channel_manager
279+
.create_inbound_payment(None, expiry_secs, Some(min_final_cltv_expiry_delta))
280+
.map_err(|e| {
281+
log_error!(self.logger, "Failed to register inbound payment: {:?}", e);
282+
Error::InvoiceCreationFailed
283+
})?,
284+
};
285+
286+
let route_hint = RouteHint(vec![RouteHintHop {
287+
src_node_id: lsps2_client.lsp_node_id,
288+
short_channel_id: buy_response.intercept_scid,
289+
fees: RoutingFees { base_msat: 0, proportional_millionths: 0 },
290+
cltv_expiry_delta: buy_response.cltv_expiry_delta as u16,
291+
htlc_minimum_msat: None,
292+
htlc_maximum_msat: None,
293+
}]);
294+
295+
let currency = self.config.network.into();
296+
let mut invoice_builder = InvoiceBuilder::new(currency)
297+
.invoice_description(description.clone())
298+
.payment_hash(payment_hash)
299+
.payment_secret(payment_secret)
300+
.current_timestamp()
301+
.min_final_cltv_expiry_delta(min_final_cltv_expiry_delta.into())
302+
.expiry_time(Duration::from_secs(expiry_secs.into()))
303+
.private_route(route_hint);
304+
305+
if let Some(amount_msat) = amount_msat {
306+
invoice_builder = invoice_builder.amount_milli_satoshis(amount_msat).basic_mpp();
307+
}
308+
309+
invoice_builder
310+
.build_signed(|hash| {
311+
Secp256k1::new()
312+
.sign_ecdsa_recoverable(hash, &self.keys_manager.get_node_secret_key())
313+
})
314+
.map_err(|e| {
315+
log_error!(self.logger, "Failed to build and sign invoice: {}", e);
316+
Error::InvoiceCreationFailed
317+
})
318+
}
319+
}

src/liquidity/client/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
// accordance with one or both of these licenses.
77

88
pub(crate) mod lsps1;
9+
pub(crate) mod lsps2;

0 commit comments

Comments
 (0)