Skip to content

Commit b9b1c3e

Browse files
committed
Move LSPS1 client logic into liquidity/client/lsps1.rs
1 parent 060ff7d commit b9b1c3e

3 files changed

Lines changed: 363 additions & 321 deletions

File tree

src/liquidity/client/lsps1.rs

Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
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::{Arc, Mutex};
11+
use std::time::Duration;
12+
13+
use bitcoin::secp256k1::PublicKey;
14+
use lightning::ln::msgs::SocketAddress;
15+
use lightning_liquidity::lsps0::ser::LSPSRequestId;
16+
use lightning_liquidity::lsps1::client::LSPS1ClientConfig as LdkLSPS1ClientConfig;
17+
use lightning_liquidity::lsps1::msgs::{
18+
LSPS1ChannelInfo, LSPS1Options, LSPS1OrderId, LSPS1OrderParams,
19+
};
20+
use tokio::sync::oneshot;
21+
22+
use crate::connection::ConnectionManager;
23+
use crate::logger::{log_error, log_info, LdkLogger, Logger};
24+
use crate::runtime::Runtime;
25+
use crate::types::Wallet;
26+
use crate::Error;
27+
28+
use super::super::{LiquiditySource, LIQUIDITY_REQUEST_TIMEOUT_SECS};
29+
30+
pub(crate) struct LSPS1Client {
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: LdkLSPS1ClientConfig,
35+
pub(crate) pending_opening_params_requests:
36+
Mutex<HashMap<LSPSRequestId, oneshot::Sender<LSPS1OpeningParamsResponse>>>,
37+
pub(crate) pending_create_order_requests:
38+
Mutex<HashMap<LSPSRequestId, oneshot::Sender<LSPS1OrderStatus>>>,
39+
pub(crate) pending_check_order_status_requests:
40+
Mutex<HashMap<LSPSRequestId, oneshot::Sender<LSPS1OrderStatus>>>,
41+
}
42+
43+
#[derive(Debug, Clone)]
44+
pub(crate) struct LSPS1ClientConfig {
45+
pub node_id: PublicKey,
46+
pub address: SocketAddress,
47+
pub token: Option<String>,
48+
}
49+
50+
#[derive(Debug, Clone)]
51+
pub(crate) struct LSPS1OpeningParamsResponse {
52+
pub(crate) supported_options: LSPS1Options,
53+
}
54+
55+
/// Represents the status of an LSPS1 channel request.
56+
#[derive(Debug, Clone)]
57+
pub struct LSPS1OrderStatus {
58+
/// The id of the channel order.
59+
pub order_id: LSPS1OrderId,
60+
/// The parameters of channel order.
61+
pub order_params: LSPS1OrderParams,
62+
/// Contains details about how to pay for the order.
63+
pub payment_options: LSPS1PaymentInfo,
64+
/// Contains information about the channel state.
65+
pub channel_state: Option<LSPS1ChannelInfo>,
66+
}
67+
68+
#[cfg(not(feature = "uniffi"))]
69+
type LSPS1PaymentInfo = lightning_liquidity::lsps1::msgs::LSPS1PaymentInfo;
70+
71+
#[cfg(feature = "uniffi")]
72+
type LSPS1PaymentInfo = crate::ffi::LSPS1PaymentInfo;
73+
74+
impl<L: Deref> LiquiditySource<L>
75+
where
76+
L::Target: LdkLogger,
77+
{
78+
pub(crate) fn get_lsps1_lsp_details(&self) -> Option<(PublicKey, SocketAddress)> {
79+
self.lsps1_client.as_ref().map(|s| (s.lsp_node_id, s.lsp_address.clone()))
80+
}
81+
82+
pub(crate) async fn lsps1_request_opening_params(
83+
&self,
84+
) -> Result<LSPS1OpeningParamsResponse, Error> {
85+
let lsps1_client = self.lsps1_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;
86+
87+
let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| {
88+
log_error!(self.logger, "LSPS1 liquidity client was not configured.",);
89+
Error::LiquiditySourceUnavailable
90+
})?;
91+
92+
let (request_sender, request_receiver) = oneshot::channel();
93+
{
94+
let mut pending_opening_params_requests_lock =
95+
lsps1_client.pending_opening_params_requests.lock().expect("lock");
96+
let request_id = client_handler.request_supported_options(lsps1_client.lsp_node_id);
97+
pending_opening_params_requests_lock.insert(request_id, request_sender);
98+
}
99+
100+
tokio::time::timeout(Duration::from_secs(LIQUIDITY_REQUEST_TIMEOUT_SECS), request_receiver)
101+
.await
102+
.map_err(|e| {
103+
log_error!(self.logger, "Liquidity request timed out: {}", e);
104+
Error::LiquidityRequestFailed
105+
})?
106+
.map_err(|e| {
107+
log_error!(self.logger, "Failed to handle response from liquidity service: {}", e);
108+
Error::LiquidityRequestFailed
109+
})
110+
}
111+
112+
pub(crate) async fn lsps1_request_channel(
113+
&self, lsp_balance_sat: u64, client_balance_sat: u64, channel_expiry_blocks: u32,
114+
announce_channel: bool, refund_address: bitcoin::Address,
115+
) -> Result<LSPS1OrderStatus, Error> {
116+
let lsps1_client = self.lsps1_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;
117+
let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| {
118+
log_error!(self.logger, "LSPS1 liquidity client was not configured.",);
119+
Error::LiquiditySourceUnavailable
120+
})?;
121+
122+
let lsp_limits = self.lsps1_request_opening_params().await?.supported_options;
123+
let channel_size_sat = lsp_balance_sat + client_balance_sat;
124+
125+
if channel_size_sat < lsp_limits.min_channel_balance_sat
126+
|| channel_size_sat > lsp_limits.max_channel_balance_sat
127+
{
128+
log_error!(
129+
self.logger,
130+
"Requested channel size of {}sat doesn't meet the LSP-provided limits (min: {}sat, max: {}sat).",
131+
channel_size_sat,
132+
lsp_limits.min_channel_balance_sat,
133+
lsp_limits.max_channel_balance_sat
134+
);
135+
return Err(Error::LiquidityRequestFailed);
136+
}
137+
138+
if lsp_balance_sat < lsp_limits.min_initial_lsp_balance_sat
139+
|| lsp_balance_sat > lsp_limits.max_initial_lsp_balance_sat
140+
{
141+
log_error!(
142+
self.logger,
143+
"Requested LSP-side balance of {}sat doesn't meet the LSP-provided limits (min: {}sat, max: {}sat).",
144+
lsp_balance_sat,
145+
lsp_limits.min_initial_lsp_balance_sat,
146+
lsp_limits.max_initial_lsp_balance_sat
147+
);
148+
return Err(Error::LiquidityRequestFailed);
149+
}
150+
151+
if client_balance_sat < lsp_limits.min_initial_client_balance_sat
152+
|| client_balance_sat > lsp_limits.max_initial_client_balance_sat
153+
{
154+
log_error!(
155+
self.logger,
156+
"Requested client-side balance of {}sat doesn't meet the LSP-provided limits (min: {}sat, max: {}sat).",
157+
client_balance_sat,
158+
lsp_limits.min_initial_client_balance_sat,
159+
lsp_limits.max_initial_client_balance_sat
160+
);
161+
return Err(Error::LiquidityRequestFailed);
162+
}
163+
164+
let order_params = LSPS1OrderParams {
165+
lsp_balance_sat,
166+
client_balance_sat,
167+
required_channel_confirmations: lsp_limits.min_required_channel_confirmations,
168+
funding_confirms_within_blocks: lsp_limits.min_funding_confirms_within_blocks,
169+
channel_expiry_blocks,
170+
token: lsps1_client.token.clone(),
171+
announce_channel,
172+
};
173+
174+
let (request_sender, request_receiver) = oneshot::channel();
175+
let request_id;
176+
{
177+
let mut pending_create_order_requests_lock =
178+
lsps1_client.pending_create_order_requests.lock().expect("lock");
179+
request_id = client_handler.create_order(
180+
&lsps1_client.lsp_node_id,
181+
order_params.clone(),
182+
Some(refund_address),
183+
);
184+
pending_create_order_requests_lock.insert(request_id.clone(), request_sender);
185+
}
186+
187+
let response = tokio::time::timeout(
188+
Duration::from_secs(LIQUIDITY_REQUEST_TIMEOUT_SECS),
189+
request_receiver,
190+
)
191+
.await
192+
.map_err(|e| {
193+
log_error!(self.logger, "Liquidity request with ID {:?} timed out: {}", request_id, e);
194+
Error::LiquidityRequestFailed
195+
})?
196+
.map_err(|e| {
197+
log_error!(self.logger, "Failed to handle response from liquidity service: {}", e);
198+
Error::LiquidityRequestFailed
199+
})?;
200+
201+
if response.order_params != order_params {
202+
log_error!(
203+
self.logger,
204+
"Aborting LSPS1 request as LSP-provided parameters don't match our order. Expected: {:?}, Received: {:?}", order_params, response.order_params
205+
);
206+
return Err(Error::LiquidityRequestFailed);
207+
}
208+
209+
Ok(response)
210+
}
211+
212+
pub(crate) async fn lsps1_check_order_status(
213+
&self, order_id: LSPS1OrderId,
214+
) -> Result<LSPS1OrderStatus, Error> {
215+
let lsps1_client = self.lsps1_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;
216+
let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| {
217+
log_error!(self.logger, "LSPS1 liquidity client was not configured.",);
218+
Error::LiquiditySourceUnavailable
219+
})?;
220+
221+
let (request_sender, request_receiver) = oneshot::channel();
222+
{
223+
let mut pending_check_order_status_requests_lock =
224+
lsps1_client.pending_check_order_status_requests.lock().expect("lock");
225+
let request_id = client_handler.check_order_status(&lsps1_client.lsp_node_id, order_id);
226+
pending_check_order_status_requests_lock.insert(request_id, request_sender);
227+
}
228+
229+
let response = tokio::time::timeout(
230+
Duration::from_secs(LIQUIDITY_REQUEST_TIMEOUT_SECS),
231+
request_receiver,
232+
)
233+
.await
234+
.map_err(|e| {
235+
log_error!(self.logger, "Liquidity request timed out: {}", e);
236+
Error::LiquidityRequestFailed
237+
})?
238+
.map_err(|e| {
239+
log_error!(self.logger, "Failed to handle response from liquidity service: {}", e);
240+
Error::LiquidityRequestFailed
241+
})?;
242+
243+
Ok(response)
244+
}
245+
}
246+
247+
/// A liquidity handler allowing to request channels via the [bLIP-51 / LSPS1] protocol.
248+
///
249+
/// Should be retrieved by calling [`Node::lsps1_liquidity`].
250+
///
251+
/// To open [bLIP-52 / LSPS2] JIT channels, please refer to
252+
/// [`Bolt11Payment::receive_via_jit_channel`].
253+
///
254+
/// [bLIP-51 / LSPS1]: https://github.com/lightning/blips/blob/master/blip-0051.md
255+
/// [bLIP-52 / LSPS2]: https://github.com/lightning/blips/blob/master/blip-0052.md
256+
/// [`Node::lsps1_liquidity`]: crate::Node::lsps1_liquidity
257+
/// [`Bolt11Payment::receive_via_jit_channel`]: crate::payment::Bolt11Payment::receive_via_jit_channel
258+
#[derive(Clone)]
259+
#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
260+
pub struct LSPS1Liquidity {
261+
runtime: Arc<Runtime>,
262+
wallet: Arc<Wallet>,
263+
connection_manager: Arc<ConnectionManager<Arc<Logger>>>,
264+
liquidity_source: Option<Arc<LiquiditySource<Arc<Logger>>>>,
265+
logger: Arc<Logger>,
266+
}
267+
268+
impl LSPS1Liquidity {
269+
pub(crate) fn new(
270+
runtime: Arc<Runtime>, wallet: Arc<Wallet>,
271+
connection_manager: Arc<ConnectionManager<Arc<Logger>>>,
272+
liquidity_source: Option<Arc<LiquiditySource<Arc<Logger>>>>, logger: Arc<Logger>,
273+
) -> Self {
274+
Self { runtime, wallet, connection_manager, liquidity_source, logger }
275+
}
276+
}
277+
278+
#[cfg_attr(feature = "uniffi", uniffi::export)]
279+
impl LSPS1Liquidity {
280+
/// Connects to the configured LSP and places an order for an inbound channel.
281+
///
282+
/// The channel will be opened after one of the returned payment options has successfully been
283+
/// paid.
284+
pub fn request_channel(
285+
&self, lsp_balance_sat: u64, client_balance_sat: u64, channel_expiry_blocks: u32,
286+
announce_channel: bool,
287+
) -> Result<LSPS1OrderStatus, Error> {
288+
let liquidity_source =
289+
self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;
290+
291+
let (lsp_node_id, lsp_address) =
292+
liquidity_source.get_lsps1_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?;
293+
294+
let con_node_id = lsp_node_id;
295+
let con_addr = lsp_address.clone();
296+
let con_cm = Arc::clone(&self.connection_manager);
297+
298+
// We need to use our main runtime here as a local runtime might not be around to poll
299+
// connection futures going forward.
300+
self.runtime.block_on(async move {
301+
con_cm.connect_peer_if_necessary(con_node_id, con_addr).await
302+
})?;
303+
304+
log_info!(self.logger, "Connected to LSP {}@{}. ", lsp_node_id, lsp_address);
305+
306+
let refund_address = self.wallet.get_new_address()?;
307+
308+
let liquidity_source = Arc::clone(&liquidity_source);
309+
let response = self.runtime.block_on(async move {
310+
liquidity_source
311+
.lsps1_request_channel(
312+
lsp_balance_sat,
313+
client_balance_sat,
314+
channel_expiry_blocks,
315+
announce_channel,
316+
refund_address,
317+
)
318+
.await
319+
})?;
320+
321+
Ok(response)
322+
}
323+
324+
/// Connects to the configured LSP and checks for the status of a previously-placed order.
325+
pub fn check_order_status(&self, order_id: LSPS1OrderId) -> Result<LSPS1OrderStatus, Error> {
326+
let liquidity_source =
327+
self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;
328+
329+
let (lsp_node_id, lsp_address) =
330+
liquidity_source.get_lsps1_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?;
331+
332+
let con_node_id = lsp_node_id;
333+
let con_addr = lsp_address.clone();
334+
let con_cm = Arc::clone(&self.connection_manager);
335+
336+
// We need to use our main runtime here as a local runtime might not be around to poll
337+
// connection futures going forward.
338+
self.runtime.block_on(async move {
339+
con_cm.connect_peer_if_necessary(con_node_id, con_addr).await
340+
})?;
341+
342+
let liquidity_source = Arc::clone(&liquidity_source);
343+
let response = self
344+
.runtime
345+
.block_on(async move { liquidity_source.lsps1_check_order_status(order_id).await })?;
346+
Ok(response)
347+
}
348+
}

src/liquidity/client/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
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+
pub(crate) mod lsps1;

0 commit comments

Comments
 (0)