Skip to content

Commit e56052d

Browse files
committed
lsps5: Add prune_webhook API to remove a client's webhook entry
Add `LSPS5ServiceHandler::prune_webhook` that removes a single webhook entry identified by `counterparty_node_id` and `LSPS5AppName`. The method is synchronous, consistent with all other public `notify_*` methods on this handler: it marks the peer state as dirty and relies on the normal `LiquidityManager::persist` loop to flush the change to the KVStore. The method reuses the existing private `PeerState::remove_webhook` helper and returns an `APIError::APIMisuseError` if the counterparty has no registered state or the given `app_name` is not found.
1 parent cc4cc39 commit e56052d

2 files changed

Lines changed: 119 additions & 0 deletions

File tree

lightning-liquidity/src/lsps5/service.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use lightning::impl_writeable_tlv_based;
3131
use lightning::ln::channelmanager::AChannelManager;
3232
use lightning::ln::msgs::{ErrorAction, LightningError};
3333
use lightning::sign::NodeSigner;
34+
use lightning::util::errors::APIError;
3435
use lightning::util::logger::Level;
3536
use lightning::util::persist::KVStore;
3637
use lightning::util::ser::Writeable;
@@ -580,6 +581,38 @@ where
580581
self.send_notifications_to_client_webhooks(client_id, notification)
581582
}
582583

584+
/// Removes a specific webhook registration for a client.
585+
///
586+
/// This can be used to prune webhook state for a client that is no longer active or whose
587+
/// webhooks are no longer relevant, complementing the automatic 30-day stale webhook pruning.
588+
/// The state change will be persisted on the next call to [`LiquidityManager::persist`].
589+
///
590+
/// Returns an [`APIError::APIMisuseError`] if the client has no registered state or no
591+
/// webhook with the given `app_name` is registered for that client.
592+
///
593+
/// [`LiquidityManager::persist`]: crate::LiquidityManager::persist
594+
pub fn prune_webhook(
595+
&self, counterparty_node_id: PublicKey, app_name: &LSPS5AppName,
596+
) -> Result<(), APIError> {
597+
let mut outer_state_lock = self.per_peer_state.write().unwrap();
598+
match outer_state_lock.get_mut(&counterparty_node_id) {
599+
Some(peer_state) => {
600+
if !peer_state.remove_webhook(app_name) {
601+
return Err(APIError::APIMisuseError {
602+
err: format!(
603+
"No webhook with app_name '{}' registered for counterparty {}",
604+
app_name, counterparty_node_id
605+
),
606+
});
607+
}
608+
Ok(())
609+
},
610+
None => Err(APIError::APIMisuseError {
611+
err: format!("No existing state with counterparty {}", counterparty_node_id),
612+
}),
613+
}
614+
}
615+
583616
fn send_notifications_to_client_webhooks(
584617
&self, client_id: PublicKey, notification: WebhookNotification,
585618
) -> Result<(), LSPS5ProtocolError> {

lightning-liquidity/tests/lsps5_integration_tests.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1633,3 +1633,89 @@ fn lsps5_service_handler_persistence_across_restarts() {
16331633
}
16341634
}
16351635
}
1636+
1637+
#[test]
1638+
fn prune_webhook_removes_registered_webhook() {
1639+
let chanmon_cfgs = create_chanmon_cfgs(2);
1640+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
1641+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
1642+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
1643+
let (lsps_nodes, _) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider));
1644+
let LSPSNodes { service_node, client_node, .. } = lsps_nodes;
1645+
create_chan_between_nodes(&service_node.inner, &client_node.inner);
1646+
1647+
let service_node_id = service_node.inner.node.get_our_node_id();
1648+
let client_node_id = client_node.inner.node.get_our_node_id();
1649+
let service_handler = service_node.liquidity_manager.lsps5_service_handler().unwrap();
1650+
1651+
// assert_lsps5_accept registers a webhook with app_name "App".
1652+
let app_name = LSPS5AppName::from_string("App".to_string()).unwrap();
1653+
1654+
// Register a webhook through the normal request flow.
1655+
assert_lsps5_accept(&service_node, &client_node);
1656+
1657+
// The notification must be emitted before pruning to confirm the webhook is registered.
1658+
let result = service_handler.notify_payment_incoming(client_node_id);
1659+
assert!(result.is_ok());
1660+
assert!(service_node.liquidity_manager.next_event().is_some(), "Webhook notification expected");
1661+
1662+
// Prune the webhook.
1663+
let prune_result = service_handler.prune_webhook(client_node_id, &app_name);
1664+
assert!(prune_result.is_ok(), "prune_webhook should succeed for a registered webhook");
1665+
1666+
// After pruning, notify should succeed but produce no event (no webhooks left).
1667+
let result_after = service_handler.notify_payment_incoming(client_node_id);
1668+
assert!(result_after.is_ok());
1669+
assert!(
1670+
service_node.liquidity_manager.next_event().is_none(),
1671+
"No notification event expected after pruning"
1672+
);
1673+
1674+
// A second prune of the same app_name must fail.
1675+
let second_prune = service_handler.prune_webhook(client_node_id, &app_name);
1676+
assert!(second_prune.is_err(), "prune_webhook should fail when the webhook is already removed");
1677+
1678+
let _ = service_node_id; // suppress unused warning
1679+
}
1680+
1681+
#[test]
1682+
fn prune_webhook_fails_for_unknown_counterparty() {
1683+
let chanmon_cfgs = create_chanmon_cfgs(2);
1684+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
1685+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
1686+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
1687+
let (lsps_nodes, _) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider));
1688+
let LSPSNodes { service_node, client_node, .. } = lsps_nodes;
1689+
create_chan_between_nodes(&service_node.inner, &client_node.inner);
1690+
1691+
let client_node_id = client_node.inner.node.get_our_node_id();
1692+
let service_handler = service_node.liquidity_manager.lsps5_service_handler().unwrap();
1693+
1694+
let app_name = LSPS5AppName::from_string("SomeApp".to_string()).unwrap();
1695+
1696+
// Pruning for a peer with no state must return an error.
1697+
let result = service_handler.prune_webhook(client_node_id, &app_name);
1698+
assert!(result.is_err(), "prune_webhook should fail when counterparty has no state");
1699+
}
1700+
1701+
#[test]
1702+
fn prune_webhook_fails_for_unknown_app_name() {
1703+
let chanmon_cfgs = create_chanmon_cfgs(2);
1704+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
1705+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
1706+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
1707+
let (lsps_nodes, _) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider));
1708+
let LSPSNodes { service_node, client_node, .. } = lsps_nodes;
1709+
create_chan_between_nodes(&service_node.inner, &client_node.inner);
1710+
1711+
let client_node_id = client_node.inner.node.get_our_node_id();
1712+
let service_handler = service_node.liquidity_manager.lsps5_service_handler().unwrap();
1713+
1714+
// Register one webhook.
1715+
assert_lsps5_accept(&service_node, &client_node);
1716+
1717+
// Pruning with a different app_name must fail.
1718+
let unknown_app_name = LSPS5AppName::from_string("UnknownApp".to_string()).unwrap();
1719+
let result = service_handler.prune_webhook(client_node_id, &unknown_app_name);
1720+
assert!(result.is_err(), "prune_webhook should fail for an unregistered app_name");
1721+
}

0 commit comments

Comments
 (0)