@@ -51,7 +51,11 @@ struct StoredWebhook {
5151 _app_name : LSPS5AppName ,
5252 url : LSPS5WebhookUrl ,
5353 _counterparty_node_id : PublicKey ,
54+ // Timestamp used for tracking when the webhook was created / updated, or when the last notification was sent.
55+ // This is used to determine if the webhook is stale and should be pruned.
5456 last_used : LSPSDateTime ,
57+ // Map of last notification sent timestamps for each notification method.
58+ // This is used to enforce notification cooldowns.
5559 last_notification_sent : HashMap < WebhookNotificationMethod , LSPSDateTime > ,
5660}
5761
@@ -60,8 +64,6 @@ struct StoredWebhook {
6064pub struct LSPS5ServiceConfig {
6165 /// Maximum number of webhooks allowed per client.
6266 pub max_webhooks_per_client : u32 ,
63- /// Minimum time between sending the same notification type in hours (default: 24)
64- pub notification_cooldown_hours : Duration ,
6567}
6668
6769/// Default maximum number of webhooks allowed per client.
@@ -72,10 +74,7 @@ pub const DEFAULT_NOTIFICATION_COOLDOWN_HOURS: Duration = Duration::from_secs(60
7274// Default configuration for LSPS5 service.
7375impl Default for LSPS5ServiceConfig {
7476 fn default ( ) -> Self {
75- Self {
76- max_webhooks_per_client : DEFAULT_MAX_WEBHOOKS_PER_CLIENT ,
77- notification_cooldown_hours : DEFAULT_NOTIFICATION_COOLDOWN_HOURS ,
78- }
77+ Self { max_webhooks_per_client : DEFAULT_MAX_WEBHOOKS_PER_CLIENT }
7978 }
8079}
8180
@@ -93,8 +92,6 @@ impl Default for LSPS5ServiceConfig {
9392/// - `lsps5.remove_webhook` -> delete a named webhook or return [`app_name_not_found`] error.
9493/// - Prune stale webhooks after a client has no open channels and no activity for at least
9594/// [`MIN_WEBHOOK_RETENTION_DAYS`].
96- /// - Rate-limit repeat notifications of the same method to a client by
97- /// [`notification_cooldown_hours`].
9895/// - Sign and enqueue outgoing webhook notifications:
9996/// - Construct JSON-RPC 2.0 Notification objects [`WebhookNotification`],
10097/// - Timestamp and LN-style zbase32-sign each payload,
@@ -109,7 +106,6 @@ impl Default for LSPS5ServiceConfig {
109106/// [`bLIP-55 / LSPS5`]: https://github.com/lightning/blips/pull/55/files
110107/// [`max_webhooks_per_client`]: super::service::LSPS5ServiceConfig::max_webhooks_per_client
111108/// [`app_name_not_found`]: super::msgs::LSPS5ProtocolError::AppNameNotFound
112- /// [`notification_cooldown_hours`]: super::service::LSPS5ServiceConfig::notification_cooldown_hours
113109/// [`WebhookNotification`]: super::msgs::WebhookNotification
114110/// [`LSPS5ServiceEvent::SendWebhookNotification`]: super::event::LSPS5ServiceEvent::SendWebhookNotification
115111/// [`app_name`]: super::msgs::LSPS5AppName
@@ -225,23 +221,27 @@ where
225221 }
226222
227223 if !no_change {
228- self . send_webhook_registered_notification (
224+ let result = self . send_webhook_registered_notification (
229225 counterparty_node_id,
230226 params. app_name ,
231227 params. webhook ,
232- )
233- . map_err ( |e| {
228+ ) ;
229+
230+ // If the send_notification failed because of a SLOW_DOWN_ERROR, it means we sent this
231+ // notification recently, and the user has not seen it yet. It's safe to continue, but we still need to handle other error types.
232+ if result. is_err ( ) && !matches ! ( result, Err ( LSPS5ProtocolError :: SlowDownError ) ) {
233+ let e = result. unwrap_err ( ) ;
234234 let msg = LSPS5Message :: Response (
235235 request_id. clone ( ) ,
236236 LSPS5Response :: SetWebhookError ( e. clone ( ) . into ( ) ) ,
237237 )
238238 . into ( ) ;
239239 self . pending_messages . enqueue ( & counterparty_node_id, msg) ;
240- LightningError {
240+ return Err ( LightningError {
241241 err : e. message ( ) . into ( ) ,
242242 action : ErrorAction :: IgnoreAndLog ( Level :: Info ) ,
243- }
244- } ) ? ;
243+ } ) ;
244+ }
245245 }
246246
247247 let msg = LSPS5Message :: Response (
@@ -327,10 +327,14 @@ where
327327 /// This builds a [`WebhookNotificationMethod::LSPS5PaymentIncoming`] webhook notification, signs it with your
328328 /// node key, and enqueues HTTP POSTs to all registered webhook URLs for that client.
329329 ///
330+ /// This may fail if a similar notification was sent too recently,
331+ /// violating the notification cooldown period defined in [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`].
332+ ///
330333 /// # Parameters
331334 /// - `client_id`: the client's node-ID whose webhooks should be invoked.
332335 ///
333336 /// [`WebhookNotificationMethod::LSPS5PaymentIncoming`]: super::msgs::WebhookNotificationMethod::LSPS5PaymentIncoming
337+ /// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
334338 pub fn notify_payment_incoming ( & self , client_id : PublicKey ) -> Result < ( ) , LSPS5ProtocolError > {
335339 let notification = WebhookNotification :: payment_incoming ( ) ;
336340 self . send_notifications_to_client_webhooks ( client_id, notification)
@@ -344,11 +348,15 @@ where
344348 /// the `timeout` block height, signs it, and enqueues HTTP POSTs to the client's
345349 /// registered webhooks.
346350 ///
351+ /// This may fail if a similar notification was sent too recently,
352+ /// violating the notification cooldown period defined in [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`].
353+ ///
347354 /// # Parameters
348355 /// - `client_id`: the client's node-ID whose webhooks should be invoked.
349356 /// - `timeout`: the block height at which the channel contract will expire.
350357 ///
351358 /// [`WebhookNotificationMethod::LSPS5ExpirySoon`]: super::msgs::WebhookNotificationMethod::LSPS5ExpirySoon
359+ /// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
352360 pub fn notify_expiry_soon (
353361 & self , client_id : PublicKey , timeout : u32 ,
354362 ) -> Result < ( ) , LSPS5ProtocolError > {
@@ -362,10 +370,14 @@ where
362370 /// liquidity for `client_id`. Builds a [`WebhookNotificationMethod::LSPS5LiquidityManagementRequest`] notification,
363371 /// signs it, and sends it to all of the client's registered webhook URLs.
364372 ///
373+ /// This may fail if a similar notification was sent too recently,
374+ /// violating the notification cooldown period defined in [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`].
375+ ///
365376 /// # Parameters
366377 /// - `client_id`: the client's node-ID whose webhooks should be invoked.
367378 ///
368379 /// [`WebhookNotificationMethod::LSPS5LiquidityManagementRequest`]: super::msgs::WebhookNotificationMethod::LSPS5LiquidityManagementRequest
380+ /// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
369381 pub fn notify_liquidity_management_request (
370382 & self , client_id : PublicKey ,
371383 ) -> Result < ( ) , LSPS5ProtocolError > {
@@ -379,10 +391,14 @@ where
379391 /// for `client_id` while the client is offline. Builds a [`WebhookNotificationMethod::LSPS5OnionMessageIncoming`]
380392 /// notification, signs it, and enqueues HTTP POSTs to each registered webhook.
381393 ///
394+ /// This may fail if a similar notification was sent too recently,
395+ /// violating the notification cooldown period defined in [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`].
396+ ///
382397 /// # Parameters
383398 /// - `client_id`: the client's node-ID whose webhooks should be invoked.
384399 ///
385400 /// [`WebhookNotificationMethod::LSPS5OnionMessageIncoming`]: super::msgs::WebhookNotificationMethod::LSPS5OnionMessageIncoming
401+ /// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
386402 pub fn notify_onion_message_incoming (
387403 & self , client_id : PublicKey ,
388404 ) -> Result < ( ) , LSPS5ProtocolError > {
@@ -403,23 +419,27 @@ where
403419 let now =
404420 LSPSDateTime :: new_from_duration_since_epoch ( self . time_provider . duration_since_epoch ( ) ) ;
405421
406- for ( app_name , webhook ) in client_webhooks. iter_mut ( ) {
407- if webhook
422+ let rate_limit_applies = client_webhooks. iter ( ) . any ( | ( _ , webhook ) | {
423+ webhook
408424 . last_notification_sent
409425 . get ( & notification. method )
410- . map ( |last_sent| now. clone ( ) . abs_diff ( & last_sent) )
411- . map_or ( true , |last_sent| {
412- last_sent >= self . config . notification_cooldown_hours . as_secs ( )
413- } ) {
414- webhook. last_notification_sent . insert ( notification. method . clone ( ) , now. clone ( ) ) ;
415- webhook. last_used = now. clone ( ) ;
416- self . send_notification (
417- client_id,
418- app_name. clone ( ) ,
419- webhook. url . clone ( ) ,
420- notification. clone ( ) ,
421- ) ?;
422- }
426+ . map ( |last_sent| now. abs_diff ( & last_sent) )
427+ . map_or ( false , |duration| duration < DEFAULT_NOTIFICATION_COOLDOWN_HOURS . as_secs ( ) )
428+ } ) ;
429+
430+ if rate_limit_applies {
431+ return Err ( LSPS5ProtocolError :: SlowDownError ) ;
432+ }
433+
434+ for ( app_name, webhook) in client_webhooks. iter_mut ( ) {
435+ webhook. last_notification_sent . insert ( notification. method . clone ( ) , now. clone ( ) ) ;
436+ webhook. last_used = now. clone ( ) ;
437+ self . send_notification (
438+ client_id,
439+ app_name. clone ( ) ,
440+ webhook. url . clone ( ) ,
441+ notification. clone ( ) ,
442+ ) ?;
423443 }
424444 Ok ( ( ) )
425445 }
0 commit comments