plugins: lsps: refactor client to use jsonrpcresponse
What changed, and why it matters
This commit is a straightforward internal code cleanup in Core Lightning's LSPS (Lightning Service Provider Specification) plugin. It changes how JSON-RPC responses are handled so that transport/parser problems are kept separate from actual JSON-RPC error responses returned by a remote server. There is no indication this fixes a security bug or introduces a vulnerability; it is a refactoring for better code organization.
No security action required. Treat as normal code-quality refactoring; review as part of routine development if this component is relevant to your deployment.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch refactors the LSPS plugin’s JSON-RPC client to return a JsonRpcResponse<RS> wrapper instead of immediately unwrapping it into Result<RS>. Callers now explicitly call .into_result()? (or .expect() in tests) to obtain the typed result. The proto/jsonrpc module is simplified to act purely as a wire format module, and the Error enum in jsonrpc/client.rs gains a new RpcError variant. No security-relevant behavior change is evident from the diff.
Changed components
plugins/lsps-plugin/src/client.rsplugins/lsps-plugin/src/jsonrpc/client.rsplugins/lsps-plugin/src/proto/jsonrpc.rsInspect captured patch +43 / −12
diff --git a/plugins/lsps-plugin/src/client.rs b/plugins/lsps-plugin/src/client.rs
index ecd14330..c0a6270b 100644
--- a/plugins/lsps-plugin/src/client.rs
+++ b/plugins/lsps-plugin/src/client.rs
@@ -146,7 +146,8 @@ async fn on_lsps_lsps2_getinfo(
let info_res: Lsps2GetInfoResponse = client
.call_typed(info_req)
.await
- .context("lsps2.get_info call failed")?;
+ .context("lsps2.get_info call failed")?
+ .into_result()?;
debug!("received lsps2.get_info response: {:?}", info_res);
Ok(serde_json::to_value(info_res)?)
@@ -243,7 +244,8 @@ async fn on_lsps_lsps2_buy(
let buy_res: Lsps2BuyResponse = client
.call_typed(buy_req)
.await
- .context("lsps2.buy call failed")?;
+ .context("lsps2.buy call failed")?
+ .into_result()?;
Ok(serde_json::to_value(buy_res)?)
}
@@ -733,7 +735,8 @@ async fn on_lsps_listprotocols(
let res: Lsps0listProtocolsResponse = client
.call_typed(request)
.await
- .map_err(|e| anyhow!("lsps0.list_protocols call failed: {}", e))?;
+ .context("lsps0.list_protocols call failed")?
+ .into_result()?;
debug!("Received lsps0.list_protocols response: {:?}", res);
Ok(serde_json::to_value(res)?)
diff --git a/plugins/lsps-plugin/src/jsonrpc/client.rs b/plugins/lsps-plugin/src/jsonrpc/client.rs
index 6eaad51c..49ec130f 100644
--- a/plugins/lsps-plugin/src/jsonrpc/client.rs
+++ b/plugins/lsps-plugin/src/jsonrpc/client.rs
@@ -1,5 +1,5 @@
use crate::proto::jsonrpc::{
- Error as JsonRpcError, JsonRpcRequest, JsonRpcResponse, RequestObject,
+ Error as JsonRpcError, JsonRpcRequest, JsonRpcResponse, RequestObject, RpcError,
};
use async_trait::async_trait;
use core::fmt::Debug;
@@ -21,6 +21,8 @@ pub enum Error {
Internal(String),
#[error("Got JSON-RPC error")]
JsonRpcError(#[from] JsonRpcError),
+ #[error("{0}")]
+ RpcError(#[from] RpcError),
#[error("Couldn't parse JSON-RPC request")]
ParseRequest {
#[source]
@@ -92,7 +94,7 @@ impl<T: Transport> JsonRpcClient<T> {
///
/// This method provides type safety by using request and response types
/// that implement the necessary traits.
- pub async fn call_typed<RQ, RS>(&self, request: RQ) -> Result<RS>
+ pub async fn call_typed<RQ, RS>(&self, request: RQ) -> Result<JsonRpcResponse<RS>>
where
RQ: JsonRpcRequest + Serialize + Send + Sync,
RS: DeserializeOwned + Serialize + Debug + Send + Sync,
@@ -103,7 +105,7 @@ impl<T: Transport> JsonRpcClient<T> {
debug!("Preparing request: method={}, id={}", method, id);
let request = request.into_request(Some(id.clone().into()));
let response: JsonRpcResponse<RS> = self.send_request(method, &request, id).await?;
- response.into()
+ Ok(response)
}
/// Sends a notification with raw JSON parameters (no response expected).
@@ -296,7 +298,8 @@ mod test_json_rpc {
let res = client_1
.call_typed::<_, DummyResponse>(req.clone())
.await
- .expect("Should have an OK result");
+ .expect("Should have an OK result")
+ .expect("Should not be a JSON-RPC error");
assert_eq!(res, expected_res);
let transport_req = transport
.last_request_json()
@@ -330,7 +333,7 @@ mod test_json_rpc {
serde_json::json!({"got": "some"}),
);
- let res_obj = JsonRpcResponse::error(err_res, "unique-id-123");
+ let res_obj = JsonRpcResponse::error(err_res.clone(), "unique-id-123");
let res_str = serde_json::to_string(&res_obj).unwrap();
let transport = TestTransport {
@@ -343,8 +346,9 @@ mod test_json_rpc {
let res = client_1
.call_typed::<_, DummyResponse>(req)
.await
- .expect_err("Expected error response");
- assert!(matches!(res, Error::JsonRpcError(..)));
+ .expect("only inner rpc error")
+ .expect_err("expect rpc error");
+ assert_eq!(res, err_res);
}
#[tokio::test]
diff --git a/plugins/lsps-plugin/src/proto/jsonrpc.rs b/plugins/lsps-plugin/src/proto/jsonrpc.rs
index b403312f..8fc682d8 100644
--- a/plugins/lsps-plugin/src/proto/jsonrpc.rs
+++ b/plugins/lsps-plugin/src/proto/jsonrpc.rs
@@ -117,6 +117,7 @@ fn is_none_or_null<T: Serialize>(opt: &Option<T>) -> bool {
}
}
+#[derive(Clone, Debug, PartialEq)]
pub struct JsonRpcResponse<R = ()> {
id: String,
body: JsonRpcResponseBody<R>,
@@ -165,15 +166,21 @@ impl<R> JsonRpcResponse<R> {
}
}
- /// Unwrap the result, panicking on RPC error
pub fn unwrap(self) -> R {
self.body.unwrap()
}
- /// Expect success or panic with message
pub fn expect(self, msg: &str) -> R {
self.body.expect(msg)
}
+
+ pub fn unwrap_err(self) -> RpcError {
+ self.body.unwrap_err()
+ }
+
+ pub fn expect_err(self, msg: &str) -> RpcError {
+ self.body.expect_err(msg)
+ }
}
// Custom Serialize to match JSON-RPC 2.0 wire format
@@ -243,6 +250,7 @@ impl<'de, R: DeserializeOwned> Deserialize<'de> for JsonRpcResponse<R> {
}
}
+#[derive(Clone, Debug, PartialEq)]
pub enum JsonRpcResponseBody<R> {
Success { result: R },
Error { error: RpcError },
@@ -294,6 +302,22 @@ impl<R> JsonRpcResponseBody<R> {
Self::Error { error } => panic!("{}: {}", msg, error),
}
}
+
+ pub fn unwrap_err(self) -> RpcError {
+ match self {
+ JsonRpcResponseBody::Success { .. } => {
+ panic!("Called unwrap_err on RPC Success")
+ }
+ JsonRpcResponseBody::Error { error } => error,
+ }
+ }
+
+ pub fn expect_err(self, msg: &str) -> RpcError {
+ match self {
+ JsonRpcResponseBody::Success { .. } => panic!("{}", msg),
+ JsonRpcResponseBody::Error { error } => error,
+ }
+ }
}
/// Macro to generate RpcError helper methods for protocol-specific error codes
Why this scored 14/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.