diff --git a/lightning-liquidity/src/lsps5/client.rs b/lightning-liquidity/src/lsps5/client.rs index 50b2c85ce1d..5bfbd5b91dd 100644 --- a/lightning-liquidity/src/lsps5/client.rs +++ b/lightning-liquidity/src/lsps5/client.rs @@ -11,7 +11,7 @@ use crate::alloc::string::ToString; use crate::events::EventQueue; -use crate::lsps0::ser::{LSPSDateTime, LSPSMessage, LSPSProtocolMessageHandler, LSPSRequestId}; +use crate::lsps0::ser::{LSPSMessage, LSPSProtocolMessageHandler, LSPSRequestId}; use crate::lsps5::event::LSPS5ClientEvent; use crate::lsps5::msgs::{ LSPS5Message, LSPS5Request, LSPS5Response, ListWebhooksRequest, RemoveWebhookRequest, @@ -22,7 +22,6 @@ use crate::message_queue::MessageQueue; use crate::prelude::{new_hash_map, HashMap}; use crate::sync::{Arc, Mutex, RwLock}; use crate::utils::generate_request_id; -use crate::utils::time::TimeProvider; use super::msgs::{LSPS5AppName, LSPS5Error, LSPS5WebhookUrl}; @@ -32,78 +31,75 @@ use lightning::ln::msgs::{ErrorAction, LightningError}; use lightning::sign::EntropySource; use lightning::util::logger::Level; +use alloc::collections::VecDeque; use alloc::string::String; use core::ops::Deref; -use core::time::Duration; -/// Default maximum age in seconds for cached responses (1 hour). -pub const DEFAULT_RESPONSE_MAX_AGE_SECS: u64 = 3600; - -#[derive(Debug, Clone)] -/// Configuration for the LSPS5 client -pub struct LSPS5ClientConfig { - /// Maximum age in seconds for cached responses (default: [`DEFAULT_RESPONSE_MAX_AGE_SECS`]). - pub response_max_age_secs: Duration, +impl PartialEq for (LSPSRequestId, (LSPS5AppName, LSPS5WebhookUrl)) { + fn eq(&self, other: &LSPSRequestId) -> bool { + &self.0 == other + } } -impl Default for LSPS5ClientConfig { - fn default() -> Self { - Self { response_max_age_secs: Duration::from_secs(DEFAULT_RESPONSE_MAX_AGE_SECS) } +impl PartialEq for (LSPSRequestId, LSPS5AppName) { + fn eq(&self, other: &LSPSRequestId) -> bool { + &self.0 == other } } -struct PeerState -where - TP::Target: TimeProvider, -{ - pending_set_webhook_requests: - HashMap, - pending_list_webhooks_requests: HashMap, - pending_remove_webhook_requests: HashMap, - last_cleanup: Option, - max_age_secs: Duration, - time_provider: TP, +#[derive(Debug, Clone, Copy, Default)] +/// Configuration for the LSPS5 client +pub struct LSPS5ClientConfig {} + +struct PeerState { + pending_set_webhook_requests: VecDeque<(LSPSRequestId, (LSPS5AppName, LSPS5WebhookUrl))>, + pending_list_webhooks_requests: VecDeque, + pending_remove_webhook_requests: VecDeque<(LSPSRequestId, LSPS5AppName)>, } -impl PeerState -where - TP::Target: TimeProvider, -{ - fn new(max_age_secs: Duration, time_provider: TP) -> Self { +const MAX_PENDING_REQUESTS: usize = 5; + +impl PeerState { + fn new() -> Self { Self { - pending_set_webhook_requests: new_hash_map(), - pending_list_webhooks_requests: new_hash_map(), - pending_remove_webhook_requests: new_hash_map(), - last_cleanup: None, - max_age_secs, - time_provider, + pending_set_webhook_requests: VecDeque::with_capacity(MAX_PENDING_REQUESTS), + pending_list_webhooks_requests: VecDeque::with_capacity(MAX_PENDING_REQUESTS), + pending_remove_webhook_requests: VecDeque::with_capacity(MAX_PENDING_REQUESTS), } } - fn cleanup_expired_responses(&mut self) { - let now = - LSPSDateTime::new_from_duration_since_epoch(self.time_provider.duration_since_epoch()); - // Only run cleanup once per minute to avoid excessive processing - const CLEANUP_INTERVAL: Duration = Duration::from_secs(60); - if let Some(last_cleanup) = &self.last_cleanup { - let time_since_last_cleanup = Duration::from_secs(now.abs_diff(&last_cleanup)); - if time_since_last_cleanup < CLEANUP_INTERVAL { - return; - } + fn add_request(&mut self, item: T, queue_selector: F) + where + F: FnOnce(&mut Self) -> &mut VecDeque, + { + let queue = queue_selector(self); + if queue.len() == MAX_PENDING_REQUESTS { + queue.pop_front(); } + queue.push_back(item); + } - self.last_cleanup = Some(now.clone()); + fn find_and_remove_request( + &mut self, queue_selector: F, request_id: &LSPSRequestId, + ) -> Option + where + F: FnOnce(&mut Self) -> &mut VecDeque, + T: Clone, + for<'a> &'a T: PartialEq<&'a LSPSRequestId>, + { + let queue = queue_selector(self); + if let Some(pos) = queue.iter().position(|item| item == request_id) { + queue.remove(pos) + } else { + None + } + } - self.pending_set_webhook_requests.retain(|_, (_, _, timestamp)| { - Duration::from_secs(timestamp.abs_diff(&now)) < self.max_age_secs - }); - self.pending_list_webhooks_requests.retain(|_, timestamp| { - Duration::from_secs(timestamp.abs_diff(&now)) < self.max_age_secs - }); - self.pending_remove_webhook_requests.retain(|_, (_, timestamp)| { - Duration::from_secs(timestamp.abs_diff(&now)) < self.max_age_secs - }); + fn is_empty(&self) -> bool { + self.pending_set_webhook_requests.is_empty() + && self.pending_list_webhooks_requests.is_empty() + && self.pending_remove_webhook_requests.is_empty() } } @@ -128,51 +124,44 @@ where /// [`lsps5.list_webhooks`]: super::msgs::LSPS5Request::ListWebhooks /// [`lsps5.remove_webhook`]: super::msgs::LSPS5Request::RemoveWebhook /// [`LSPS5Validator`]: super::validator::LSPS5Validator -pub struct LSPS5ClientHandler +pub struct LSPS5ClientHandler where ES::Target: EntropySource, - TP::Target: TimeProvider, { pending_messages: Arc, pending_events: Arc, entropy_source: ES, - per_peer_state: RwLock>>>, - config: LSPS5ClientConfig, - time_provider: TP, + per_peer_state: RwLock>>, + _config: LSPS5ClientConfig, } -impl LSPS5ClientHandler +impl LSPS5ClientHandler where ES::Target: EntropySource, - TP::Target: TimeProvider, { /// Constructs an `LSPS5ClientHandler`. - pub(crate) fn new_with_time_provider( + pub(crate) fn new( entropy_source: ES, pending_messages: Arc, pending_events: Arc, - config: LSPS5ClientConfig, time_provider: TP, + _config: LSPS5ClientConfig, ) -> Self { Self { pending_messages, pending_events, entropy_source, per_peer_state: RwLock::new(new_hash_map()), - config, - time_provider, + _config, } } fn with_peer_state(&self, counterparty_node_id: PublicKey, f: F) -> R where - F: FnOnce(&mut PeerState) -> R, + F: FnOnce(&mut PeerState) -> R, { let mut outer_state_lock = self.per_peer_state.write().unwrap(); - let inner_state_lock = outer_state_lock.entry(counterparty_node_id).or_insert(Mutex::new( - PeerState::new(self.config.response_max_age_secs, self.time_provider.clone()), - )); + let inner_state_lock = + outer_state_lock.entry(counterparty_node_id).or_insert(Mutex::new(PeerState::new())); let mut peer_state_lock = inner_state_lock.lock().unwrap(); - peer_state_lock.cleanup_expired_responses(); - f(&mut *peer_state_lock) } @@ -212,15 +201,9 @@ where let request_id = generate_request_id(&self.entropy_source); self.with_peer_state(counterparty_node_id, |peer_state| { - peer_state.pending_set_webhook_requests.insert( - request_id.clone(), - ( - app_name.clone(), - lsps_webhook_url.clone(), - LSPSDateTime::new_from_duration_since_epoch( - self.time_provider.duration_since_epoch(), - ), - ), + peer_state.add_request( + (request_id.clone(), (app_name.clone(), lsps_webhook_url.clone())), + |s| &mut s.pending_set_webhook_requests, ); }); @@ -251,11 +234,9 @@ where /// [`LSPS5Response::ListWebhooks`]: super::msgs::LSPS5Response::ListWebhooks pub fn list_webhooks(&self, counterparty_node_id: PublicKey) -> LSPSRequestId { let request_id = generate_request_id(&self.entropy_source); - let now = - LSPSDateTime::new_from_duration_since_epoch(self.time_provider.duration_since_epoch()); self.with_peer_state(counterparty_node_id, |peer_state| { - peer_state.pending_list_webhooks_requests.insert(request_id.clone(), now); + peer_state.add_request(request_id.clone(), |s| &mut s.pending_list_webhooks_requests); }); let request = LSPS5Request::ListWebhooks(ListWebhooksRequest {}); @@ -290,13 +271,11 @@ where let app_name = LSPS5AppName::from_string(app_name)?; let request_id = generate_request_id(&self.entropy_source); - let now = - LSPSDateTime::new_from_duration_since_epoch(self.time_provider.duration_since_epoch()); self.with_peer_state(counterparty_node_id, |peer_state| { - peer_state - .pending_remove_webhook_requests - .insert(request_id.clone(), (app_name.clone(), now)); + peer_state.add_request((request_id.clone(), app_name.clone()), |s| { + &mut s.pending_remove_webhook_requests + }); }); let request = LSPS5Request::RemoveWebhook(RemoveWebhookRequest { app_name }); @@ -326,9 +305,9 @@ where action: ErrorAction::IgnoreAndLog(Level::Debug), }); let event_queue_notifier = self.pending_events.notifier(); - let handle_response = |peer_state: &mut PeerState| { - if let Some((app_name, webhook_url, _)) = - peer_state.pending_set_webhook_requests.remove(&request_id) + let handle_response = |peer_state: &mut PeerState| { + if let Some((_, (app_name, webhook_url))) = peer_state + .find_and_remove_request(|s| &mut s.pending_set_webhook_requests, &request_id) { match &response { LSPS5Response::SetWebhook(r) => { @@ -360,7 +339,9 @@ where }); }, } - } else if peer_state.pending_list_webhooks_requests.remove(&request_id).is_some() { + } else if let Some(_) = peer_state + .find_and_remove_request(|s| &mut s.pending_list_webhooks_requests, &request_id) + { match &response { LSPS5Response::ListWebhooks(r) => { event_queue_notifier.enqueue(LSPS5ClientEvent::WebhooksListed { @@ -378,8 +359,8 @@ where }); }, } - } else if let Some((app_name, _)) = - peer_state.pending_remove_webhook_requests.remove(&request_id) + } else if let Some((_, app_name)) = peer_state + .find_and_remove_request(|s| &mut s.pending_remove_webhook_requests, &request_id) { match &response { LSPS5Response::RemoveWebhook(_) => { @@ -414,14 +395,31 @@ where } }; self.with_peer_state(*counterparty_node_id, handle_response); + + self.check_and_remove_empty_peer_state(counterparty_node_id); + result } + + fn check_and_remove_empty_peer_state(&self, counterparty_node_id: &PublicKey) { + let mut outer_state_lock = self.per_peer_state.write().unwrap(); + let should_remove = + if let Some(peer_state_mutex) = outer_state_lock.get(counterparty_node_id) { + let peer_state = peer_state_mutex.lock().unwrap(); + peer_state.is_empty() + } else { + false + }; + + if should_remove { + outer_state_lock.remove(counterparty_node_id); + } + } } -impl LSPSProtocolMessageHandler for LSPS5ClientHandler +impl LSPSProtocolMessageHandler for LSPS5ClientHandler where ES::Target: EntropySource, - TP::Target: TimeProvider, { type ProtocolMessage = LSPS5Message; const PROTOCOL_NUMBER: Option = Some(5); @@ -435,31 +433,40 @@ where #[cfg(all(test, feature = "time"))] mod tests { - use core::time::Duration; use super::*; - use crate::{ - lsps0::ser::LSPSRequestId, lsps5::msgs::SetWebhookResponse, tests::utils::TestEntropy, - utils::time::DefaultTimeProvider, - }; + use crate::{lsps0::ser::LSPSRequestId, lsps5::msgs::SetWebhookResponse}; use bitcoin::{key::Secp256k1, secp256k1::SecretKey}; + use core::sync::atomic::{AtomicU64, Ordering}; + + struct UniqueTestEntropy { + counter: AtomicU64, + } + + impl EntropySource for UniqueTestEntropy { + fn get_secure_random_bytes(&self) -> [u8; 32] { + let counter = self.counter.fetch_add(1, Ordering::SeqCst); + let mut bytes = [0u8; 32]; + bytes[0..8].copy_from_slice(&counter.to_be_bytes()); + bytes + } + } fn setup_test_client() -> ( - LSPS5ClientHandler, Arc>, + LSPS5ClientHandler>, Arc, Arc, PublicKey, PublicKey, ) { - let test_entropy_source = Arc::new(TestEntropy {}); + let test_entropy_source = Arc::new(UniqueTestEntropy { counter: AtomicU64::new(2) }); let message_queue = Arc::new(MessageQueue::new()); let event_queue = Arc::new(EventQueue::new()); - let client = LSPS5ClientHandler::new_with_time_provider( + let client = LSPS5ClientHandler::new( test_entropy_source, Arc::clone(&message_queue), Arc::clone(&event_queue), LSPS5ClientConfig::default(), - Arc::new(DefaultTimeProvider), ); let secp = Secp256k1::new(); @@ -486,10 +493,16 @@ mod tests { let outer_state_lock = client.per_peer_state.read().unwrap(); let peer_1_state = outer_state_lock.get(&peer_1).unwrap().lock().unwrap(); - assert!(peer_1_state.pending_set_webhook_requests.contains_key(&req_id_1)); + assert!(peer_1_state + .pending_set_webhook_requests + .iter() + .any(|(id, _)| id == &req_id_1)); let peer_2_state = outer_state_lock.get(&peer_2).unwrap().lock().unwrap(); - assert!(peer_2_state.pending_set_webhook_requests.contains_key(&req_id_2)); + assert!(peer_2_state + .pending_set_webhook_requests + .iter() + .any(|(id, _)| id == &req_id_2)); } } @@ -508,133 +521,162 @@ mod tests { { let outer_state_lock = client.per_peer_state.read().unwrap(); let peer_state = outer_state_lock.get(&peer).unwrap().lock().unwrap(); - assert_eq!( - peer_state.pending_set_webhook_requests.get(&set_req_id).unwrap(), - &( - lsps5_app_name.clone(), - lsps5_webhook_url, - peer_state.pending_set_webhook_requests.get(&set_req_id).unwrap().2.clone() - ) - ); + let set_request = peer_state + .pending_set_webhook_requests + .iter() + .find(|(id, _)| id == &set_req_id) + .unwrap(); + assert_eq!(&set_request.1, &(lsps5_app_name.clone(), lsps5_webhook_url)); - assert!(peer_state.pending_list_webhooks_requests.contains_key(&list_req_id)); + assert!(peer_state.pending_list_webhooks_requests.contains(&list_req_id)); - assert_eq!( - peer_state.pending_remove_webhook_requests.get(&remove_req_id).unwrap().0, - lsps5_app_name - ); + let remove_request = peer_state + .pending_remove_webhook_requests + .iter() + .find(|(id, _)| id == &remove_req_id) + .unwrap(); + assert_eq!(&remove_request.1, &lsps5_app_name); } } #[test] - fn test_handle_response_clears_pending_state() { - let (client, _, _, peer, _) = setup_test_client(); + fn test_unknown_request_id_handling() { + let (client, _message_queue, _, peer, _) = setup_test_client(); - let req_id = client + let _valid_req = client .set_webhook(peer, "test-app".to_string(), "https://example.com/hook".to_string()) .unwrap(); + let unknown_req_id = LSPSRequestId("unknown:request:id".to_string()); let response = LSPS5Response::SetWebhook(SetWebhookResponse { num_webhooks: 1, max_webhooks: 5, no_change: false, }); - let response_msg = LSPS5Message::Response(req_id.clone(), response); + let response_msg = LSPS5Message::Response(unknown_req_id, response); + + let result = client.handle_message(response_msg, &peer); + assert!(result.is_err()); + let error = result.unwrap_err(); + assert!(error.err.to_lowercase().contains("unknown request id")); + } + + #[test] + fn test_pending_request_eviction() { + let (client, _, _, peer, _) = setup_test_client(); + + let mut request_ids = Vec::new(); + for i in 0..MAX_PENDING_REQUESTS { + let req_id = client + .set_webhook(peer, format!("app-{}", i), format!("https://example.com/hook{}", i)) + .unwrap(); + request_ids.push(req_id); + } { let outer_state_lock = client.per_peer_state.read().unwrap(); let peer_state = outer_state_lock.get(&peer).unwrap().lock().unwrap(); - assert!(peer_state.pending_set_webhook_requests.contains_key(&req_id)); + for req_id in &request_ids { + assert!(peer_state.pending_set_webhook_requests.iter().any(|(id, _)| id == req_id)); + } + assert_eq!(peer_state.pending_set_webhook_requests.len(), MAX_PENDING_REQUESTS); } - client.handle_message(response_msg, &peer).unwrap(); + let new_req_id = client + .set_webhook(peer, "app-new".to_string(), "https://example.com/hook-new".to_string()) + .unwrap(); { let outer_state_lock = client.per_peer_state.read().unwrap(); let peer_state = outer_state_lock.get(&peer).unwrap().lock().unwrap(); - assert!(!peer_state.pending_set_webhook_requests.contains_key(&req_id)); - } - } - - #[test] - fn test_cleanup_expired_responses() { - let (client, _, _, _, _) = setup_test_client(); - let time_provider = &client.time_provider; - const OLD_APP_NAME: &str = "test-app-old"; - const NEW_APP_NAME: &str = "test-app-new"; - const WEBHOOK_URL: &str = "https://example.com/hook"; - let lsps5_old_app_name = LSPS5AppName::from_string(OLD_APP_NAME.to_string()).unwrap(); - let lsps5_new_app_name = LSPS5AppName::from_string(NEW_APP_NAME.to_string()).unwrap(); - let lsps5_webhook_url = LSPS5WebhookUrl::from_string(WEBHOOK_URL.to_string()).unwrap(); - let now = time_provider.duration_since_epoch(); - let mut peer_state = PeerState::>::new( - Duration::from_secs(1800), - Arc::clone(time_provider), - ); - peer_state.last_cleanup = Some(LSPSDateTime::new_from_duration_since_epoch( - now.checked_sub(Duration::from_secs(120)).unwrap(), - )); - - let old_request_id = LSPSRequestId("test:request:old".to_string()); - let new_request_id = LSPSRequestId("test:request:new".to_string()); - - // Add an old request (should be removed during cleanup) - peer_state.pending_set_webhook_requests.insert( - old_request_id.clone(), - ( - lsps5_old_app_name, - lsps5_webhook_url.clone(), - LSPSDateTime::new_from_duration_since_epoch( - now.checked_sub(Duration::from_secs(7200)).unwrap(), - ), - ), // 2 hours old - ); + assert_eq!(peer_state.pending_set_webhook_requests.len(), MAX_PENDING_REQUESTS); - // Add a recent request (should be kept) - peer_state.pending_set_webhook_requests.insert( - new_request_id.clone(), - ( - lsps5_new_app_name, - lsps5_webhook_url, - LSPSDateTime::new_from_duration_since_epoch( - now.checked_sub(Duration::from_secs(600)).unwrap(), - ), - ), // 10 minutes old - ); - - peer_state.cleanup_expired_responses(); + assert!(!peer_state + .pending_set_webhook_requests + .iter() + .any(|(id, _)| id == &request_ids[0])); - assert!(!peer_state.pending_set_webhook_requests.contains_key(&old_request_id)); - assert!(peer_state.pending_set_webhook_requests.contains_key(&new_request_id)); + for req_id in &request_ids[1..] { + assert!(peer_state.pending_set_webhook_requests.iter().any(|(id, _)| id == req_id)); + } - let cleanup_age = if let Some(last_cleanup) = peer_state.last_cleanup { - LSPSDateTime::new_from_duration_since_epoch(time_provider.duration_since_epoch()) - .abs_diff(&last_cleanup) - } else { - 0 - }; - assert!(cleanup_age < 10); + assert!(peer_state + .pending_set_webhook_requests + .iter() + .any(|(id, _)| id == &new_req_id)); + } } #[test] - fn test_unknown_request_id_handling() { - let (client, _message_queue, _, peer, _) = setup_test_client(); + fn test_peer_state_cleanup_and_recreation() { + let (client, _, _, peer, _) = setup_test_client(); - let _valid_req = client + let set_webhook_req_id = client .set_webhook(peer, "test-app".to_string(), "https://example.com/hook".to_string()) .unwrap(); - let unknown_req_id = LSPSRequestId("unknown:request:id".to_string()); - let response = LSPS5Response::SetWebhook(SetWebhookResponse { + let list_webhooks_req_id = client.list_webhooks(peer); + + { + let state = client.per_peer_state.read().unwrap(); + assert!(state.contains_key(&peer)); + let peer_state = state.get(&peer).unwrap().lock().unwrap(); + assert!(peer_state + .pending_set_webhook_requests + .iter() + .any(|(id, _)| id == &set_webhook_req_id)); + assert!(peer_state.pending_list_webhooks_requests.contains(&list_webhooks_req_id)); + } + + let set_webhook_response = LSPS5Response::SetWebhook(SetWebhookResponse { num_webhooks: 1, max_webhooks: 5, no_change: false, }); - let response_msg = LSPS5Message::Response(unknown_req_id, response); + let response_msg = LSPS5Message::Response(set_webhook_req_id.clone(), set_webhook_response); + // trigger cleanup but there is still a pending request + // so the peer state should not be removed + client.handle_message(response_msg, &peer).unwrap(); - let result = client.handle_message(response_msg, &peer); - assert!(result.is_err()); - let error = result.unwrap_err(); - assert!(error.err.to_lowercase().contains("unknown request id")); + { + let state = client.per_peer_state.read().unwrap(); + assert!(state.contains_key(&peer)); + let peer_state = state.get(&peer).unwrap().lock().unwrap(); + assert!(!peer_state + .pending_set_webhook_requests + .iter() + .any(|(id, _)| id == &set_webhook_req_id)); + assert!(peer_state.pending_list_webhooks_requests.contains(&list_webhooks_req_id)); + } + + let list_webhooks_response = + LSPS5Response::ListWebhooks(crate::lsps5::msgs::ListWebhooksResponse { + app_names: vec![], + max_webhooks: 5, + }); + let response_msg = LSPS5Message::Response(list_webhooks_req_id, list_webhooks_response); + + // now the pending request is handled, so the peer state should be removed + client.handle_message(response_msg, &peer).unwrap(); + + { + let state = client.per_peer_state.read().unwrap(); + assert!(!state.contains_key(&peer)); + } + + // check that it's possible to recreate the peer state by sending a new request + let new_req_id = client + .set_webhook(peer, "test-app-2".to_string(), "https://example.com/hook2".to_string()) + .unwrap(); + + { + let state = client.per_peer_state.read().unwrap(); + assert!(state.contains_key(&peer)); + let peer_state = state.get(&peer).unwrap().lock().unwrap(); + assert!(peer_state + .pending_set_webhook_requests + .iter() + .any(|(id, _)| id == &new_req_id)); + } } } diff --git a/lightning-liquidity/src/manager.rs b/lightning-liquidity/src/manager.rs index 6d412bd966a..7321be7dcba 100644 --- a/lightning-liquidity/src/manager.rs +++ b/lightning-liquidity/src/manager.rs @@ -181,7 +181,7 @@ pub struct LiquidityManager< lsps2_service_handler: Option>, lsps2_client_handler: Option>, lsps5_service_handler: Option>, - lsps5_client_handler: Option>, + lsps5_client_handler: Option>, service_config: Option, _client_config: Option, best_block: RwLock>, @@ -276,12 +276,11 @@ where let lsps5_client_handler = client_config.as_ref().and_then(|config| { config.lsps5_client_config.as_ref().map(|config| { - LSPS5ClientHandler::new_with_time_provider( + LSPS5ClientHandler::new( entropy_source.clone(), Arc::clone(&pending_messages), Arc::clone(&pending_events), config.clone(), - time_provider.clone(), ) }) }); @@ -411,7 +410,7 @@ where /// Returns a reference to the LSPS5 client-side handler. /// /// The returned hendler allows to initiate the LSPS5 client-side flow. That is, it allows to - pub fn lsps5_client_handler(&self) -> Option<&LSPS5ClientHandler> { + pub fn lsps5_client_handler(&self) -> Option<&LSPS5ClientHandler> { self.lsps5_client_handler.as_ref() } diff --git a/lightning-liquidity/src/utils/mod.rs b/lightning-liquidity/src/utils/mod.rs index 21cd59d2b6d..637c62c0d81 100644 --- a/lightning-liquidity/src/utils/mod.rs +++ b/lightning-liquidity/src/utils/mod.rs @@ -7,6 +7,8 @@ use lightning::sign::EntropySource; use crate::lsps0::ser::LSPSRequestId; +pub mod time; + /// Converts a human-readable string representation of a short channel ID (SCID) pub fn scid_from_human_readable_string(human_readable_scid: &str) -> Result { let mut parts = human_readable_scid.split('x'); @@ -56,5 +58,3 @@ mod tests { assert_eq!(vout_from_scid(scid), vout); } } - -pub mod time;