lightning_liquidity/lsps2/
client.rs

1// This file is Copyright its original authors, visible in version control
2// history.
3//
4// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
5// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. You may not use this file except in accordance with one or both of these
7// licenses.
8
9//! Contains the main LSPS2 client object, [`LSPS2ClientHandler`].
10
11use crate::events::{Event, EventQueue};
12use crate::lsps0::ser::{ProtocolMessageHandler, RequestId, ResponseError};
13use crate::lsps2::event::LSPS2ClientEvent;
14use crate::message_queue::MessageQueue;
15use crate::prelude::{new_hash_map, new_hash_set, HashMap, HashSet, String, ToString};
16use crate::sync::{Arc, Mutex, RwLock};
17
18use lightning::ln::msgs::{ErrorAction, LightningError};
19use lightning::sign::EntropySource;
20use lightning::util::errors::APIError;
21use lightning::util::logger::Level;
22
23use bitcoin::secp256k1::PublicKey;
24
25use core::default::Default;
26use core::ops::Deref;
27
28use crate::lsps2::msgs::{
29	BuyRequest, BuyResponse, GetInfoRequest, GetInfoResponse, LSPS2Message, LSPS2Request,
30	LSPS2Response, OpeningFeeParams,
31};
32
33/// Client-side configuration options for JIT channels.
34#[derive(Clone, Debug, Copy, Default)]
35pub struct LSPS2ClientConfig {}
36
37struct InboundJITChannel {
38	payment_size_msat: Option<u64>,
39}
40
41impl InboundJITChannel {
42	fn new(payment_size_msat: Option<u64>) -> Self {
43		Self { payment_size_msat }
44	}
45}
46
47struct PeerState {
48	pending_get_info_requests: HashSet<RequestId>,
49	pending_buy_requests: HashMap<RequestId, InboundJITChannel>,
50}
51
52impl PeerState {
53	fn new() -> Self {
54		let pending_get_info_requests = new_hash_set();
55		let pending_buy_requests = new_hash_map();
56		Self { pending_get_info_requests, pending_buy_requests }
57	}
58}
59
60/// The main object allowing to send and receive LSPS2 messages.
61///
62/// Note that currently only the 'client-trusts-LSP' trust model is supported, i.e., we don't
63/// provide any additional API guidance to allow withholding the preimage until the channel is
64/// opened. Please refer to the [`LSPS2 specification`] for more information.
65///
66/// [`LSPS2 specification`]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS2#trust-models
67pub struct LSPS2ClientHandler<ES: Deref>
68where
69	ES::Target: EntropySource,
70{
71	entropy_source: ES,
72	pending_messages: Arc<MessageQueue>,
73	pending_events: Arc<EventQueue>,
74	per_peer_state: RwLock<HashMap<PublicKey, Mutex<PeerState>>>,
75	_config: LSPS2ClientConfig,
76}
77
78impl<ES: Deref> LSPS2ClientHandler<ES>
79where
80	ES::Target: EntropySource,
81{
82	/// Constructs an `LSPS2ClientHandler`.
83	pub(crate) fn new(
84		entropy_source: ES, pending_messages: Arc<MessageQueue>, pending_events: Arc<EventQueue>,
85		_config: LSPS2ClientConfig,
86	) -> Self {
87		Self {
88			entropy_source,
89			pending_messages,
90			pending_events,
91			per_peer_state: RwLock::new(new_hash_map()),
92			_config,
93		}
94	}
95
96	/// Request the channel opening parameters from the LSP.
97	///
98	/// This initiates the JIT-channel flow that, at the end of it, will have the LSP
99	/// open a channel with sufficient inbound liquidity to be able to receive the payment.
100	///
101	/// The user will receive the LSP's response via an [`OpeningParametersReady`] event.
102	///
103	/// `counterparty_node_id` is the `node_id` of the LSP you would like to use.
104	///
105	/// `token` is an optional `String` that will be provided to the LSP.
106	/// It can be used by the LSP as an API key, coupon code, or some other way to identify a user.
107	///
108	/// Returns the used [`RequestId`], which will be returned via [`OpeningParametersReady`].
109	///
110	/// [`OpeningParametersReady`]: crate::lsps2::event::LSPS2ClientEvent::OpeningParametersReady
111	pub fn request_opening_params(
112		&self, counterparty_node_id: PublicKey, token: Option<String>,
113	) -> RequestId {
114		let request_id = crate::utils::generate_request_id(&self.entropy_source);
115
116		{
117			let mut outer_state_lock = self.per_peer_state.write().unwrap();
118			let inner_state_lock = outer_state_lock
119				.entry(counterparty_node_id)
120				.or_insert(Mutex::new(PeerState::new()));
121			let mut peer_state_lock = inner_state_lock.lock().unwrap();
122			peer_state_lock.pending_get_info_requests.insert(request_id.clone());
123		}
124
125		let request = LSPS2Request::GetInfo(GetInfoRequest { token });
126		let msg = LSPS2Message::Request(request_id.clone(), request).into();
127		self.pending_messages.enqueue(&counterparty_node_id, msg);
128
129		request_id
130	}
131
132	/// Confirms a set of chosen channel opening parameters to use for the JIT channel and
133	/// requests the necessary invoice generation parameters from the LSP.
134	///
135	/// Should be called in response to receiving a [`OpeningParametersReady`] event.
136	///
137	/// The user will receive the LSP's response via an [`InvoiceParametersReady`] event.
138	///
139	/// If `payment_size_msat` is [`Option::Some`] then the invoice will be for a fixed amount
140	/// and MPP can be used to pay it.
141	///
142	/// If `payment_size_msat` is [`Option::None`] then the invoice can be for an arbitrary amount
143	/// but MPP can no longer be used to pay it.
144	///
145	/// The client agrees to paying an opening fee equal to
146	/// `max(min_fee_msat, proportional*(payment_size_msat/1_000_000))`.
147	///
148	/// [`OpeningParametersReady`]: crate::lsps2::event::LSPS2ClientEvent::OpeningParametersReady
149	/// [`InvoiceParametersReady`]: crate::lsps2::event::LSPS2ClientEvent::InvoiceParametersReady
150	pub fn select_opening_params(
151		&self, counterparty_node_id: PublicKey, payment_size_msat: Option<u64>,
152		opening_fee_params: OpeningFeeParams,
153	) -> Result<RequestId, APIError> {
154		let request_id = crate::utils::generate_request_id(&self.entropy_source);
155
156		{
157			let mut outer_state_lock = self.per_peer_state.write().unwrap();
158			let inner_state_lock = outer_state_lock
159				.entry(counterparty_node_id)
160				.or_insert(Mutex::new(PeerState::new()));
161			let mut peer_state_lock = inner_state_lock.lock().unwrap();
162
163			let jit_channel = InboundJITChannel::new(payment_size_msat);
164			if peer_state_lock
165				.pending_buy_requests
166				.insert(request_id.clone(), jit_channel)
167				.is_some()
168			{
169				return Err(APIError::APIMisuseError {
170					err: "Failed due to duplicate request_id. This should never happen!"
171						.to_string(),
172				});
173			}
174		}
175
176		let request = LSPS2Request::Buy(BuyRequest { opening_fee_params, payment_size_msat });
177		let msg = LSPS2Message::Request(request_id.clone(), request).into();
178		self.pending_messages.enqueue(&counterparty_node_id, msg);
179
180		Ok(request_id)
181	}
182
183	fn handle_get_info_response(
184		&self, request_id: RequestId, counterparty_node_id: &PublicKey, result: GetInfoResponse,
185	) -> Result<(), LightningError> {
186		let outer_state_lock = self.per_peer_state.read().unwrap();
187		match outer_state_lock.get(counterparty_node_id) {
188			Some(inner_state_lock) => {
189				let mut peer_state = inner_state_lock.lock().unwrap();
190
191				if !peer_state.pending_get_info_requests.remove(&request_id) {
192					return Err(LightningError {
193						err: format!(
194							"Received get_info response for an unknown request: {:?}",
195							request_id
196						),
197						action: ErrorAction::IgnoreAndLog(Level::Info),
198					});
199				}
200
201				self.pending_events.enqueue(Event::LSPS2Client(
202					LSPS2ClientEvent::OpeningParametersReady {
203						request_id,
204						counterparty_node_id: *counterparty_node_id,
205						opening_fee_params_menu: result.opening_fee_params_menu,
206					},
207				));
208			},
209			None => {
210				return Err(LightningError {
211					err: format!(
212						"Received get_info response from unknown peer: {:?}",
213						counterparty_node_id
214					),
215					action: ErrorAction::IgnoreAndLog(Level::Info),
216				})
217			},
218		}
219
220		Ok(())
221	}
222
223	fn handle_get_info_error(
224		&self, request_id: RequestId, counterparty_node_id: &PublicKey, _error: ResponseError,
225	) -> Result<(), LightningError> {
226		let outer_state_lock = self.per_peer_state.read().unwrap();
227		match outer_state_lock.get(counterparty_node_id) {
228			Some(inner_state_lock) => {
229				let mut peer_state = inner_state_lock.lock().unwrap();
230
231				if !peer_state.pending_get_info_requests.remove(&request_id) {
232					return Err(LightningError {
233						err: format!(
234							"Received get_info error for an unknown request: {:?}",
235							request_id
236						),
237						action: ErrorAction::IgnoreAndLog(Level::Info),
238					});
239				}
240
241				Ok(())
242			},
243			None => {
244				return Err(LightningError { err: format!("Received error response for a get_info request from an unknown counterparty ({:?})",counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)});
245			},
246		}
247	}
248
249	fn handle_buy_response(
250		&self, request_id: RequestId, counterparty_node_id: &PublicKey, result: BuyResponse,
251	) -> Result<(), LightningError> {
252		let outer_state_lock = self.per_peer_state.read().unwrap();
253		match outer_state_lock.get(counterparty_node_id) {
254			Some(inner_state_lock) => {
255				let mut peer_state = inner_state_lock.lock().unwrap();
256
257				let jit_channel =
258					peer_state.pending_buy_requests.remove(&request_id).ok_or(LightningError {
259						err: format!(
260							"Received buy response for an unknown request: {:?}",
261							request_id
262						),
263						action: ErrorAction::IgnoreAndLog(Level::Info),
264					})?;
265
266				if let Ok(intercept_scid) = result.jit_channel_scid.to_scid() {
267					self.pending_events.enqueue(Event::LSPS2Client(
268						LSPS2ClientEvent::InvoiceParametersReady {
269							request_id,
270							counterparty_node_id: *counterparty_node_id,
271							intercept_scid,
272							cltv_expiry_delta: result.lsp_cltv_expiry_delta,
273							payment_size_msat: jit_channel.payment_size_msat,
274						},
275					));
276				} else {
277					return Err(LightningError {
278						err: format!(
279							"Received buy response with an invalid intercept scid {:?}",
280							result.jit_channel_scid
281						),
282						action: ErrorAction::IgnoreAndLog(Level::Info),
283					});
284				}
285			},
286			None => {
287				return Err(LightningError {
288					err: format!(
289						"Received buy response from unknown peer: {:?}",
290						counterparty_node_id
291					),
292					action: ErrorAction::IgnoreAndLog(Level::Info),
293				});
294			},
295		}
296		Ok(())
297	}
298
299	fn handle_buy_error(
300		&self, request_id: RequestId, counterparty_node_id: &PublicKey, _error: ResponseError,
301	) -> Result<(), LightningError> {
302		let outer_state_lock = self.per_peer_state.read().unwrap();
303		match outer_state_lock.get(counterparty_node_id) {
304			Some(inner_state_lock) => {
305				let mut peer_state = inner_state_lock.lock().unwrap();
306
307				peer_state.pending_buy_requests.remove(&request_id).ok_or(LightningError {
308					err: format!("Received buy error for an unknown request: {:?}", request_id),
309					action: ErrorAction::IgnoreAndLog(Level::Info),
310				})?;
311
312				Ok(())
313			},
314			None => {
315				return Err(LightningError { err: format!("Received error response for a buy request from an unknown counterparty ({:?})", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)});
316			},
317		}
318	}
319}
320
321impl<ES: Deref> ProtocolMessageHandler for LSPS2ClientHandler<ES>
322where
323	ES::Target: EntropySource,
324{
325	type ProtocolMessage = LSPS2Message;
326	const PROTOCOL_NUMBER: Option<u16> = Some(2);
327
328	fn handle_message(
329		&self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey,
330	) -> Result<(), LightningError> {
331		match message {
332			LSPS2Message::Response(request_id, response) => match response {
333				LSPS2Response::GetInfo(result) => {
334					self.handle_get_info_response(request_id, counterparty_node_id, result)
335				},
336				LSPS2Response::GetInfoError(error) => {
337					self.handle_get_info_error(request_id, counterparty_node_id, error)
338				},
339				LSPS2Response::Buy(result) => {
340					self.handle_buy_response(request_id, counterparty_node_id, result)
341				},
342				LSPS2Response::BuyError(error) => {
343					self.handle_buy_error(request_id, counterparty_node_id, error)
344				},
345			},
346			_ => {
347				debug_assert!(
348					false,
349					"Client handler received LSPS2 request message. This should never happen."
350				);
351				Err(LightningError { err: format!("Client handler received LSPS2 request message from node {:?}. This should never happen.", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)})
352			},
353		}
354	}
355}
356
357#[cfg(test)]
358mod tests {}