Skip to content

Commit b0f9c9d

Browse files
committed
WIP: Fix up errors and most tests. Start extracintg some tests/code to rpc crate
1 parent 4576c47 commit b0f9c9d

File tree

14 files changed

+290
-49
lines changed

14 files changed

+290
-49
lines changed

lightclient/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ pub enum LightClientRpcError {
5959
#[error("RPC Error: {0}.")]
6060
pub struct JsonRpcError(Box<RawValue>);
6161

62+
impl JsonRpcError {
63+
/// Attempt to deserialize this error into some type.
64+
pub fn try_deserialize<'a, T: serde::de::Deserialize<'a>>(&'a self) -> Result<T, serde_json::Error> {
65+
serde_json::from_str(self.0.get())
66+
}
67+
}
68+
6269
/// This represents a single light client connection to the network. Instantiate
6370
/// it with [`LightClient::relay_chain()`] to communicate with a relay chain, and
6471
/// then call [`LightClient::parachain()`] to establish connections to parachains.

rpcs/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ description = "Make RPC calls to Substrate based nodes"
1515
keywords = ["parity", "subxt", "rpcs"]
1616

1717
[features]
18-
default = ["jsonrpsee", "native"]
18+
default = ["jsonrpsee", "native", "unstable-light-client"]
1919

2020
subxt-core = ["dep:subxt-core"]
2121
jsonrpsee = ["dep:jsonrpsee", "dep:tokio-util"]

rpcs/src/client/jsonrpsee_impl.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::Error;
77
use futures::stream::{StreamExt, TryStreamExt};
88
use jsonrpsee::{
99
core::{
10-
client::{Client, ClientT, SubscriptionClientT, SubscriptionKind},
10+
client::{Error as JsonrpseeError, Client, ClientT, SubscriptionClientT, SubscriptionKind},
1111
traits::ToRpcParams,
1212
},
1313
types::SubscriptionId,
@@ -31,7 +31,7 @@ impl RpcClientT for Client {
3131
Box::pin(async move {
3232
let res = ClientT::request(self, method, Params(params))
3333
.await
34-
.map_err(|e| Error::Client(Box::new(e)))?;
34+
.map_err(error_to_rpc_error)?;
3535
Ok(res)
3636
})
3737
}
@@ -50,7 +50,7 @@ impl RpcClientT for Client {
5050
unsub,
5151
)
5252
.await
53-
.map_err(|e| Error::Client(Box::new(e)))?;
53+
.map_err(error_to_rpc_error)?;
5454

5555
let id = match stream.kind() {
5656
SubscriptionKind::Subscription(SubscriptionId::Str(id)) => {
@@ -66,3 +66,21 @@ impl RpcClientT for Client {
6666
})
6767
}
6868
}
69+
70+
/// Convert a JsonrpseeError into the RPC error in this crate.
71+
/// The main reason for this is to capture user errors so that
72+
/// they can be represented/handled without casting.
73+
fn error_to_rpc_error(error: JsonrpseeError) -> Error {
74+
match error {
75+
JsonrpseeError::Call(e) => {
76+
Error::User(crate::UserError {
77+
code: e.code(),
78+
message: e.message().to_owned(),
79+
data: e.data().map(|d| d.to_owned())
80+
})
81+
},
82+
e => {
83+
Error::Client(Box::new(e))
84+
}
85+
}
86+
}

rpcs/src/client/lightclient_impl.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// see LICENSE for license details.
44

55
use super::{RawRpcFuture, RawRpcSubscription, RpcClientT};
6-
use crate::error::RpcError;
6+
use crate::Error;
77
use futures::stream::{StreamExt, TryStreamExt};
88
use serde_json::value::RawValue;
99
use subxt_lightclient::{LightClientRpc, LightClientRpcError};
@@ -36,18 +36,27 @@ impl RpcClientT for LightClientRpc {
3636

3737
let id = Some(sub.id().to_owned());
3838
let stream = sub
39-
.map_err(|e| RpcError::ClientError(Box::new(e)))
39+
.map_err(|e| Error::Client(Box::new(e)))
4040
.boxed();
4141

4242
Ok(RawRpcSubscription { id, stream })
4343
})
4444
}
4545
}
4646

47-
fn lc_err_to_rpc_err(err: LightClientRpcError) -> RpcError {
47+
fn lc_err_to_rpc_err(err: LightClientRpcError) -> Error {
4848
match err {
49-
LightClientRpcError::JsonRpcError(e) => RpcError::ClientError(Box::new(e)),
50-
LightClientRpcError::SmoldotError(e) => RpcError::ClientError(Box::new(e)),
51-
LightClientRpcError::BackgroundTaskDropped => RpcError::ClientError(Box::new("Smoldot background task was dropped")),
49+
LightClientRpcError::JsonRpcError(e) => {
50+
// If the error is a typical user error, report it as such, else
51+
// just wrap the error into a ClientError.
52+
let Ok(user_error) = e.try_deserialize() else {
53+
return Error::Client(Box::<CoreError>::from(e))
54+
};
55+
Error::User(user_error)
56+
},
57+
LightClientRpcError::SmoldotError(e) => Error::Client(Box::<CoreError>::from(e)),
58+
LightClientRpcError::BackgroundTaskDropped => Error::Client(Box::<CoreError>::from("Smoldot background task was dropped")),
5259
}
53-
}
60+
}
61+
62+
type CoreError = dyn core::error::Error + Send + Sync + 'static;

rpcs/src/client/mock_rpc_client.rs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
2+
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
3+
// see LICENSE for license details.
4+
5+
//! This module exposes a [`MockRpcClient`], which is useful for testing.
6+
7+
use super::{RpcClientT, RawRpcFuture, RawRpcSubscription};
8+
use crate::Error;
9+
use core::future::Future;
10+
use serde_json::value::RawValue;
11+
12+
type MethodHandlerFn = Box<dyn Fn(&str, Option<Box<serde_json::value::RawValue>>) -> RawRpcFuture<'static, Box<RawValue>> + Send + Sync + 'static>;
13+
type SubscriptionHandlerFn = Box<dyn Fn(&str, Option<Box<serde_json::value::RawValue>>, &str) -> RawRpcFuture<'static, RawRpcSubscription> + Send + Sync + 'static>;
14+
15+
/// A mock RPC client that responds programmatically to requests.
16+
/// Useful for testing.
17+
pub struct MockRpcClient {
18+
method_handler: MethodHandlerFn,
19+
subscription_handler: SubscriptionHandlerFn
20+
}
21+
22+
impl MockRpcClient {
23+
/// Create a [`MockRpcClient`] by providing a function to handle method calls
24+
/// and a function to handle subscription calls.
25+
pub fn from_handlers<M, S, MA, SA>(method_handler: M, subscription_handler: S) -> MockRpcClient
26+
where
27+
M: IntoMethodHandler<MA>,
28+
S: IntoSubscriptionHandler<SA>,
29+
{
30+
MockRpcClient {
31+
method_handler: method_handler.into_method_handler(),
32+
subscription_handler: subscription_handler.into_subscription_handler()
33+
}
34+
}
35+
}
36+
37+
impl RpcClientT for MockRpcClient {
38+
fn request_raw<'a>(
39+
&'a self,
40+
method: &'a str,
41+
params: Option<Box<serde_json::value::RawValue>>,
42+
) -> RawRpcFuture<'a, Box<serde_json::value::RawValue>> {
43+
(self.method_handler)(method, params)
44+
}
45+
46+
fn subscribe_raw<'a>(
47+
&'a self,
48+
sub: &'a str,
49+
params: Option<Box<serde_json::value::RawValue>>,
50+
unsub: &'a str,
51+
) -> RawRpcFuture<'a, RawRpcSubscription> {
52+
(self.subscription_handler)(sub, params, unsub)
53+
}
54+
}
55+
56+
/// Return responses wrapped in this to have them serialized to JSON.
57+
pub struct Json<T>(T);
58+
59+
/// Anything that can be converted into a valid handler response implements this.
60+
pub trait IntoHandlerResponse {
61+
/// Convert self into a handler response.
62+
fn into_handler_response(self) -> Result<Box<RawValue>, Error>;
63+
}
64+
65+
impl <T: serde::Serialize> IntoHandlerResponse for Result<T, Error> {
66+
fn into_handler_response(self) -> Result<Box<RawValue>, Error> {
67+
self.and_then(|val| serialize_to_raw_value(&val))
68+
}
69+
}
70+
71+
impl IntoHandlerResponse for Box<RawValue> {
72+
fn into_handler_response(self) -> Result<Box<RawValue>, Error> {
73+
Ok(self)
74+
}
75+
}
76+
77+
impl IntoHandlerResponse for serde_json::Value {
78+
fn into_handler_response(self) -> Result<Box<RawValue>, Error> {
79+
serialize_to_raw_value(&self)
80+
}
81+
}
82+
83+
impl <T: serde::Serialize> IntoHandlerResponse for Json<T> {
84+
fn into_handler_response(self) -> Result<Box<RawValue>, Error> {
85+
serialize_to_raw_value(&self.0)
86+
}
87+
}
88+
89+
fn serialize_to_raw_value<T: serde::Serialize>(val: &T) -> Result<Box<RawValue>, Error> {
90+
let res = serde_json::to_string(val).map_err(Error::Deserialization)?;
91+
let raw_value = RawValue::from_string(res).map_err(Error::Deserialization)?;
92+
Ok(raw_value)
93+
}
94+
95+
/// Anything that is a valid method handler implements this trait.
96+
pub trait IntoMethodHandler<A> {
97+
/// Convert self into a method handler function.
98+
fn into_method_handler(self) -> MethodHandlerFn;
99+
}
100+
101+
enum SyncMethodHandler {}
102+
impl <F, R> IntoMethodHandler<SyncMethodHandler> for F
103+
where
104+
F: Fn(&str, Option<Box<serde_json::value::RawValue>>) -> R + Send + Sync + 'static,
105+
R: IntoHandlerResponse + Send + 'static,
106+
{
107+
fn into_method_handler(self) -> MethodHandlerFn {
108+
Box::new(move |method: &str, params: Option<Box<serde_json::value::RawValue>>| {
109+
let res = self(method, params);
110+
Box::pin(async move { res.into_handler_response() })
111+
})
112+
}
113+
}
114+
115+
enum AsyncMethodHandler {}
116+
impl <F, Fut, R> IntoMethodHandler<AsyncMethodHandler> for F
117+
where
118+
F: Fn(&str, Option<Box<serde_json::value::RawValue>>) -> Fut + Send + Sync + 'static,
119+
Fut: Future<Output = R> + Send + 'static,
120+
R: IntoHandlerResponse + Send + 'static,
121+
{
122+
fn into_method_handler(self) -> MethodHandlerFn {
123+
Box::new(move |method: &str, params: Option<Box<serde_json::value::RawValue>>| {
124+
let fut = self(method, params);
125+
Box::pin(async move { fut.await.into_handler_response() })
126+
})
127+
}
128+
}
129+
130+
/// Anything that is a valid subscription handler implements this trait.
131+
pub trait IntoSubscriptionHandler<A> {
132+
/// Convert self into a subscription handler function.
133+
fn into_subscription_handler(self) -> SubscriptionHandlerFn;
134+
}
135+
136+
enum SyncSubscriptionHandler {}
137+
impl <F, R> IntoMethodHandler<SyncMethodHandler> for F
138+
where
139+
F: Fn(&str, Option<Box<serde_json::value::RawValue>>) -> R + Send + Sync + 'static,
140+
R: IntoHandlerResponse + Send + 'static,
141+
{
142+
fn into_method_handler(self) -> MethodHandlerFn {
143+
Box::new(move |method: &str, params: Option<Box<serde_json::value::RawValue>>| {
144+
let res = self(method, params);
145+
Box::pin(async move { res.into_handler_response() })
146+
})
147+
}
148+
}

rpcs/src/client/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,19 @@ crate::macros::cfg_jsonrpsee! {
6363

6464
crate::macros::cfg_unstable_light_client! {
6565
mod lightclient_impl;
66-
pub use lightclient_impl::LightClientRpc as LightClientRpcClient;
66+
pub use subxt_lightclient::LightClientRpc as LightClientRpcClient;
6767
}
6868

6969
crate::macros::cfg_reconnecting_rpc_client! {
7070
pub mod reconnecting_rpc_client;
7171
pub use reconnecting_rpc_client::RpcClient as ReconnectingRpcClient;
7272
}
7373

74+
#[cfg(test)]
75+
pub mod mock_rpc_client;
76+
#[cfg(test)]
77+
pub use mock_rpc_client::MockRpcClient;
78+
7479
mod rpc_client;
7580
mod rpc_client_t;
7681

rpcs/src/client/reconnecting_rpc_client/mod.rs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -427,13 +427,7 @@ impl RpcClientT for RpcClient {
427427
async {
428428
self.request(method.to_string(), params)
429429
.await
430-
.map_err(|e| match e {
431-
Error::DisconnectedWillReconnect(e) => {
432-
SubxtRpcError::DisconnectedWillReconnect(e.to_string())
433-
}
434-
Error::Dropped => SubxtRpcError::Client(Box::new(e)),
435-
Error::RpcError(e) => SubxtRpcError::Client(Box::new(e)),
436-
})
430+
.map_err(error_to_rpc_error)
437431
}
438432
.boxed()
439433
}
@@ -448,7 +442,7 @@ impl RpcClientT for RpcClient {
448442
let sub = self
449443
.subscribe(sub.to_string(), params, unsub.to_string())
450444
.await
451-
.map_err(|e| SubxtRpcError::Client(Box::new(e)))?;
445+
.map_err(error_to_rpc_error)?;
452446

453447
let id = match sub.id() {
454448
SubscriptionId::Num(n) => n.to_string(),
@@ -471,6 +465,27 @@ impl RpcClientT for RpcClient {
471465
}
472466
}
473467

468+
/// Convert a reconnecting client Error into the RPC error in this crate.
469+
/// The main reason for this is to capture user errors so that
470+
/// they can be represented/handled without casting.
471+
fn error_to_rpc_error(error: Error) -> SubxtRpcError {
472+
match error {
473+
Error::DisconnectedWillReconnect(reason) => {
474+
SubxtRpcError::DisconnectedWillReconnect(reason.to_string())
475+
},
476+
Error::RpcError(RpcError::Call(e)) => {
477+
SubxtRpcError::User(crate::UserError {
478+
code: e.code(),
479+
message: e.message().to_owned(),
480+
data: e.data().map(|d| d.to_owned())
481+
})
482+
},
483+
e => {
484+
SubxtRpcError::Client(Box::new(e))
485+
}
486+
}
487+
}
488+
474489
async fn background_task<P>(
475490
mut client: Arc<WsClient>,
476491
mut rx: UnboundedReceiver<Op>,

rpcs/src/client/rpc_client_t.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub trait RpcClientT: Send + Sync + 'static {
5454
}
5555

5656
/// A boxed future that is returned from the [`RpcClientT`] methods.
57-
pub type RawRpcFuture<'a, T, E = Error> = Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'a>>;
57+
pub type RawRpcFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, Error>> + Send + 'a>>;
5858

5959
/// The RPC subscription returned from [`RpcClientT`]'s `subscription` method.
6060
pub struct RawRpcSubscription {

0 commit comments

Comments
 (0)