@@ -51,7 +51,11 @@ struct StoredWebhook {
51
51
_app_name : LSPS5AppName ,
52
52
url : LSPS5WebhookUrl ,
53
53
_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.
54
56
last_used : LSPSDateTime ,
57
+ // Map of last notification sent timestamps for each notification method.
58
+ // This is used to enforce notification cooldowns.
55
59
last_notification_sent : HashMap < WebhookNotificationMethod , LSPSDateTime > ,
56
60
}
57
61
@@ -60,8 +64,6 @@ struct StoredWebhook {
60
64
pub struct LSPS5ServiceConfig {
61
65
/// Maximum number of webhooks allowed per client.
62
66
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 ,
65
67
}
66
68
67
69
/// Default maximum number of webhooks allowed per client.
@@ -72,10 +74,7 @@ pub const DEFAULT_NOTIFICATION_COOLDOWN_HOURS: Duration = Duration::from_secs(60
72
74
// Default configuration for LSPS5 service.
73
75
impl Default for LSPS5ServiceConfig {
74
76
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 }
79
78
}
80
79
}
81
80
@@ -93,8 +92,6 @@ impl Default for LSPS5ServiceConfig {
93
92
/// - `lsps5.remove_webhook` -> delete a named webhook or return [`app_name_not_found`] error.
94
93
/// - Prune stale webhooks after a client has no open channels and no activity for at least
95
94
/// [`MIN_WEBHOOK_RETENTION_DAYS`].
96
- /// - Rate-limit repeat notifications of the same method to a client by
97
- /// [`notification_cooldown_hours`].
98
95
/// - Sign and enqueue outgoing webhook notifications:
99
96
/// - Construct JSON-RPC 2.0 Notification objects [`WebhookNotification`],
100
97
/// - Timestamp and LN-style zbase32-sign each payload,
@@ -109,7 +106,6 @@ impl Default for LSPS5ServiceConfig {
109
106
/// [`bLIP-55 / LSPS5`]: https://github.com/lightning/blips/pull/55/files
110
107
/// [`max_webhooks_per_client`]: super::service::LSPS5ServiceConfig::max_webhooks_per_client
111
108
/// [`app_name_not_found`]: super::msgs::LSPS5ProtocolError::AppNameNotFound
112
- /// [`notification_cooldown_hours`]: super::service::LSPS5ServiceConfig::notification_cooldown_hours
113
109
/// [`WebhookNotification`]: super::msgs::WebhookNotification
114
110
/// [`LSPS5ServiceEvent::SendWebhookNotification`]: super::event::LSPS5ServiceEvent::SendWebhookNotification
115
111
/// [`app_name`]: super::msgs::LSPS5AppName
@@ -225,23 +221,27 @@ where
225
221
}
226
222
227
223
if !no_change {
228
- self . send_webhook_registered_notification (
224
+ let result = self . send_webhook_registered_notification (
229
225
counterparty_node_id,
230
226
params. app_name ,
231
227
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 ( ) ;
234
234
let msg = LSPS5Message :: Response (
235
235
request_id. clone ( ) ,
236
236
LSPS5Response :: SetWebhookError ( e. clone ( ) . into ( ) ) ,
237
237
)
238
238
. into ( ) ;
239
239
self . pending_messages . enqueue ( & counterparty_node_id, msg) ;
240
- LightningError {
240
+ return Err ( LightningError {
241
241
err : e. message ( ) . into ( ) ,
242
242
action : ErrorAction :: IgnoreAndLog ( Level :: Info ) ,
243
- }
244
- } ) ? ;
243
+ } ) ;
244
+ }
245
245
}
246
246
247
247
let msg = LSPS5Message :: Response (
@@ -327,10 +327,14 @@ where
327
327
/// This builds a [`WebhookNotificationMethod::LSPS5PaymentIncoming`] webhook notification, signs it with your
328
328
/// node key, and enqueues HTTP POSTs to all registered webhook URLs for that client.
329
329
///
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
+ ///
330
333
/// # Parameters
331
334
/// - `client_id`: the client's node-ID whose webhooks should be invoked.
332
335
///
333
336
/// [`WebhookNotificationMethod::LSPS5PaymentIncoming`]: super::msgs::WebhookNotificationMethod::LSPS5PaymentIncoming
337
+ /// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
334
338
pub fn notify_payment_incoming ( & self , client_id : PublicKey ) -> Result < ( ) , LSPS5ProtocolError > {
335
339
let notification = WebhookNotification :: payment_incoming ( ) ;
336
340
self . send_notifications_to_client_webhooks ( client_id, notification)
@@ -344,11 +348,15 @@ where
344
348
/// the `timeout` block height, signs it, and enqueues HTTP POSTs to the client's
345
349
/// registered webhooks.
346
350
///
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
+ ///
347
354
/// # Parameters
348
355
/// - `client_id`: the client's node-ID whose webhooks should be invoked.
349
356
/// - `timeout`: the block height at which the channel contract will expire.
350
357
///
351
358
/// [`WebhookNotificationMethod::LSPS5ExpirySoon`]: super::msgs::WebhookNotificationMethod::LSPS5ExpirySoon
359
+ /// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
352
360
pub fn notify_expiry_soon (
353
361
& self , client_id : PublicKey , timeout : u32 ,
354
362
) -> Result < ( ) , LSPS5ProtocolError > {
@@ -362,10 +370,14 @@ where
362
370
/// liquidity for `client_id`. Builds a [`WebhookNotificationMethod::LSPS5LiquidityManagementRequest`] notification,
363
371
/// signs it, and sends it to all of the client's registered webhook URLs.
364
372
///
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
+ ///
365
376
/// # Parameters
366
377
/// - `client_id`: the client's node-ID whose webhooks should be invoked.
367
378
///
368
379
/// [`WebhookNotificationMethod::LSPS5LiquidityManagementRequest`]: super::msgs::WebhookNotificationMethod::LSPS5LiquidityManagementRequest
380
+ /// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
369
381
pub fn notify_liquidity_management_request (
370
382
& self , client_id : PublicKey ,
371
383
) -> Result < ( ) , LSPS5ProtocolError > {
@@ -379,10 +391,14 @@ where
379
391
/// for `client_id` while the client is offline. Builds a [`WebhookNotificationMethod::LSPS5OnionMessageIncoming`]
380
392
/// notification, signs it, and enqueues HTTP POSTs to each registered webhook.
381
393
///
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
+ ///
382
397
/// # Parameters
383
398
/// - `client_id`: the client's node-ID whose webhooks should be invoked.
384
399
///
385
400
/// [`WebhookNotificationMethod::LSPS5OnionMessageIncoming`]: super::msgs::WebhookNotificationMethod::LSPS5OnionMessageIncoming
401
+ /// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
386
402
pub fn notify_onion_message_incoming (
387
403
& self , client_id : PublicKey ,
388
404
) -> Result < ( ) , LSPS5ProtocolError > {
@@ -403,23 +419,27 @@ where
403
419
let now =
404
420
LSPSDateTime :: new_from_duration_since_epoch ( self . time_provider . duration_since_epoch ( ) ) ;
405
421
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
408
424
. last_notification_sent
409
425
. 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
+ ) ?;
423
443
}
424
444
Ok ( ( ) )
425
445
}
0 commit comments