crates: add convenience typed methods for hook subscriptions
What changed, and why it matters
This commit adds new convenience methods to the Core Lightning plugin library in Rust. It lets plugin authors register 'hooks' (callbacks triggered by the lightning daemon) using typed request/response structures instead of raw JSON. The change is purely additive and does not fix any known bug or vulnerability.
No security action required. This is a routine feature addition. Reviewers may optionally note that deserialization failure terminates the process, which is intentional defensive behavior.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff introduces hook_typed on Builder and new_typed on HookBuilder in plugins/src/lib.rs. These methods deserialize incoming hook requests into a user-specified Req type and serialize the returned Resp type back to JSON. If deserialization fails, the plugin logs a warning and calls std::process::exit(1). The existing raw JSON hook methods remain unchanged. No security defect is present in the diff.
Changed components
plugins/src/lib.rscln-plugin Rust crateInspect captured patch +90 / −1
diff --git a/plugins/src/lib.rs b/plugins/src/lib.rs
index f29fc0ba..483f9fed 100644
--- a/plugins/src/lib.rs
+++ b/plugins/src/lib.rs
@@ -2,6 +2,7 @@ use crate::codec::{JsonCodec, JsonRpcCodec};
pub use anyhow::anyhow;
use anyhow::{Context, Result};
use futures::sink::SinkExt;
+use serde::de::DeserializeOwned;
use serde::Serialize;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
extern crate log;
@@ -195,7 +196,9 @@ where
self
}
- /// Add a subscription to a given `hookname`
+ /// Add a hook subscription for `hookname` with a raw [`serde_json::Value`] request and response.
+ /// Prefer [`Builder::hook_typed`] for type-safe hooks, or [`Builder::hook_from_builder`] if you
+ /// need to configure `before`, `after`, or `filters`.
pub fn hook<C, F>(mut self, hookname: &str, callback: C) -> Self
where
C: Send + Sync + 'static,
@@ -215,11 +218,60 @@ where
self
}
+ /// Add a hook subscription using a [`HookBuilder`], which allows configuring `before`, `after`,
+ /// and `filters` in addition to the callback. Use [`HookBuilder::new`] for raw
+ /// [`serde_json::Value`] hooks or [`HookBuilder::new_typed`] for type-safe hooks.
pub fn hook_from_builder(mut self, hook: HookBuilder<S>) -> Builder<S, I, O> {
self.hooks.insert(hook.name.clone(), hook.build());
self
}
+ /// Add a hook subscription for `hookname` with typed request and response. The request is
+ /// deserialized from JSON into `Req` and the response is serialized from `Resp` back to JSON
+ /// automatically. If deserialization of the request fails, the hook returns an error to CLN.
+ /// Use [`Builder::hook_from_builder`] with [`HookBuilder::new_typed`] if you additionally need
+ /// to configure `before`, `after`, or `filters`.
+ pub fn hook_typed<C, F, Req, Resp>(mut self, hookname: &str, callback: C) -> Self
+ where
+ C: Send + Sync + 'static,
+ C: Fn(Plugin<S>, Req) -> F + 'static,
+ F: Future<Output = Result<Resp, Error>> + Send + 'static,
+ Req: DeserializeOwned + Send + 'static,
+ Resp: Serialize + Send + 'static,
+ {
+ let hookname = hookname.to_string();
+ self.hooks.insert(
+ hookname.clone(),
+ Hook {
+ name: hookname.clone(),
+ callback: Box::new(move |p, r| {
+ let typed_req = serde_json::from_value(r).unwrap_or_else(|e| {
+ let error = format!(
+ "cln-plugin: hook '{hookname}' received a request that doesn't match \
+ the expected schema. Error: {e}"
+ );
+ println!(
+ "{}",
+ serde_json::json!({"jsonrpc": "2.0",
+ "method": "log",
+ "params": {"level":"warn", "message":error}})
+ );
+ std::process::exit(1);
+ });
+ let fut = callback(p, typed_req);
+ Box::pin(async move {
+ let typed_resp = fut.await?;
+ serde_json::to_value(typed_resp).map_err(Error::from)
+ })
+ }),
+ before: Vec::new(),
+ after: Vec::new(),
+ filters: None,
+ },
+ );
+ self
+ }
+
/// Register a custom RPC method for the RPC passthrough from the
/// main daemon
pub fn rpcmethod<C, F>(mut self, name: &str, description: &str, callback: C) -> Builder<S, I, O>
@@ -498,6 +550,43 @@ where
}
}
+ pub fn new_typed<C, F, Req, Resp>(name: &str, callback: C) -> Self
+ where
+ C: Send + Sync + 'static,
+ C: Fn(Plugin<S>, Req) -> F + 'static,
+ F: Future<Output = Result<Resp, Error>> + Send + 'static,
+ Req: DeserializeOwned + Send + 'static,
+ Resp: Serialize + Send + 'static,
+ {
+ let hookname = name.to_string();
+ Self {
+ name: hookname.clone(),
+ callback: Box::new(move |p, r| {
+ let typed_req = serde_json::from_value(r).unwrap_or_else(|e| {
+ let error = format!(
+ "cln-plugin: hook '{hookname}' received a request that doesn't match \
+ the expected schema. Error: {e}"
+ );
+ println!(
+ "{}",
+ serde_json::json!({"jsonrpc": "2.0",
+ "method": "log",
+ "params": {"level":"warn", "message":error}})
+ );
+ std::process::exit(1);
+ });
+ let fut = callback(p, typed_req);
+ Box::pin(async move {
+ let typed_resp = fut.await?;
+ serde_json::to_value(typed_resp).map_err(Error::from)
+ })
+ }),
+ before: Vec::new(),
+ after: Vec::new(),
+ filters: None,
+ }
+ }
+
pub fn before(mut self, before: Vec<String>) -> Self {
self.before = before;
self
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.