Skip to content

Commit 6859fbe

Browse files
committed
Move LSPS2 client logic into liquidity/client/lsps2.rs
1 parent 3b59644 commit 6859fbe

3 files changed

Lines changed: 356 additions & 321 deletions

File tree

src/liquidity/client/lsps2.rs

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

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)