99//! Client implementation for LSPS5 webhook registration
1010
1111use crate :: events:: EventQueue ;
12- use crate :: lsps0:: ser:: { LSPSDateTime , LSPSMessage , LSPSProtocolMessageHandler , LSPSRequestId } ;
12+ use crate :: lsps0:: ser:: { LSPSMessage , LSPSProtocolMessageHandler , LSPSRequestId } ;
1313use crate :: lsps5:: event:: LSPS5ClientEvent ;
1414use crate :: lsps5:: msgs:: {
1515 LSPS5Message , LSPS5Request , LSPS5Response , ListWebhooksRequest , RemoveWebhookRequest ,
@@ -24,39 +24,51 @@ use lightning::util::message_signing;
2424
2525use crate :: sync:: { Arc , Mutex , RwLock } ;
2626use core:: ops:: Deref ;
27- use core:: str:: FromStr ;
2827
2928use crate :: prelude:: { new_hash_map, HashMap , String } ;
3029
3130use super :: msgs:: { Lsps5AppName , Lsps5WebhookUrl } ;
31+ use super :: service:: { DefaultTimeProvider , TimeProvider } ;
3232use super :: url_utils:: Url ;
33- use chrono :: Duration ;
33+ use core :: time :: Duration ;
3434use lightning:: sign:: EntropySource ;
3535use lightning:: util:: logger:: Level ;
3636
3737/// Default maximum age in seconds for cached responses (1 hour)
3838pub const DEFAULT_RESPONSE_MAX_AGE_SECS : u64 = 3600 ;
3939
4040/// Configuration options for LSPS5 client operations
41- #[ derive( Debug , Clone ) ]
41+ #[ derive( Clone ) ]
4242pub struct LSPS5ClientConfig {
4343 /// Maximum age in seconds for cached responses (default: 3600 - 1 hour)
4444 pub response_max_age_secs : u64 ,
45+ /// Time provider for LSPS5 service
46+ pub time_provider : Arc < dyn TimeProvider > ,
4547}
4648
4749impl Default for LSPS5ClientConfig {
4850 fn default ( ) -> Self {
49- Self { response_max_age_secs : DEFAULT_RESPONSE_MAX_AGE_SECS }
51+ Self {
52+ response_max_age_secs : DEFAULT_RESPONSE_MAX_AGE_SECS ,
53+ time_provider : Arc :: new ( DefaultTimeProvider ) ,
54+ }
55+ }
56+ }
57+
58+ impl LSPS5ClientConfig {
59+ /// Set a custom time provider for the LSPS5 service
60+ pub fn with_time_provider ( mut self , time_provider : impl TimeProvider + ' static ) -> Self {
61+ self . time_provider = Arc :: new ( time_provider) ;
62+ self
5063 }
5164}
5265
5366struct PeerState {
54- pending_set_webhook_requests :
55- HashMap < LSPSRequestId , ( Lsps5AppName , Lsps5WebhookUrl , LSPSDateTime ) > , // RequestId -> (app_name, webhook_url, timestamp)
56- pending_list_webhooks_requests : HashMap < LSPSRequestId , LSPSDateTime > , // RequestId -> timestamp
57- pending_remove_webhook_requests : HashMap < LSPSRequestId , ( Lsps5AppName , LSPSDateTime ) > , // RequestId -> (app_name, timestamp)
67+ pending_set_webhook_requests : HashMap < LSPSRequestId , ( Lsps5AppName , Lsps5WebhookUrl , Duration ) > , // RequestId -> (app_name, webhook_url, timestamp)
68+ pending_list_webhooks_requests : HashMap < LSPSRequestId , Duration > , // RequestId -> timestamp
69+ pending_remove_webhook_requests : HashMap < LSPSRequestId , ( Lsps5AppName , Duration ) > , // RequestId -> (app_name, timestamp)
5870 // Last cleanup time for garbage collection
59- last_cleanup : LSPSDateTime , // Seconds since epoch
71+ last_cleanup : Duration , // Seconds since epoch
6072}
6173
6274impl PeerState {
@@ -65,37 +77,34 @@ impl PeerState {
6577 pending_set_webhook_requests : new_hash_map ( ) ,
6678 pending_list_webhooks_requests : new_hash_map ( ) ,
6779 pending_remove_webhook_requests : new_hash_map ( ) ,
68- last_cleanup : LSPSDateTime :: now ( ) ,
80+ last_cleanup : Duration :: from_secs ( 0 ) ,
6981 }
7082 }
7183
7284 /// Clean up expired responses based on max_age
73- fn cleanup_expired_responses ( & mut self , max_age_secs : u64 ) {
74- let now = LSPSDateTime :: now ( ) ;
85+ fn cleanup_expired_responses (
86+ & mut self , max_age_secs : u64 , time_provider : Arc < dyn TimeProvider > ,
87+ ) {
88+ let now = time_provider. now ( ) ;
7589
7690 // Only run cleanup once per minute to avoid excessive processing
77- if now. duration_since ( & self . last_cleanup ) < Duration :: seconds ( 60 ) {
91+ if now. abs_diff ( self . last_cleanup ) < Duration :: from_secs ( 60 ) {
7892 return ;
7993 }
8094
8195 self . last_cleanup = now. clone ( ) ;
8296
8397 // Calculate the cutoff time for expired requests
84- let cutoff = now
85- . checked_sub_signed ( Duration :: seconds ( max_age_secs. try_into ( ) . unwrap ( ) ) )
86- . expect ( "Cutoff time must be computed" ) ;
98+ let cutoff = now. abs_diff ( Duration :: from_secs ( max_age_secs. try_into ( ) . unwrap ( ) ) ) ;
8799
88100 // Remove expired set_webhook requests
89- self . pending_set_webhook_requests
90- . retain ( |_, ( _, _, timestamp) | timestamp. timestamp ( ) > cutoff. timestamp ( ) ) ;
101+ self . pending_set_webhook_requests . retain ( |_, ( _, _, timestamp) | * timestamp > cutoff) ;
91102
92103 // Remove expired list_webhooks requests
93- self . pending_list_webhooks_requests
94- . retain ( |_, timestamp| timestamp. timestamp ( ) > cutoff. timestamp ( ) ) ;
104+ self . pending_list_webhooks_requests . retain ( |_, timestamp| * timestamp > cutoff) ;
95105
96106 // Remove expired remove_webhook requests
97- self . pending_remove_webhook_requests
98- . retain ( |_, ( _, timestamp) | timestamp. timestamp ( ) > cutoff. timestamp ( ) ) ;
107+ self . pending_remove_webhook_requests . retain ( |_, ( _, timestamp) | * timestamp > cutoff) ;
99108 }
100109}
101110
@@ -114,6 +123,8 @@ where
114123 per_peer_state : RwLock < HashMap < PublicKey , Mutex < PeerState > > > ,
115124 /// Client configuration
116125 config : LSPS5ClientConfig ,
126+ /// Time provider for LSPS5 service
127+ time_provider : Arc < dyn TimeProvider > ,
117128}
118129
119130impl < ES : Deref > LSPS5ClientHandler < ES >
@@ -126,12 +137,14 @@ where
126137 entropy_source : ES , pending_messages : Arc < MessageQueue > , pending_events : Arc < EventQueue > ,
127138 config : LSPS5ClientConfig ,
128139 ) -> Self {
140+ let time_provider = config. time_provider . clone ( ) ;
129141 Self {
130142 pending_messages,
131143 pending_events,
132144 entropy_source,
133145 per_peer_state : RwLock :: new ( new_hash_map ( ) ) ,
134146 config,
147+ time_provider,
135148 }
136149 }
137150
@@ -146,7 +159,10 @@ where
146159 let mut peer_state_lock = inner_state_lock. lock ( ) . unwrap ( ) ;
147160
148161 // Clean up expired responses using configured max age
149- peer_state_lock. cleanup_expired_responses ( self . config . response_max_age_secs ) ;
162+ peer_state_lock. cleanup_expired_responses (
163+ self . config . response_max_age_secs ,
164+ self . time_provider . clone ( ) ,
165+ ) ;
150166
151167 // Execute the provided function with the locked peer state
152168 f ( & mut * peer_state_lock)
@@ -208,7 +224,7 @@ where
208224 self . with_peer_state ( counterparty_node_id, |peer_state| {
209225 peer_state. pending_set_webhook_requests . insert (
210226 request_id. clone ( ) ,
211- ( app_name. clone ( ) , webhook. clone ( ) , LSPSDateTime :: now ( ) ) ,
227+ ( app_name. clone ( ) , webhook. clone ( ) , self . time_provider . now ( ) ) ,
212228 ) ;
213229 } ) ;
214230
@@ -242,7 +258,7 @@ where
242258 self . with_peer_state ( counterparty_node_id, |peer_state| {
243259 peer_state
244260 . pending_list_webhooks_requests
245- . insert ( request_id. clone ( ) , LSPSDateTime :: now ( ) ) ;
261+ . insert ( request_id. clone ( ) , self . time_provider . now ( ) ) ;
246262 } ) ;
247263
248264 // Create the request
@@ -284,7 +300,7 @@ where
284300 self . with_peer_state ( counterparty_node_id, |peer_state| {
285301 peer_state
286302 . pending_remove_webhook_requests
287- . insert ( request_id. clone ( ) , ( app_name. clone ( ) , LSPSDateTime :: now ( ) ) ) ;
303+ . insert ( request_id. clone ( ) , ( app_name. clone ( ) , self . time_provider . now ( ) ) ) ;
288304 } ) ;
289305
290306 let request = LSPS5Request :: RemoveWebhook ( RemoveWebhookRequest { app_name } ) ;
@@ -501,20 +517,20 @@ where
501517 /// * On error: LightningError with error description
502518 pub fn verify_notification_signature (
503519 counterparty_node_id : PublicKey , timestamp : & str , signature : & str ,
504- notification : & WebhookNotification ,
520+ notification : & WebhookNotification , time_provider : Arc < dyn TimeProvider > ,
505521 ) -> Result < bool , LightningError > {
506522 // Check timestamp format
507- match LSPSDateTime :: from_str ( timestamp) {
523+ match time_provider . from_rfc3339 ( timestamp) {
508524 Ok ( timestamp_dt) => {
509525 // Check timestamp is within 10 minutes of current time
510- let now = LSPSDateTime :: now ( ) ;
511- let diff = ( timestamp_dt . timestamp ( ) - now. timestamp ( ) ) . abs ( ) ;
512- if diff > 600 {
526+ let now = time_provider . now ( ) ;
527+ let diff = now. abs_diff ( timestamp_dt ) ;
528+ if diff > Duration :: from_secs ( 600 ) {
513529 // 10 minutes
514530 return Err ( LightningError {
515531 err : format ! (
516- "Timestamp too far from current time: {} (diff: {} seconds)" ,
517- timestamp , diff
532+ "Timestamp too far from current time: {:? } (diff: {:? } seconds)" ,
533+ now , diff
518534 ) ,
519535 action : ErrorAction :: IgnoreAndLog ( Level :: Error ) ,
520536 } ) ;
@@ -571,6 +587,7 @@ where
571587 /// * On error: LightningError with error description
572588 pub fn parse_webhook_notification (
573589 counterparty_node_id : PublicKey , timestamp : & str , signature : & str , notification_json : & str ,
590+ time_provider : Arc < dyn TimeProvider > ,
574591 ) -> Result < WebhookNotification , LightningError > {
575592 // Parse the notification JSON
576593 let notification: WebhookNotification = match serde_json:: from_str ( notification_json) {
@@ -588,6 +605,7 @@ where
588605 timestamp,
589606 signature,
590607 & notification,
608+ time_provider,
591609 ) {
592610 Ok ( _) => Ok ( notification) ,
593611 Err ( e) => Err ( e) ,
@@ -750,17 +768,20 @@ mod tests {
750768
751769 #[ test]
752770 fn test_cleanup_expired_responses ( ) {
771+ // use DefaultTimeProvider
772+ let ( client, _, _, _, _) = setup_test_client ( ) ;
773+ let time_provider = client. time_provider ;
753774 const OLD_APP_NAME : & str = "test-app-old" ;
754775 const NEW_APP_NAME : & str = "test-app-new" ;
755776 const WEBHOOK_URL : & str = "https://example.com/hook" ;
756777 let lsps5_old_app_name = Lsps5AppName :: new ( OLD_APP_NAME . to_string ( ) ) . unwrap ( ) ;
757778 let lsps5_new_app_name = Lsps5AppName :: new ( NEW_APP_NAME . to_string ( ) ) . unwrap ( ) ;
758779 let lsps5_webhook_url = Lsps5WebhookUrl :: new ( WEBHOOK_URL . to_string ( ) ) . unwrap ( ) ;
759780 // The current time for setting request timestamps
760- let now = LSPSDateTime :: now ( ) ;
781+ let now = time_provider . now ( ) ;
761782 // Create a mock PeerState with a very old cleanup time
762783 let mut peer_state = PeerState :: new ( ) ;
763- peer_state. last_cleanup = now. checked_sub_signed ( Duration :: seconds ( 120 ) ) . unwrap ( ) ;
784+ peer_state. last_cleanup = now. abs_diff ( Duration :: from_secs ( 120 ) ) ;
764785
765786 // Add some test requests with different timestamps
766787 let old_request_id = LSPSRequestId ( "test:request:old" . to_string ( ) ) ;
@@ -772,30 +793,26 @@ mod tests {
772793 (
773794 lsps5_old_app_name,
774795 lsps5_webhook_url. clone ( ) ,
775- now. checked_sub_signed ( Duration :: seconds ( 7200 ) ) . unwrap ( ) ,
796+ now. abs_diff ( Duration :: from_secs ( 7200 ) ) ,
776797 ) , // 2 hours old
777798 ) ;
778799
779800 // Add a recent request (should be kept)
780801 peer_state. pending_set_webhook_requests . insert (
781802 new_request_id. clone ( ) ,
782- (
783- lsps5_new_app_name,
784- lsps5_webhook_url,
785- now. checked_sub_signed ( Duration :: seconds ( 600 ) ) . unwrap ( ) ,
786- ) , // 10 minutes old
803+ ( lsps5_new_app_name, lsps5_webhook_url, now. abs_diff ( Duration :: from_secs ( 600 ) ) ) , // 10 minutes old
787804 ) ;
788805
789806 // Run cleanup with 30 minutes (1800 seconds) max age
790- peer_state. cleanup_expired_responses ( 1800 ) ;
807+ peer_state. cleanup_expired_responses ( 1800 , time_provider . clone ( ) ) ;
791808
792809 // Verify old request is removed and new request is kept
793810 assert ! ( !peer_state. pending_set_webhook_requests. contains_key( & old_request_id) ) ;
794811 assert ! ( peer_state. pending_set_webhook_requests. contains_key( & new_request_id) ) ;
795812
796813 // Verify last_cleanup was updated within the last 10 seconds
797- let cleanup_age = LSPSDateTime :: now ( ) . duration_since ( & peer_state. last_cleanup ) ;
798- assert ! ( cleanup_age < Duration :: seconds ( 10 ) ) ;
814+ let cleanup_age = time_provider . clone ( ) . now ( ) . abs_diff ( peer_state. last_cleanup ) ;
815+ assert ! ( cleanup_age < Duration :: from_secs ( 10 ) ) ;
799816 }
800817
801818 #[ test]
0 commit comments