@@ -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,18 @@ 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 ,
67
+ }
68
+
69
+ /// Default maximum number of webhooks allowed per client.
70
+ pub const DEFAULT_MAX_WEBHOOKS_PER_CLIENT : u32 = 10 ;
71
+ /// Default notification cooldown time in hours.
72
+ pub const DEFAULT_NOTIFICATION_COOLDOWN_HOURS : Duration = Duration :: from_secs ( 60 * 60 ) ; // 1 hour
73
+
74
+ // Default configuration for LSPS5 service.
75
+ impl Default for LSPS5ServiceConfig {
76
+ fn default ( ) -> Self {
77
+ Self { max_webhooks_per_client : DEFAULT_MAX_WEBHOOKS_PER_CLIENT }
78
+ }
65
79
}
66
80
67
81
/// Service-side handler for the [`bLIP-55 / LSPS5`] webhook registration protocol.
@@ -78,8 +92,6 @@ pub struct LSPS5ServiceConfig {
78
92
/// - `lsps5.remove_webhook` -> delete a named webhook or return [`app_name_not_found`] error.
79
93
/// - Prune stale webhooks after a client has no open channels and no activity for at least
80
94
/// [`MIN_WEBHOOK_RETENTION_DAYS`].
81
- /// - Rate-limit repeat notifications of the same method to a client by
82
- /// [`notification_cooldown_hours`].
83
95
/// - Sign and enqueue outgoing webhook notifications:
84
96
/// - Construct JSON-RPC 2.0 Notification objects [`WebhookNotification`],
85
97
/// - Timestamp and LN-style zbase32-sign each payload,
@@ -94,7 +106,6 @@ pub struct LSPS5ServiceConfig {
94
106
/// [`bLIP-55 / LSPS5`]: https://github.com/lightning/blips/pull/55/files
95
107
/// [`max_webhooks_per_client`]: super::service::LSPS5ServiceConfig::max_webhooks_per_client
96
108
/// [`app_name_not_found`]: super::msgs::LSPS5ProtocolError::AppNameNotFound
97
- /// [`notification_cooldown_hours`]: super::service::LSPS5ServiceConfig::notification_cooldown_hours
98
109
/// [`WebhookNotification`]: super::msgs::WebhookNotification
99
110
/// [`LSPS5ServiceEvent::SendWebhookNotification`]: super::event::LSPS5ServiceEvent::SendWebhookNotification
100
111
/// [`app_name`]: super::msgs::LSPS5AppName
@@ -318,10 +329,14 @@ where
318
329
/// This builds a [`WebhookNotificationMethod::LSPS5PaymentIncoming`] webhook notification, signs it with your
319
330
/// node key, and enqueues HTTP POSTs to all registered webhook URLs for that client.
320
331
///
332
+ /// This may fail if a similar notification was sent too recently,
333
+ /// violating the notification cooldown period defined in [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`].
334
+ ///
321
335
/// # Parameters
322
336
/// - `client_id`: the client's node-ID whose webhooks should be invoked.
323
337
///
324
338
/// [`WebhookNotificationMethod::LSPS5PaymentIncoming`]: super::msgs::WebhookNotificationMethod::LSPS5PaymentIncoming
339
+ /// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
325
340
pub fn notify_payment_incoming ( & self , client_id : PublicKey ) -> Result < ( ) , LSPS5ProtocolError > {
326
341
let notification = WebhookNotification :: payment_incoming ( ) ;
327
342
self . send_notifications_to_client_webhooks ( client_id, notification)
@@ -335,11 +350,15 @@ where
335
350
/// the `timeout` block height, signs it, and enqueues HTTP POSTs to the client's
336
351
/// registered webhooks.
337
352
///
353
+ /// This may fail if a similar notification was sent too recently,
354
+ /// violating the notification cooldown period defined in [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`].
355
+ ///
338
356
/// # Parameters
339
357
/// - `client_id`: the client's node-ID whose webhooks should be invoked.
340
358
/// - `timeout`: the block height at which the channel contract will expire.
341
359
///
342
360
/// [`WebhookNotificationMethod::LSPS5ExpirySoon`]: super::msgs::WebhookNotificationMethod::LSPS5ExpirySoon
361
+ /// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
343
362
pub fn notify_expiry_soon (
344
363
& self , client_id : PublicKey , timeout : u32 ,
345
364
) -> Result < ( ) , LSPS5ProtocolError > {
@@ -353,10 +372,14 @@ where
353
372
/// liquidity for `client_id`. Builds a [`WebhookNotificationMethod::LSPS5LiquidityManagementRequest`] notification,
354
373
/// signs it, and sends it to all of the client's registered webhook URLs.
355
374
///
375
+ /// This may fail if a similar notification was sent too recently,
376
+ /// violating the notification cooldown period defined in [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`].
377
+ ///
356
378
/// # Parameters
357
379
/// - `client_id`: the client's node-ID whose webhooks should be invoked.
358
380
///
359
381
/// [`WebhookNotificationMethod::LSPS5LiquidityManagementRequest`]: super::msgs::WebhookNotificationMethod::LSPS5LiquidityManagementRequest
382
+ /// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
360
383
pub fn notify_liquidity_management_request (
361
384
& self , client_id : PublicKey ,
362
385
) -> Result < ( ) , LSPS5ProtocolError > {
@@ -370,10 +393,14 @@ where
370
393
/// for `client_id` while the client is offline. Builds a [`WebhookNotificationMethod::LSPS5OnionMessageIncoming`]
371
394
/// notification, signs it, and enqueues HTTP POSTs to each registered webhook.
372
395
///
396
+ /// This may fail if a similar notification was sent too recently,
397
+ /// violating the notification cooldown period defined in [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`].
398
+ ///
373
399
/// # Parameters
374
400
/// - `client_id`: the client's node-ID whose webhooks should be invoked.
375
401
///
376
402
/// [`WebhookNotificationMethod::LSPS5OnionMessageIncoming`]: super::msgs::WebhookNotificationMethod::LSPS5OnionMessageIncoming
403
+ /// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
377
404
pub fn notify_onion_message_incoming (
378
405
& self , client_id : PublicKey ,
379
406
) -> Result < ( ) , LSPS5ProtocolError > {
@@ -394,24 +421,34 @@ where
394
421
let now =
395
422
LSPSDateTime :: new_from_duration_since_epoch ( self . time_provider . duration_since_epoch ( ) ) ;
396
423
397
- for ( app_name, webhook) in client_webhooks. iter_mut ( ) {
398
- if webhook
399
- . last_notification_sent
400
- . get ( & notification. method )
401
- . map ( |last_sent| now. clone ( ) . abs_diff ( & last_sent) )
402
- . map_or ( true , |duration| {
403
- duration >= self . config . notification_cooldown_hours . as_secs ( )
404
- } ) {
405
- webhook. last_notification_sent . insert ( notification. method . clone ( ) , now. clone ( ) ) ;
406
- webhook. last_used = now. clone ( ) ;
407
- self . send_notification (
408
- client_id,
409
- app_name. clone ( ) ,
410
- webhook. url . clone ( ) ,
411
- notification. clone ( ) ,
412
- ) ?;
424
+ // We must avoid sending multiple notifications of the same method
425
+ // (other than lsps5.webhook_registered) close in time.
426
+ if notification. method != WebhookNotificationMethod :: LSPS5WebhookRegistered {
427
+ let rate_limit_applies = client_webhooks. iter ( ) . any ( |( _, webhook) | {
428
+ webhook
429
+ . last_notification_sent
430
+ . get ( & notification. method )
431
+ . map ( |last_sent| now. abs_diff ( & last_sent) )
432
+ . map_or ( false , |duration| {
433
+ duration < DEFAULT_NOTIFICATION_COOLDOWN_HOURS . as_secs ( )
434
+ } )
435
+ } ) ;
436
+
437
+ if rate_limit_applies {
438
+ return Err ( LSPS5ProtocolError :: SlowDownError ) ;
413
439
}
414
440
}
441
+
442
+ for ( app_name, webhook) in client_webhooks. iter_mut ( ) {
443
+ webhook. last_notification_sent . insert ( notification. method . clone ( ) , now. clone ( ) ) ;
444
+ webhook. last_used = now. clone ( ) ;
445
+ self . send_notification (
446
+ client_id,
447
+ app_name. clone ( ) ,
448
+ webhook. url . clone ( ) ,
449
+ notification. clone ( ) ,
450
+ ) ?;
451
+ }
415
452
Ok ( ( ) )
416
453
}
417
454
@@ -487,6 +524,15 @@ where
487
524
. iter ( )
488
525
. any ( |c| c. is_usable && c. counterparty . node_id == * client_id)
489
526
}
527
+
528
+ pub ( crate ) fn peer_connected ( & self , counterparty_node_id : & PublicKey ) {
529
+ let mut webhooks = self . webhooks . lock ( ) . unwrap ( ) ;
530
+ if let Some ( client_webhooks) = webhooks. get_mut ( counterparty_node_id) {
531
+ for webhook in client_webhooks. values_mut ( ) {
532
+ webhook. last_notification_sent . clear ( ) ;
533
+ }
534
+ }
535
+ }
490
536
}
491
537
492
538
impl < CM : Deref , NS : Deref , TP : Deref > LSPSProtocolMessageHandler for LSPS5ServiceHandler < CM , NS , TP >
0 commit comments