lsps: Implement JSON-RPC V2 client

Adds an async safe JSON-RPC V2 client for a generic transport layer. The
transport layer we will use later on are BOLT8 lightning messages.

Signed-off-by: Peter Neuroth <pet.v.ne@gmail.com>
This commit is contained in:
Peter Neuroth
2025-03-17 17:38:25 +01:00
committed by ShahanaFarooqui
parent ea5635c4c8
commit 203621a629
5 changed files with 595 additions and 30 deletions

196
Cargo.lock generated
View File

@@ -419,8 +419,15 @@ dependencies = [
name = "cln-lsps"
version = "0.1.0"
dependencies = [
"async-trait",
"dashmap",
"hex",
"log",
"rand 0.9.0",
"serde",
"serde_json",
"thiserror 2.0.11",
"tokio",
]
[[package]]
@@ -519,6 +526,20 @@ dependencies = [
"typenum",
]
[[package]]
name = "dashmap"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
dependencies = [
"cfg-if",
"crossbeam-utils",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "data-encoding"
version = "2.7.0"
@@ -602,7 +623,7 @@ dependencies = [
"hyper 1.5.2",
"hyper-util",
"pin-project-lite",
"rand",
"rand 0.8.5",
"serde",
"serde_json",
"smallvec",
@@ -786,7 +807,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
dependencies = [
"cfg-if",
"libc",
"wasi 0.13.3+wasi-0.2.2",
"windows-targets",
]
[[package]]
@@ -839,6 +872,12 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
version = "0.15.2"
@@ -1231,6 +1270,16 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "lockfree-object-pool"
version = "0.1.6"
@@ -1317,7 +1366,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
]
@@ -1411,6 +1460,29 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "parking_lot"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
]
[[package]]
name = "pem"
version = "3.0.4"
@@ -1481,7 +1553,7 @@ version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy",
"zerocopy 0.7.35",
]
[[package]]
@@ -1572,8 +1644,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
"zerocopy 0.8.23",
]
[[package]]
@@ -1583,7 +1666,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.3",
]
[[package]]
@@ -1592,7 +1685,16 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
"getrandom 0.2.15",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.1",
]
[[package]]
@@ -1609,6 +1711,15 @@ dependencies = [
"yasna",
]
[[package]]
name = "redox_syscall"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
dependencies = [
"bitflags 2.8.0",
]
[[package]]
name = "regex"
version = "1.11.1"
@@ -1661,7 +1772,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
dependencies = [
"cc",
"cfg-if",
"getrandom",
"getrandom 0.2.15",
"libc",
"spin",
"untrusted",
@@ -1813,6 +1924,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sct"
version = "0.7.1"
@@ -1934,6 +2051,15 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
dependencies = [
"libc",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
@@ -2075,7 +2201,7 @@ checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704"
dependencies = [
"cfg-if",
"fastrand",
"getrandom",
"getrandom 0.2.15",
"once_cell",
"rustix",
"windows-sys 0.59.0",
@@ -2183,15 +2309,17 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.43.0"
version = "1.44.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.52.0",
@@ -2343,7 +2471,7 @@ dependencies = [
"indexmap 1.9.3",
"pin-project",
"pin-project-lite",
"rand",
"rand 0.8.5",
"slab",
"tokio",
"tokio-util",
@@ -2474,7 +2602,7 @@ dependencies = [
"http 1.2.0",
"httparse",
"log",
"rand",
"rand 0.8.5",
"sha1",
"thiserror 1.0.69",
"utf-8",
@@ -2619,6 +2747,15 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.13.3+wasi-0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "winapi"
version = "0.3.9"
@@ -2732,6 +2869,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "wit-bindgen-rt"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
dependencies = [
"bitflags 2.8.0",
]
[[package]]
name = "write16"
version = "1.0.0"
@@ -2802,7 +2948,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive",
"zerocopy-derive 0.7.35",
]
[[package]]
name = "zerocopy"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6"
dependencies = [
"zerocopy-derive 0.8.23",
]
[[package]]
@@ -2816,6 +2971,17 @@ dependencies = [
"syn",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zerofrom"
version = "0.1.5"

View File

@@ -4,5 +4,12 @@ version = "0.1.0"
edition = "2021"
[dependencies]
async-trait = "0.1"
dashmap = "6.1"
hex = "0.4"
log = "0.4"
rand = "0.9"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "2.0"
tokio = { version = "1.44", features = ["full"] }

View File

@@ -0,0 +1,351 @@
use async_trait::async_trait;
use core::fmt::Debug;
use log::{debug, error};
use rand::rngs::OsRng;
use rand::TryRngCore;
use serde::{de::DeserializeOwned, Serialize};
use serde_json::Value;
use std::sync::Arc;
use crate::jsonrpc::{
Error, JsonRpcRequest, JsonRpcResponse, RequestObject, ResponseObject, Result,
};
/// Defines the interface for transporting JSON-RPC messages.
///
/// Implementors of this trait are responsible for actually sending the JSON-RPC
/// request over some transport mechanism (RPC, Bolt8, etc.)
#[async_trait]
pub trait Transport {
async fn send(&self, request: String) -> core::result::Result<String, Error>;
async fn notify(&self, request: String) -> core::result::Result<(), Error>;
}
/// A typed JSON-RPC client that works with any transport implementation.
///
/// This client handles the JSON-RPC protocol details including message
/// formatting, request ID generation, and response parsing.
#[derive(Clone)]
pub struct JsonRpcClient<T: Transport> {
transport: Arc<T>,
}
impl<T: Transport> JsonRpcClient<T> {
pub fn new(transport: T) -> Self {
Self {
transport: Arc::new(transport),
}
}
/// Makes a JSON-RPC method call with raw JSON parameters and returns a raw
/// JSON result.
pub async fn call_raw(&self, method: &str, params: Option<Value>) -> Result<Value> {
let id = generate_random_id();
debug!("Preparing request: method={}, id={}", method, id);
let request = RequestObject {
jsonrpc: "2.0".into(),
method: method.into(),
params,
id: Some(id.clone().into()),
};
let res_obj = self.send_request(method, &request, id).await?;
Value::from_response(res_obj)
}
/// Makes a typed JSON-RPC method call with a request object and returns a
/// typed response.
///
/// 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>
where
RQ: JsonRpcRequest + Serialize + Send + Sync,
RS: DeserializeOwned + Serialize + Debug + Send + Sync,
{
let method = RQ::METHOD;
let id = generate_random_id();
debug!("Preparing request: method={}, id={}", method, id);
let request = request.into_request(Some(id.clone().into()));
let res_obj = self.send_request(method, &request, id).await?;
RS::from_response(res_obj)
}
/// Sends a notification with raw JSON parameters (no response expected).
pub async fn notify_raw(&self, method: &str, params: Option<Value>) -> Result<()> {
debug!("Preparing notification: method={}", method);
let request = RequestObject {
jsonrpc: "2.0".into(),
method: method.into(),
params,
id: None,
};
Ok(self.send_notification(method, &request).await?)
}
/// Sends a typed notification (no response expected).
pub async fn notify_typed<RQ>(&self, request: RQ) -> Result<()>
where
RQ: JsonRpcRequest + Serialize + Send + Sync,
{
let method = RQ::METHOD;
debug!("Preparing notification: method={}", method);
let request = request.into_request(None);
Ok(self.send_notification(method, &request).await?)
}
async fn send_request<RS, RP>(
&self,
method: &str,
payload: &RP,
id: String,
) -> Result<ResponseObject<RS>>
where
RP: Serialize + Send + Sync,
RS: DeserializeOwned + Serialize + Debug + Send + Sync,
{
let request_json = serde_json::to_string(&payload)?;
debug!(
"Sending request: method={}, id={}, request={:?}",
method, id, &request_json
);
let start = tokio::time::Instant::now();
let res_str = self.transport.send(request_json).await?;
let elapsed = start.elapsed();
debug!(
"Received response: method={}, id={}, response={}, elapsed={}ms",
method,
id,
&res_str,
elapsed.as_millis()
);
Ok(serde_json::from_str(&res_str)?)
}
async fn send_notification<RP>(&self, method: &str, payload: &RP) -> Result<()>
where
RP: Serialize + Send + Sync,
{
let request_json = serde_json::to_string(&payload)?;
debug!("Sending notification: method={}", method);
let start = tokio::time::Instant::now();
self.transport.notify(request_json).await?;
let elapsed = start.elapsed();
debug!(
"Sent notification: method={}, elapsed={}ms",
method,
elapsed.as_millis()
);
Ok(())
}
}
/// Generates a random ID for JSON-RPC requests.
///
/// Uses a secure random number generator to create a hex-encoded ID. Falls back
/// to a timestamp-based ID if random generation fails.
fn generate_random_id() -> String {
let mut bytes = [0u8; 10];
match OsRng.try_fill_bytes(&mut bytes) {
Ok(_) => hex::encode(bytes),
Err(e) => {
// Fallback to a timestamp-based ID if random generation fails
error!(
"Failed to generate random ID: {}, falling back to timestamp",
e
);
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
format!("fallback-{}", timestamp)
}
}
}
#[cfg(test)]
mod test_json_rpc {
use serde::Deserialize;
use tokio::sync::OnceCell;
use super::*;
use crate::jsonrpc::{self, RpcError};
#[derive(Clone)]
struct TestTransport {
req: Arc<OnceCell<String>>,
res: Arc<Option<String>>,
err: Arc<Option<String>>,
}
impl TestTransport {
// Get the last request as parsed JSON
fn last_request_json(&self) -> Option<Value> {
self.req
.get()
.and_then(|req_str| serde_json::from_str(req_str).ok())
}
}
#[async_trait]
impl Transport for TestTransport {
async fn send(&self, req: String) -> core::result::Result<String, Error> {
// Store the request
let _ = self.req.set(req);
// Check for error first
if let Some(err) = &*self.err {
return Err(Error::Transport(jsonrpc::TransportError::Other(err.into())));
}
// Then check for response
if let Some(res) = &*self.res {
return Ok(res.clone());
}
panic!("TestTransport: neither result nor error is set.");
}
async fn notify(&self, req: String) -> core::result::Result<(), Error> {
// Store the request
let _ = self.req.set(req);
// Check for error
if let Some(err) = &*self.err {
return Err(Error::Transport(jsonrpc::TransportError::Other(err.into())));
}
Ok(())
}
}
#[derive(Default, Clone, Serialize, Deserialize, Debug)]
struct DummyCall {
foo: String,
bar: i32,
}
impl JsonRpcRequest for DummyCall {
const METHOD: &'static str = "dummy_call";
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
struct DummyResponse {
foo: String,
bar: i32,
}
#[tokio::test]
async fn test_typed_call_w_response() {
let req = DummyCall {
foo: String::from("hello world!"),
bar: 13,
};
let expected_res = DummyResponse {
foo: String::from("hello client!"),
bar: 10,
};
let res_obj = expected_res
.clone()
.into_response(String::from("unique-id-123"));
let res_str = serde_json::to_string(&res_obj).unwrap();
let transport = TestTransport {
req: Arc::new(OnceCell::const_new()),
res: Arc::new(Some(res_str)),
err: Arc::new(None),
};
let client_1 = JsonRpcClient::new(transport.clone());
let res = client_1
.call_typed::<_, DummyResponse>(req.clone())
.await
.expect("Should have an OK result");
assert_eq!(res, expected_res);
let transport_req = transport
.last_request_json()
.expect("Transport should have gotten a request");
assert_eq!(
transport_req
.get("jsonrpc")
.and_then(|v| v.as_str())
.unwrap(),
"2.0"
);
assert_eq!(
transport_req
.get("params")
.and_then(|v| v.as_object())
.unwrap(),
serde_json::to_value(&req).unwrap().as_object().unwrap()
);
}
#[tokio::test]
async fn test_typed_call_w_rpc_error() {
let req = DummyCall {
foo: "hello world!".into(),
bar: 13,
};
let err_res = RpcError::custom_error_with_data(
-32099,
"got a custom error",
serde_json::json!({"got": "some"}),
);
let res_obj = err_res.clone().into_response("unique-id-123".into());
let res_str = serde_json::to_string(&res_obj).unwrap();
let transport = TestTransport {
req: Arc::new(OnceCell::const_new()),
res: Arc::new(Some(res_str)),
err: Arc::new(None),
};
let client_1 = JsonRpcClient::new(transport);
let res = client_1
.call_typed::<_, DummyResponse>(req)
.await
.expect_err("Expected error response");
assert!(match res {
Error::Rpc(rpc_error) => {
assert_eq!(rpc_error, err_res);
true
}
_ => false,
});
}
#[tokio::test]
async fn test_typed_call_w_transport_error() {
let req = DummyCall {
foo: "hello world!".into(),
bar: 13,
};
let transport = TestTransport {
req: Arc::new(OnceCell::const_new()),
res: Arc::new(None),
err: Arc::new(Some(String::from("transport error"))),
};
let client_1 = JsonRpcClient::new(transport);
let res = client_1
.call_typed::<_, DummyResponse>(req)
.await
.expect_err("Expected error response");
assert!(match res {
Error::Transport(err) => {
assert_eq!(err.to_string(), "Other error: transport error");
true
}
_ => false,
});
}
}

View File

@@ -1,6 +1,9 @@
pub mod client;
use log::debug;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::{self, Value};
use std::fmt;
use thiserror::Error;
// Constants for JSON-RPC error codes
const PARSE_ERROR: i64 = -32700;
@@ -9,6 +12,32 @@ const METHOD_NOT_FOUND: i64 = -32601;
const INVALID_PARAMS: i64 = -32602;
const INTERNAL_ERROR: i64 = -32603;
#[derive(Error, Debug)]
pub enum Error {
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("RPC error: {0}")]
Rpc(#[from] RpcError),
#[error("Transport error: {0}")]
Transport(#[from] TransportError),
#[error("Other error: {0}")]
Other(String),
}
impl Error {
pub fn other<T: core::fmt::Display>(v: T) -> Self {
return Self::Other(v.to_string());
}
}
#[derive(Error, Debug)]
pub enum TransportError {
#[error("Other error: {0}")]
Other(String),
}
pub type Result<T> = std::result::Result<T, Error>;
/// Trait to convert a struct into a JSON-RPC RequestObject.
pub trait JsonRpcRequest: Serialize {
const METHOD: &'static str;
@@ -42,15 +71,22 @@ where
}
}
fn from_response(resp: ResponseObject<T>) -> Result<T, RpcError> {
fn from_response(resp: ResponseObject<T>) -> Result<T>
where
T: core::fmt::Debug,
{
match (resp.result, resp.error) {
(Some(result), None) => Ok(result),
(None, Some(error)) => Err(error),
_ => Err(RpcError {
code: -32603,
message: "Invalid response format".into(),
data: None,
}),
(None, Some(error)) => Err(Error::Rpc(error)),
_ => {
debug!(
"Invalid JSON-RPC response - missing both result and error fields, or both set: id={}",
resp.id
);
Err(Error::Rpc(RpcError::internal_error(
"not a valid json respone",
)))
}
}
}
}
@@ -143,11 +179,11 @@ where
impl<T> ResponseObject<T>
where
T: DeserializeOwned + Serialize,
T: DeserializeOwned + Serialize + core::fmt::Debug,
{
/// Returns a potential data (result) if the code execution passed else it
/// returns with RPC error, data (error details) if there was
pub fn into_inner(self) -> Result<T, RpcError> {
pub fn into_inner(self) -> Result<T> {
T::from_response(self)
}
}
@@ -422,12 +458,17 @@ mod test_message_serialization {
let response = response_object.into_inner();
let err = response.unwrap_err();
assert_eq!(err.code, -32099);
assert_eq!(err.message, "something bad happened");
assert_eq!(
err.data,
serde_json::from_str("{\"f1\":\"v1\",\"f2\":2}").unwrap()
);
match err {
Error::Rpc(err) => {
assert_eq!(err.code, -32099);
assert_eq!(err.message, "something bad happened");
assert_eq!(
err.data,
serde_json::from_str("{\"f1\":\"v1\",\"f2\":2}").unwrap()
);
}
_ => assert!(false),
}
}
#[test]