Split RPCs into a separate crate (#1910)

* WIP extract RPCs into separate crate

* fmt

* Fix test

* Remove unused deps

* fix import

* WIP: Fix up errors and most tests. Start extracintg some tests/code to rpc crate

* MockRpcClient sync or async

* MockRpcClient only async but better type inference

* WIP MockRpcClient FnMuts and some test updates to use it

* Get all but one test working with new MockRpcClient

* WIP trying to debug failure

* WIP, Tests mostly fixed, need to add back oen more

* Get mock RPC tests working

* fmt

* fmt

* Clippy and comment tweak

* update CI to explicitly check subxt-rpc features

* clippy

* small tweaks after pass over

* feature flag rename

* update some docs

* Fix some examples

* fmt

* Fix features flags to work with web/wasm32

* Fix unused dep warning

* explicit targets in wasm CI

* Add better crate level docs

* fmt

* Address review comments

* Comment out flaky test for now and make more obvious how similar POlkadot and Substrate configs are

* Not a doc comment

* Remove unused imports
This commit is contained in:
James Wilson
2025-02-18 12:07:00 +00:00
committed by GitHub
parent 333de953ec
commit 816a86423b
50 changed files with 4575 additions and 1186 deletions
+17 -1
View File
@@ -191,13 +191,24 @@ jobs:
cargo check -p subxt-signer --no-default-features --features ecdsa
cargo check -p subxt-signer --no-default-features --features unstable-eth
# Subxt-rpcs has a bunch of clients that can be exposed. Check that they all stand on their own.
- name: Cargo check subxt-rpcs
run: |
cargo check -p subxt-rpcs
cargo check -p subxt-rpcs --no-default-features --features native
cargo check -p subxt-rpcs --no-default-features --features native,subxt
cargo check -p subxt-rpcs --no-default-features --features native,jsonrpsee
cargo check -p subxt-rpcs --no-default-features --features native,reconnecting-rpc-client
cargo check -p subxt-rpcs --no-default-features --features native,mock-rpc-client
cargo check -p subxt-rpcs --no-default-features --features native,unstable-light-client
# We can't enable web features here, so no cargo hack.
- name: Cargo check subxt-lightclient
run: cargo check -p subxt-lightclient
# Next, check each other package in isolation.
- name: Cargo hack; check each feature/crate on its own
run: cargo hack --exclude subxt --exclude subxt-signer --exclude subxt-lightclient --exclude-all-features --each-feature check --workspace
run: cargo hack --exclude subxt --exclude subxt-signer --exclude subxt-lightclient --exclude subxt-rpcs --exclude-all-features --each-feature check --workspace
# Check the parachain-example code, which isn't a part of the workspace so is otherwise ignored.
- name: Cargo check parachain-example
@@ -225,6 +236,11 @@ jobs:
- name: Rust Cache
uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2.7.7
- name: Cargo check web features which require wasm32 target.
run: |
cargo check -p subxt-rpcs --target wasm32-unknown-unknown --no-default-features --features web
cargo check -p subxt-rpcs --target wasm32-unknown-unknown --no-default-features --features web,reconnecting-rpc-client
# Check WASM examples, which aren't a part of the workspace and so are otherwise missed:
- name: Cargo check WASM examples
run: |
Generated
+31 -3
View File
@@ -3976,6 +3976,7 @@ dependencies = [
"subxt",
"subxt-codegen",
"subxt-metadata",
"subxt-rpcs",
"subxt-signer",
"subxt-test-macro",
"syn 2.0.87",
@@ -10551,14 +10552,11 @@ dependencies = [
"bitvec",
"derive-where",
"either",
"finito",
"frame-metadata 18.0.0",
"futures",
"getrandom",
"hex",
"http-body",
"hyper",
"impl-serde 0.5.0",
"jsonrpsee",
"parity-scale-codec",
"polkadot-sdk",
@@ -10574,6 +10572,7 @@ dependencies = [
"subxt-lightclient",
"subxt-macro",
"subxt-metadata",
"subxt-rpcs",
"subxt-signer",
"thiserror 2.0.0",
"tokio",
@@ -10720,6 +10719,35 @@ dependencies = [
"thiserror 2.0.0",
]
[[package]]
name = "subxt-rpcs"
version = "0.39.0"
dependencies = [
"derive-where",
"finito",
"frame-metadata 18.0.0",
"futures",
"getrandom",
"hex",
"http-body",
"hyper",
"impl-serde 0.5.0",
"jsonrpsee",
"parity-scale-codec",
"primitive-types 0.13.1",
"serde",
"serde_json",
"subxt-core",
"subxt-lightclient",
"thiserror 2.0.0",
"tokio",
"tokio-util",
"tower",
"tracing",
"url",
"wasm-bindgen-futures",
]
[[package]]
name = "subxt-signer"
version = "0.39.0"
+6 -1
View File
@@ -12,6 +12,7 @@ members = [
"testing/generate-custom-metadata",
"macro",
"metadata",
"rpcs",
"signer",
"subxt",
"scripts/artifacts",
@@ -82,7 +83,7 @@ frame-metadata = { version = "18.0.0", default-features = false }
futures = { version = "0.3.31", default-features = false, features = ["std"] }
getrandom = { version = "0.2", default-features = false }
hashbrown = "0.14.5"
hex = { version = "0.4.3", default-features = false }
hex = { version = "0.4.3", default-features = false, features = ["alloc"] }
heck = "0.5.0"
impl-serde = { version = "0.5.0", default-features = false }
indoc = "2"
@@ -116,6 +117,9 @@ which = "6.0.3"
strip-ansi-escapes = "0.2.0"
proptest = "1.5.0"
hex-literal = "0.4.1"
tower = "0.4"
hyper = "1"
http-body = "1"
# Light client support:
smoldot = { version = "0.18.0", default-features = false }
@@ -145,6 +149,7 @@ subxt-macro = { version = "0.39.0", path = "macro" }
subxt-metadata = { version = "0.39.0", path = "metadata", default-features = false }
subxt-codegen = { version = "0.39.0", path = "codegen" }
subxt-signer = { version = "0.39.0", path = "signer", default-features = false }
subxt-rpcs = { version = "0.39.0", path = "rpcs", default-features = false }
subxt-lightclient = { version = "0.39.0", path = "lightclient", default-features = false }
subxt-utils-fetchmetadata = { version = "0.39.0", path = "utils/fetch-metadata", default-features = false }
test-runtime = { path = "testing/test-runtime" }
+1
View File
@@ -86,6 +86,7 @@ We also assume that ongoing work done is being merged directly to the `master` b
```
(cd core && cargo publish) && \
(cd rpcs && cargo publish) && \
(cd subxt && cargo publish) && \
(cd signer && cargo publish) && \
(cd cli && cargo publish);
+1 -1
View File
@@ -40,7 +40,7 @@ scale-encode = { workspace = true, default-features = false, features = ["derive
frame-metadata = { workspace = true, default-features = false }
subxt-metadata = { workspace = true, default-features = false }
derive-where = { workspace = true }
hex = { workspace = true, default-features = false, features = ["alloc"] }
hex = { workspace = true }
serde = { workspace = true, default-features = false, features = ["derive"] }
serde_json = { workspace = true, default-features = false, features = ["raw_value", "alloc"] }
tracing = { workspace = true, default-features = false }
+1 -1
View File
@@ -39,7 +39,7 @@ pub trait Config: Sized + Send + Sync + 'static {
type Hash: BlockHash;
/// The account ID type.
type AccountId: Debug + Clone + Encode;
type AccountId: Debug + Clone + Encode + Serialize;
/// The address type.
type Address: Debug + Encode + From<Self::AccountId>;
+8 -2
View File
@@ -19,12 +19,18 @@ pub enum PolkadotConfig {}
impl Config for PolkadotConfig {
type Hash = <SubstrateConfig as Config>::Hash;
type AccountId = <SubstrateConfig as Config>::AccountId;
type Address = MultiAddress<Self::AccountId, ()>;
type Signature = <SubstrateConfig as Config>::Signature;
type Hasher = <SubstrateConfig as Config>::Hasher;
type Header = <SubstrateConfig as Config>::Header;
type AssetId = <SubstrateConfig as Config>::AssetId;
// Address on Polkadot has no account index, whereas it's u32 on
// the default substrate dev node.
type Address = MultiAddress<Self::AccountId, ()>;
// These are the same as the default substrate node, but redefined
// because we need to pass the PolkadotConfig trait as a param.
type ExtrinsicParams = PolkadotExtrinsicParams<Self>;
type AssetId = u32;
}
/// A struct representing the signed extra and additional parameters required
+9
View File
@@ -59,6 +59,15 @@ pub enum LightClientRpcError {
#[error("RPC Error: {0}.")]
pub struct JsonRpcError(Box<RawValue>);
impl JsonRpcError {
/// Attempt to deserialize this error into some type.
pub fn try_deserialize<'a, T: serde::de::Deserialize<'a>>(
&'a self,
) -> Result<T, serde_json::Error> {
serde_json::from_str(self.0.get())
}
}
/// This represents a single light client connection to the network. Instantiate
/// it with [`LightClient::relay_chain()`] to communicate with a relay chain, and
/// then call [`LightClient::parachain()`] to establish connections to parachains.
+98
View File
@@ -0,0 +1,98 @@
[package]
name = "subxt-rpcs"
version.workspace = true
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
publish = true
license.workspace = true
readme = "README.md"
repository.workspace = true
documentation.workspace = true
homepage.workspace = true
description = "Make RPC calls to Substrate based nodes"
keywords = ["parity", "subxt", "rpcs"]
[features]
default = ["jsonrpsee", "native"]
subxt = ["dep:subxt-core"]
jsonrpsee = ["dep:jsonrpsee", "dep:tokio-util"]
unstable-light-client = [
"dep:subxt-lightclient"
]
reconnecting-rpc-client = [
"jsonrpsee",
"dep:finito",
"dep:tokio",
"tokio/sync",
]
mock-rpc-client = [
"dep:tokio",
"tokio/sync",
]
# Enable this for native (ie non web/wasm builds).
# Exactly 1 of "web" and "native" is expected.
native = [
"jsonrpsee?/async-client",
"jsonrpsee?/client-ws-transport-tls",
"jsonrpsee?/ws-client",
"subxt-lightclient?/native",
]
# Enable this for web/wasm builds.
# Exactly 1 of "web" and "native" is expected.
web = [
"jsonrpsee?/async-wasm-client",
"jsonrpsee?/client-web-transport",
"jsonrpsee?/wasm-client",
"subxt-lightclient?/web",
"finito?/wasm-bindgen",
"dep:wasm-bindgen-futures",
"getrandom/js",
]
[dependencies]
codec = { workspace = true }
derive-where = { workspace = true }
futures = { workspace = true }
hex = { workspace = true }
impl-serde = { workspace = true }
primitive-types = { workspace = true, features = ["serde"] }
serde = { workspace = true }
serde_json = { workspace = true, features = ["default", "raw_value"] }
thiserror = { workspace = true }
frame-metadata = { workspace = true, features = ["decode"] }
url = { workspace = true }
tracing = { workspace = true }
getrandom = { workspace = true, optional = true }
# Included with the jsonrpsee feature
jsonrpsee = { workspace = true, optional = true }
tokio-util = { workspace = true, features = ["compat"], optional = true }
# Included with the reconnecting-rpc-client feature
finito = { workspace = true, optional = true }
tokio = { workspace = true, optional = true }
# Included with the lightclient feature
subxt-lightclient = { workspace = true, optional = true, default-features = false }
# Included with the subxt-core feature to impl Config for RpcConfig
subxt-core = { workspace = true, optional = true }
# Included with WASM feature
wasm-bindgen-futures = { workspace = true, optional = true }
[dev-dependencies]
tower = { workspace = true }
hyper = { workspace = true }
http-body = { workspace = true }
[lints]
workspace = true
@@ -1,13 +1,13 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use super::{RawRpcFuture, RawRpcSubscription, RpcClientT};
use crate::error::RpcError;
use crate::Error;
use futures::stream::{StreamExt, TryStreamExt};
use jsonrpsee::{
core::{
client::{Client, ClientT, SubscriptionClientT, SubscriptionKind},
client::{Error as JsonrpseeError, Client, ClientT, SubscriptionClientT, SubscriptionKind},
traits::ToRpcParams,
},
types::SubscriptionId,
@@ -29,9 +29,7 @@ impl RpcClientT for Client {
params: Option<Box<RawValue>>,
) -> RawRpcFuture<'a, Box<RawValue>> {
Box::pin(async move {
let res = ClientT::request(self, method, Params(params))
.await
.map_err(|e| RpcError::ClientError(Box::new(e)))?;
let res = ClientT::request(self, method, Params(params)).await?;
Ok(res)
})
}
@@ -48,9 +46,7 @@ impl RpcClientT for Client {
sub,
Params(params),
unsub,
)
.await
.map_err(|e| RpcError::ClientError(Box::new(e)))?;
).await?;
let id = match stream.kind() {
SubscriptionKind::Subscription(SubscriptionId::Str(id)) => {
@@ -60,9 +56,29 @@ impl RpcClientT for Client {
};
let stream = stream
.map_err(|e| RpcError::ClientError(Box::new(e)))
.map_err(|e| Error::Client(Box::new(e)))
.boxed();
Ok(RawRpcSubscription { stream, id })
})
}
}
// Convert a JsonrpseeError into the RPC error in this crate.
// The main reason for this is to capture user errors so that
// they can be represented/handled without casting.
impl From<JsonrpseeError> for Error {
fn from(error: JsonrpseeError) -> Self {
match error {
JsonrpseeError::Call(e) => {
Error::User(crate::UserError {
code: e.code(),
message: e.message().to_owned(),
data: e.data().map(|d| d.to_owned())
})
},
e => {
Error::Client(Box::new(e))
}
}
}
}
@@ -1,9 +1,9 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use super::{RawRpcFuture, RawRpcSubscription, RpcClientT};
use crate::error::RpcError;
use crate::Error;
use futures::stream::{StreamExt, TryStreamExt};
use serde_json::value::RawValue;
use subxt_lightclient::{LightClientRpc, LightClientRpcError};
@@ -16,8 +16,7 @@ impl RpcClientT for LightClientRpc {
) -> RawRpcFuture<'a, Box<RawValue>> {
Box::pin(async move {
let res = self.request(method.to_owned(), params)
.await
.map_err(lc_err_to_rpc_err)?;
.await?;
Ok(res)
})
@@ -31,12 +30,11 @@ impl RpcClientT for LightClientRpc {
) -> RawRpcFuture<'a, RawRpcSubscription> {
Box::pin(async move {
let sub = self.subscribe(sub.to_owned(), params, unsub.to_owned())
.await
.map_err(lc_err_to_rpc_err)?;
.await?;
let id = Some(sub.id().to_owned());
let stream = sub
.map_err(|e| RpcError::ClientError(Box::new(e)))
.map_err(|e| Error::Client(Box::new(e)))
.boxed();
Ok(RawRpcSubscription { id, stream })
@@ -44,10 +42,21 @@ impl RpcClientT for LightClientRpc {
}
}
fn lc_err_to_rpc_err(err: LightClientRpcError) -> RpcError {
match err {
LightClientRpcError::JsonRpcError(e) => RpcError::ClientError(Box::new(e)),
LightClientRpcError::SmoldotError(e) => RpcError::RequestRejected(e),
LightClientRpcError::BackgroundTaskDropped => RpcError::SubscriptionDropped,
impl From<LightClientRpcError> for Error {
fn from(err: LightClientRpcError) -> Error {
match err {
LightClientRpcError::JsonRpcError(e) => {
// If the error is a typical user error, report it as such, else
// just wrap the error into a ClientError.
let Ok(user_error) = e.try_deserialize() else {
return Error::Client(Box::<CoreError>::from(e))
};
Error::User(user_error)
},
LightClientRpcError::SmoldotError(e) => Error::Client(Box::<CoreError>::from(e)),
LightClientRpcError::BackgroundTaskDropped => Error::Client(Box::<CoreError>::from("Smoldot background task was dropped")),
}
}
}
}
type CoreError = dyn core::error::Error + Send + Sync + 'static;
+633
View File
@@ -0,0 +1,633 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! This module exposes a [`MockRpcClient`], which is useful for testing.
//!
//! # Example
//!
//! ```rust
//! use subxt_rpcs::client::{ RpcClient, MockRpcClient };
//! use subxt_rpcs::client::mock_rpc_client::Json;
//!
//! let mut state = vec![
//! Json(1u8),
//! Json(2u8),
//! Json(3u8),
//! ];
//!
//! // Define a mock client by providing some functions which intercept
//! // method and subscription calls and return some response.
//! let mock_client = MockRpcClient::builder()
//! .method_handler_once("foo", move |params| {
//! // Return each item from our state, and then null afterwards.
//! let val = state.pop();
//! async move { val }
//! })
//! .subscription_handler("bar", |params, unsub| async move {
//! // Arrays, vecs or an RpcSubscription can be returned here to
//! // signal the set of values to be handed back on a subscription.
//! vec![Json(1), Json(2), Json(3)]
//! })
//! .build();
//!
//! // Build an RPC Client that can be used in Subxt or in conjunction with
//! // the RPC methods provided in this crate.
//! let rpc_client = RpcClient::new(mock_client);
//! ```
use super::{RpcClientT, RawRpcFuture, RawRpcSubscription};
use crate::{Error, UserError};
use core::future::Future;
use futures::StreamExt;
use serde_json::value::RawValue;
use std::sync::{Arc, Mutex};
use std::collections::{HashMap, VecDeque};
type MethodHandlerFnOnce = Box<dyn FnOnce(&str, Option<Box<serde_json::value::RawValue>>) -> RawRpcFuture<'static, Box<RawValue>> + Send + Sync + 'static>;
type SubscriptionHandlerFnOnce = Box<dyn FnOnce(&str, Option<Box<serde_json::value::RawValue>>, &str) -> RawRpcFuture<'static, RawRpcSubscription> + Send + Sync + 'static>;
type MethodHandlerFn = Box<dyn FnMut(&str, Option<Box<serde_json::value::RawValue>>) -> RawRpcFuture<'static, Box<RawValue>> + Send + Sync + 'static>;
type SubscriptionHandlerFn = Box<dyn FnMut(&str, Option<Box<serde_json::value::RawValue>>, &str) -> RawRpcFuture<'static, RawRpcSubscription> + Send + Sync + 'static>;
/// A builder to configure and build a new [`MockRpcClient`].
#[derive(Default)]
pub struct MockRpcClientBuilder {
method_handlers_once: HashMap<String, VecDeque<MethodHandlerFnOnce>>,
method_handlers: HashMap<String, MethodHandlerFn>,
method_fallback: Option<MethodHandlerFn>,
subscription_handlers_once: HashMap<String, VecDeque<SubscriptionHandlerFnOnce>>,
subscription_handlers: HashMap<String, SubscriptionHandlerFn>,
subscription_fallback: Option<SubscriptionHandlerFn>
}
impl MockRpcClientBuilder {
/// Add a handler for a specific RPC method. This is called exactly once, and multiple such calls for the same method can be
/// added. Only when any calls registered with this have been used up is the method set by [`Self::method_handler`] called.
pub fn method_handler_once<MethodHandler, MFut, MRes>(mut self, name: impl Into<String>, f: MethodHandler) -> Self
where
MethodHandler: FnOnce(Option<Box<serde_json::value::RawValue>>) -> MFut + Send + Sync + 'static,
MFut: Future<Output = MRes> + Send + 'static,
MRes: IntoHandlerResponse,
{
let handler: MethodHandlerFnOnce = Box::new(move |_method: &str, params: Option<Box<serde_json::value::RawValue>>| {
let fut = f(params);
Box::pin(async move { fut.await.into_handler_response() })
});
self.method_handlers_once.entry(name.into()).or_default().push_back(handler);
self
}
/// Add a handler for a specific RPC method.
pub fn method_handler<MethodHandler, MFut, MRes>(mut self, name: impl Into<String>, mut f: MethodHandler) -> Self
where
MethodHandler: FnMut(Option<Box<serde_json::value::RawValue>>) -> MFut + Send + Sync + 'static,
MFut: Future<Output = MRes> + Send + 'static,
MRes: IntoHandlerResponse,
{
let handler: MethodHandlerFn = Box::new(move |_method: &str, params: Option<Box<serde_json::value::RawValue>>| {
let fut = f(params);
Box::pin(async move { fut.await.into_handler_response() })
});
self.method_handlers.insert(name.into(), handler);
self
}
/// Add a fallback handler to handle any methods not handled by a specific handler.
pub fn method_fallback<MethodHandler, MFut, MRes>(mut self, mut f: MethodHandler) -> Self
where
MethodHandler: FnMut(String, Option<Box<serde_json::value::RawValue>>) -> MFut + Send + Sync + 'static,
MFut: Future<Output = MRes> + Send + 'static,
MRes: IntoHandlerResponse,
{
let handler: MethodHandlerFn = Box::new(move |method: &str, params: Option<Box<serde_json::value::RawValue>>| {
let fut = f(method.to_owned(), params);
Box::pin(async move { fut.await.into_handler_response() })
});
self.method_fallback = Some(handler);
self
}
/// Add a handler for a specific RPC subscription.
pub fn subscription_handler_once<SubscriptionHandler, SFut, SRes>(mut self, name: impl Into<String>, f: SubscriptionHandler) -> Self
where
SubscriptionHandler: FnOnce(Option<Box<serde_json::value::RawValue>>, String) -> SFut + Send + Sync + 'static,
SFut: Future<Output = SRes> + Send + 'static,
SRes: IntoSubscriptionResponse,
{
let handler: SubscriptionHandlerFnOnce = Box::new(move |_sub: &str, params: Option<Box<serde_json::value::RawValue>>, unsub: &str| {
let fut = f(params, unsub.to_owned());
Box::pin(async move { fut.await.into_subscription_response() })
});
self.subscription_handlers_once.entry(name.into()).or_default().push_back(handler);
self
}
/// Add a handler for a specific RPC subscription.
pub fn subscription_handler<SubscriptionHandler, SFut, SRes>(mut self, name: impl Into<String>, mut f: SubscriptionHandler) -> Self
where
SubscriptionHandler: FnMut(Option<Box<serde_json::value::RawValue>>, String) -> SFut + Send + Sync + 'static,
SFut: Future<Output = SRes> + Send + 'static,
SRes: IntoSubscriptionResponse,
{
let handler: SubscriptionHandlerFn = Box::new(move |_sub: &str, params: Option<Box<serde_json::value::RawValue>>, unsub: &str| {
let fut = f(params, unsub.to_owned());
Box::pin(async move { fut.await.into_subscription_response() })
});
self.subscription_handlers.insert(name.into(), handler);
self
}
/// Add a fallback handler to handle any subscriptions not handled by a specific handler.
pub fn subscription_fallback<SubscriptionHandler, SFut, SRes>(mut self, mut f: SubscriptionHandler) -> Self
where
SubscriptionHandler: FnMut(String, Option<Box<serde_json::value::RawValue>>, String) -> SFut + Send + Sync + 'static,
SFut: Future<Output = SRes> + Send + 'static,
SRes: IntoSubscriptionResponse,
{
let handler: SubscriptionHandlerFn = Box::new(move |sub: &str, params: Option<Box<serde_json::value::RawValue>>, unsub: &str| {
let fut = f(sub.to_owned(), params, unsub.to_owned());
Box::pin(async move { fut.await.into_subscription_response() })
});
self.subscription_fallback = Some(handler);
self
}
/// Construct a [`MockRpcClient`] given some state which will be mutably available to each of the handlers.
pub fn build(self) -> MockRpcClient {
MockRpcClient {
method_handlers_once: Arc::new(Mutex::new(self.method_handlers_once)),
method_handlers: Arc::new(Mutex::new(self.method_handlers)),
method_fallback: self.method_fallback.map(|f| Arc::new(Mutex::new(f))),
subscription_handlers_once: Arc::new(Mutex::new(self.subscription_handlers_once)),
subscription_handlers: Arc::new(Mutex::new(self.subscription_handlers)),
subscription_fallback: self.subscription_fallback.map(|f| Arc::new(Mutex::new(f))),
}
}
}
/// A mock RPC client that responds programmatically to requests.
/// Useful for testing.
#[derive(Clone)]
pub struct MockRpcClient {
// These are all accessed for just long enough to call the method. The method
// returns a future, but the method call itself isn't held for long.
method_handlers_once: Arc<Mutex<HashMap<String, VecDeque<MethodHandlerFnOnce>>>>,
method_handlers: Arc<Mutex<HashMap<String, MethodHandlerFn>>>,
method_fallback: Option<Arc<Mutex<MethodHandlerFn>>>,
subscription_handlers_once: Arc<Mutex<HashMap<String, VecDeque<SubscriptionHandlerFnOnce>>>>,
subscription_handlers: Arc<Mutex<HashMap<String, SubscriptionHandlerFn>>>,
subscription_fallback: Option<Arc<Mutex<SubscriptionHandlerFn>>>,
}
impl MockRpcClient {
/// Construct a new [`MockRpcClient`]
pub fn builder() -> MockRpcClientBuilder {
MockRpcClientBuilder::default()
}
}
impl RpcClientT for MockRpcClient {
fn request_raw<'a>(
&'a self,
method: &'a str,
params: Option<Box<serde_json::value::RawValue>>,
) -> RawRpcFuture<'a, Box<serde_json::value::RawValue>> {
// Remove and call a one-time handler if any exist.
let mut handlers_once = self.method_handlers_once.lock().unwrap();
if let Some(handlers) = handlers_once.get_mut(method) {
if let Some(handler) = handlers.pop_front() {
return handler(method, params)
}
}
drop(handlers_once);
// Call a specific handler for the method if one is found.
let mut handlers = self.method_handlers.lock().unwrap();
if let Some(handler) = handlers.get_mut(method) {
return handler(method, params)
}
drop(handlers);
// Call a fallback handler if one exists
if let Some(handler) = &self.method_fallback {
let mut handler = handler.lock().unwrap();
return handler(method, params)
}
// Else, method not found.
Box::pin(async move { Err(UserError::method_not_found().into()) })
}
fn subscribe_raw<'a>(
&'a self,
sub: &'a str,
params: Option<Box<serde_json::value::RawValue>>,
unsub: &'a str,
) -> RawRpcFuture<'a, RawRpcSubscription> {
// Remove and call a one-time handler if any exist.
let mut handlers_once = self.subscription_handlers_once.lock().unwrap();
if let Some(handlers) = handlers_once.get_mut(sub) {
if let Some(handler) = handlers.pop_front() {
return handler(sub, params, unsub)
}
}
drop(handlers_once);
// Call a specific handler for the subscrpition if one is found.
let mut handlers = self.subscription_handlers.lock().unwrap();
if let Some(handler) = handlers.get_mut(sub) {
return handler(sub, params, unsub)
}
drop(handlers);
// Call a fallback handler if one exists
if let Some(handler) = &self.subscription_fallback {
let mut handler = handler.lock().unwrap();
return handler(sub, params, unsub)
}
// Else, method not found.
Box::pin(async move { Err(UserError::method_not_found().into()) })
}
}
/// Return responses wrapped in this to have them serialized to JSON.
pub struct Json<T>(pub T);
impl Json<serde_json::Value> {
/// Create a [`Json<serde_json::Value>`] from some serializable value.
/// Useful when value types are heterogenous.
pub fn value_of<T: serde::Serialize>(item: T) -> Self {
Json(serde_json::to_value(item).expect("item cannot be converted to a serde_json::Value"))
}
}
/// Anything that can be converted into a valid handler response implements this.
pub trait IntoHandlerResponse {
/// Convert self into a handler response.
fn into_handler_response(self) -> Result<Box<RawValue>, Error>;
}
impl <T: IntoHandlerResponse> IntoHandlerResponse for Result<T, Error> {
fn into_handler_response(self) -> Result<Box<RawValue>, Error> {
self.and_then(|val| val.into_handler_response())
}
}
impl <T: IntoHandlerResponse> IntoHandlerResponse for Option<T> {
fn into_handler_response(self) -> Result<Box<RawValue>, Error> {
self.ok_or_else(|| UserError::method_not_found().into())
.and_then(|val| val.into_handler_response())
}
}
impl IntoHandlerResponse for Box<RawValue> {
fn into_handler_response(self) -> Result<Box<RawValue>, Error> {
Ok(self)
}
}
impl IntoHandlerResponse for serde_json::Value {
fn into_handler_response(self) -> Result<Box<RawValue>, Error> {
serialize_to_raw_value(&self)
}
}
impl <T: serde::Serialize> IntoHandlerResponse for Json<T> {
fn into_handler_response(self) -> Result<Box<RawValue>, Error> {
serialize_to_raw_value(&self.0)
}
}
impl IntoHandlerResponse for core::convert::Infallible {
fn into_handler_response(self) -> Result<Box<RawValue>, Error> {
match self {}
}
}
fn serialize_to_raw_value<T: serde::Serialize>(val: &T) -> Result<Box<RawValue>, Error> {
let res = serde_json::to_string(val).map_err(Error::Deserialization)?;
let raw_value = RawValue::from_string(res).map_err(Error::Deserialization)?;
Ok(raw_value)
}
/// Anything that can be a response to a subscription handler implements this.
pub trait IntoSubscriptionResponse {
/// Convert self into a handler response.
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error>;
}
// A tuple of a subscription plus some string is treated as a subscription with that string ID.
impl <T: IntoSubscriptionResponse, S: Into<String>> IntoSubscriptionResponse for (T, S) {
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
self.0
.into_subscription_response()
.map(|mut r| {
r.id = Some(self.1.into());
r
})
}
}
impl <T: IntoHandlerResponse + Send + 'static> IntoSubscriptionResponse for tokio::sync::mpsc::Receiver<T> {
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
struct IntoStream<T>(tokio::sync::mpsc::Receiver<T>);
impl <T> futures::Stream for IntoStream<T> {
type Item = T;
fn poll_next(mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll<Option<Self::Item>> {
self.0.poll_recv(cx)
}
}
Ok(RawRpcSubscription {
stream: Box::pin(IntoStream(self).map(|item| item.into_handler_response())),
id: None,
})
}
}
impl <T: IntoHandlerResponse + Send + 'static> IntoSubscriptionResponse for tokio::sync::mpsc::UnboundedReceiver<T> {
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
struct IntoStream<T>(tokio::sync::mpsc::UnboundedReceiver<T>);
impl <T> futures::Stream for IntoStream<T> {
type Item = T;
fn poll_next(mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll<Option<Self::Item>> {
self.0.poll_recv(cx)
}
}
Ok(RawRpcSubscription {
stream: Box::pin(IntoStream(self).map(|item| item.into_handler_response())),
id: None,
})
}
}
impl IntoSubscriptionResponse for RawRpcSubscription {
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
Ok(self)
}
}
impl <T: IntoSubscriptionResponse> IntoSubscriptionResponse for Result<T, Error> {
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
self.and_then(|res| res.into_subscription_response())
}
}
impl <T: IntoHandlerResponse + Send + 'static> IntoSubscriptionResponse for Vec<T> {
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
let iter = self.into_iter().map(|item| item.into_handler_response());
Ok(RawRpcSubscription {
stream: Box::pin(futures::stream::iter(iter)),
id: None,
})
}
}
impl <T: IntoSubscriptionResponse + Send + 'static> IntoSubscriptionResponse for Option<T> {
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
match self {
Some(sub) => {
sub.into_subscription_response()
},
None => {
Ok(RawRpcSubscription {
stream: Box::pin(futures::stream::empty()),
id: None,
})
}
}
}
}
impl <T: IntoHandlerResponse + Send + 'static, const N: usize> IntoSubscriptionResponse for [T; N] {
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
let iter = self.into_iter().map(|item| item.into_handler_response());
Ok(RawRpcSubscription {
stream: Box::pin(futures::stream::iter(iter)),
id: None,
})
}
}
impl IntoSubscriptionResponse for core::convert::Infallible {
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
match self {}
}
}
/// Send the first items and then the second items back on a subscription;
/// If any one of the responses is an error, we'll return the error.
/// If one response has an ID and the other doesn't, we'll use that ID.
pub struct AndThen<A, B>(pub A, pub B);
impl <A: IntoSubscriptionResponse, B: IntoSubscriptionResponse> IntoSubscriptionResponse for AndThen<A, B> {
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
let a_responses = self.0.into_subscription_response();
let b_responses = self.1.into_subscription_response();
match (a_responses, b_responses) {
(Err(a), _) => {
Err(a)
},
(_, Err(b)) => {
Err(b)
},
(Ok(mut a), Ok(b)) => {
a.stream = Box::pin(a.stream.chain(b.stream));
a.id = a.id.or(b.id);
Ok(a)
}
}
}
}
/// Send back either one response or the other.
pub enum Either<A, B> {
/// The first possibility.
A(A),
/// The second possibility.
B(B)
}
impl <A: IntoHandlerResponse, B: IntoHandlerResponse> IntoHandlerResponse for Either<A, B> {
fn into_handler_response(self) -> Result<Box<RawValue>, Error> {
match self {
Either::A(a) => a.into_handler_response(),
Either::B(b) => b.into_handler_response(),
}
}
}
impl <A: IntoSubscriptionResponse, B: IntoSubscriptionResponse> IntoSubscriptionResponse for Either<A, B> {
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
match self {
Either::A(a) => a.into_subscription_response(),
Either::B(b) => b.into_subscription_response(),
}
}
}
#[cfg(test)]
mod test {
use crate::{RpcClient, rpc_params};
use super::*;
#[tokio::test]
async fn test_method_params() {
let rpc_client = MockRpcClient::builder()
.method_handler("foo", |params| async {
Json(params)
})
.build();
let rpc_client = RpcClient::new(rpc_client);
// We get back whatever params we give
let res: (i32,i32,i32) = rpc_client.request("foo", rpc_params![1, 2, 3]).await.unwrap();
assert_eq!(res, (1,2,3));
let res: (String,) = rpc_client.request("foo", rpc_params!["hello"]).await.unwrap();
assert_eq!(res, ("hello".to_owned(),));
}
#[tokio::test]
async fn test_method_handler_then_fallback() {
let rpc_client = MockRpcClient::builder()
.method_handler("foo", |_params| async {
Json(1)
})
.method_fallback(|name, _params| async {
Json(name)
})
.build();
let rpc_client = RpcClient::new(rpc_client);
// Whenever we call "foo", we get 1 back.
for i in [1,1,1,1] {
let res: i32 = rpc_client.request("foo", rpc_params![]).await.unwrap();
assert_eq!(res, i);
}
// Whenever we call anything else, we get the name of the method back
for name in ["bar", "wibble", "steve"] {
let res: String = rpc_client.request(name, rpc_params![]).await.unwrap();
assert_eq!(res, name);
}
}
#[tokio::test]
async fn test_method_once_then_handler() {
let rpc_client = MockRpcClient::builder()
.method_handler_once("foo", |_params| async {
Json(1)
})
.method_handler("foo", |_params| async {
Json(2)
})
.build();
let rpc_client = RpcClient::new(rpc_client);
// Check that we call the "once" one time and then the second after that.
for i in [1,2,2,2,2] {
let res: i32 = rpc_client.request("foo", rpc_params![]).await.unwrap();
assert_eq!(res, i);
}
}
#[tokio::test]
async fn test_method_once() {
let rpc_client = MockRpcClient::builder()
.method_handler_once("foo", |_params| async {
Json(1)
})
.method_handler_once("foo", |_params| async {
Json(2)
})
.method_handler_once("foo", |_params| async {
Json(3)
})
.build();
let rpc_client = RpcClient::new(rpc_client);
// Check that each method is only called once, in the right order.
for i in [1,2,3] {
let res: i32 = rpc_client.request("foo", rpc_params![]).await.unwrap();
assert_eq!(res, i);
}
// Check that we get a "method not found" error afterwards.
let err = rpc_client.request::<i32>("foo", rpc_params![]).await.unwrap_err();
let not_found_code = UserError::method_not_found().code;
assert!(matches!(err, Error::User(u) if u.code == not_found_code));
}
#[tokio::test]
async fn test_subscription_once_then_handler_then_fallback() {
let rpc_client = MockRpcClient::builder()
.subscription_handler_once("foo", |_params, _unsub| async {
vec![Json(0), Json(0)]
})
.subscription_handler("foo", |_params, _unsub| async {
vec![Json(1), Json(2), Json(3)]
})
.subscription_fallback(|_name, _params, _unsub| async {
vec![Json(4)]
})
.build();
let rpc_client = RpcClient::new(rpc_client);
// "foo" returns 0,0 the first time it's subscribed to
let sub = rpc_client.subscribe::<i32>("foo", rpc_params![], "unsub").await.unwrap();
let res: Vec<i32> = sub.map(|i| i.unwrap()).collect().await;
assert_eq!(res, vec![0,0]);
// then, "foo" returns 1,2,3 in subscription every other time
for _ in 1..5 {
let sub = rpc_client.subscribe::<i32>("foo", rpc_params![], "unsub").await.unwrap();
let res: Vec<i32> = sub.map(|i| i.unwrap()).collect().await;
assert_eq!(res, vec![1,2,3]);
}
// anything else returns 4
let sub = rpc_client.subscribe::<i32>("bar", rpc_params![], "unsub").await.unwrap();
let res: Vec<i32> = sub.map(|i| i.unwrap()).collect().await;
assert_eq!(res, vec![4]);
}
#[tokio::test]
async fn test_subscription_and_then_with_channel() {
let (tx, rx) = tokio::sync::mpsc::channel(10);
let rpc_client = MockRpcClient::builder()
.subscription_handler_once("foo", move |_params, _unsub| async move {
AndThen(
// These should be sent first..
vec![Json(1), Json(2), Json(3)],
// .. and then anything the channel is handing back.
rx
)
})
.build();
let rpc_client = RpcClient::new(rpc_client);
// Send a few values down the channel to be handed back in "foo" subscription:
tokio::spawn(async move {
for i in 4..=6 {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
tx.send(Json(i)).await.unwrap();
}
});
// Expect all values back:
let sub = rpc_client.subscribe::<i32>("foo", rpc_params![], "unsub").await.unwrap();
let res: Vec<i32> = sub.map(|i| i.unwrap()).collect().await;
assert_eq!(res, vec![1,2,3,4,5,6]);
}
}
+52
View File
@@ -0,0 +1,52 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! RPC types and client for interacting with a substrate node.
//!
//! An RPC client is instantiated and then used to create some methods, for instance
//! [`crate::methods::ChainHeadRpcMethods`], which defines the calls that can be made with it.
//! The core RPC client bits are:
//!
//! - [`RpcClientT`] is the underlying dynamic RPC implementation. This provides
//! the low level [`RpcClientT::request_raw`] and [`RpcClientT::subscribe_raw`]
//! methods.
//! - [`RpcClient`] is the higher level wrapper around this, offering
//! the [`RpcClient::request`] and [`RpcClient::subscribe`] methods.
//!
//! We then expose implementations here (depending on which features are enabled)
//! which implement [`RpcClientT`] and can therefore be used to construct [`RpcClient`]s.
//!
//! - **jsonrpsee**: Enable an RPC client based on `jsonrpsee`.
//! - **unstable-light-client**: Enable an RPC client which uses the Smoldot light client under
//! the hood to communicate with the network of choice.
//! - **reconnecting-rpc-client**: Enable an RPC client based on `jsonrpsee` which handles
//! reconnecting automatically in the event of network issues.
//! - **mock-rpc-client**: Enable a mock RPC client that can be used in tests.
//!
crate::macros::cfg_jsonrpsee! {
mod jsonrpsee_impl;
pub use jsonrpsee::core::client::Client as JsonrpseeRpcClient;
}
crate::macros::cfg_unstable_light_client! {
mod lightclient_impl;
pub use subxt_lightclient::LightClientRpc as LightClientRpcClient;
}
crate::macros::cfg_reconnecting_rpc_client! {
pub mod reconnecting_rpc_client;
pub use reconnecting_rpc_client::RpcClient as ReconnectingRpcClient;
}
crate::macros::cfg_mock_rpc_client! {
pub mod mock_rpc_client;
pub use mock_rpc_client::MockRpcClient;
}
mod rpc_client;
mod rpc_client_t;
pub use rpc_client::{rpc_params, RpcClient, RpcParams, RpcSubscription};
pub use rpc_client_t::{RawRpcFuture, RawRpcSubscription, RawValue, RpcClientT};
@@ -1,4 +1,4 @@
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
@@ -10,42 +10,7 @@
//!
//! The logic which action to take for individual calls and subscriptions are
//! handled by the subxt backend implementations.
//!
//! # Example
//!
//! ```no_run
//! use std::time::Duration;
//! use futures::StreamExt;
//! use subxt::backend::rpc::reconnecting_rpc_client::{RpcClient, ExponentialBackoff};
//! use subxt::{OnlineClient, PolkadotConfig};
//!
//! #[tokio::main]
//! async fn main() {
//! let rpc = RpcClient::builder()
//! .retry_policy(ExponentialBackoff::from_millis(100).max_delay(Duration::from_secs(10)))
//! .build("ws://localhost:9944".to_string())
//! .await
//! .unwrap();
//!
//! let subxt_client: OnlineClient<PolkadotConfig> = OnlineClient::from_rpc_client(rpc.clone()).await.unwrap();
//! let mut blocks_sub = subxt_client.blocks().subscribe_finalized().await.unwrap();
//!
//! while let Some(block) = blocks_sub.next().await {
//! let block = match block {
//! Ok(b) => b,
//! Err(e) => {
//! if e.is_disconnected_will_reconnect() {
//! println!("The RPC connection was lost and we may have missed a few blocks");
//! continue;
//! } else {
//! panic!("Error: {}", e);
//! }
//! }
//! };
//! println!("Block #{} ({})", block.number(), block.hash());
//! }
//! }
//! ```
//!
mod platform;
#[cfg(test)]
@@ -60,7 +25,7 @@ use std::{
};
use super::{RawRpcFuture, RawRpcSubscription, RpcClientT};
use crate::error::RpcError as SubxtRpcError;
use crate::Error as SubxtRpcError;
use finito::Retry;
use futures::{FutureExt, Stream, StreamExt, TryStreamExt};
@@ -427,13 +392,7 @@ impl RpcClientT for RpcClient {
async {
self.request(method.to_string(), params)
.await
.map_err(|e| match e {
Error::DisconnectedWillReconnect(e) => {
SubxtRpcError::DisconnectedWillReconnect(e.to_string())
}
Error::Dropped => SubxtRpcError::ClientError(Box::new(e)),
Error::RpcError(e) => SubxtRpcError::ClientError(Box::new(e)),
})
.map_err(error_to_rpc_error)
}
.boxed()
}
@@ -448,7 +407,7 @@ impl RpcClientT for RpcClient {
let sub = self
.subscribe(sub.to_string(), params, unsub.to_string())
.await
.map_err(|e| SubxtRpcError::ClientError(Box::new(e)))?;
.map_err(error_to_rpc_error)?;
let id = match sub.id() {
SubscriptionId::Num(n) => n.to_string(),
@@ -471,6 +430,27 @@ impl RpcClientT for RpcClient {
}
}
/// Convert a reconnecting client Error into the RPC error in this crate.
/// The main reason for this is to capture user errors so that
/// they can be represented/handled without casting.
fn error_to_rpc_error(error: Error) -> SubxtRpcError {
match error {
Error::DisconnectedWillReconnect(reason) => {
SubxtRpcError::DisconnectedWillReconnect(reason.to_string())
},
Error::RpcError(RpcError::Call(e)) => {
SubxtRpcError::User(crate::UserError {
code: e.code(),
message: e.message().to_owned(),
data: e.data().map(|d| d.to_owned())
})
},
e => {
SubxtRpcError::Client(Box::new(e))
}
}
}
async fn background_task<P>(
mut client: Arc<WsClient>,
mut rx: UnboundedReceiver<Op>,
@@ -1,8 +1,8 @@
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use crate::backend::rpc::reconnecting_rpc_client::{RpcClientBuilder, RpcError};
use super::{RpcClientBuilder, RpcError};
use jsonrpsee::core::client::Client;
use std::sync::Arc;
use url::Url;
@@ -1,4 +1,4 @@
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
@@ -1,10 +1,10 @@
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! Utils.
use crate::backend::rpc::reconnecting_rpc_client::RpcError;
use super::RpcError;
pub fn display_close_reason(err: &RpcError) -> String {
match err {
@@ -1,17 +1,16 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use super::{RawRpcSubscription, RpcClientT};
use crate::error::Error;
use crate::Error;
use futures::{Stream, StreamExt};
use serde::{de::DeserializeOwned, Serialize};
use serde_json::value::RawValue;
use std::{pin::Pin, sync::Arc, task::Poll};
/// A concrete wrapper around an [`RpcClientT`] which provides some higher level helper methods,
/// is cheaply cloneable, and can be handed to things like [`crate::client::OnlineClient`] to
/// instantiate it.
/// A concrete wrapper around an [`RpcClientT`] which provides some higher level helper methods
/// and is cheaply cloneable.
#[derive(Clone)]
pub struct RpcClient {
client: Arc<dyn RpcClientT>,
@@ -35,7 +34,7 @@ impl RpcClient {
pub async fn from_insecure_url<U: AsRef<str>>(url: U) -> Result<Self, Error> {
let client = jsonrpsee_helpers::client(url.as_ref())
.await
.map_err(|e| crate::error::RpcError::ClientError(Box::new(e)))?;
.map_err(|e| Error::Client(Box::new(e)))?;
Ok(Self::new(client))
}
@@ -56,7 +55,7 @@ impl RpcClient {
params: RpcParams,
) -> Result<Res, Error> {
let res = self.client.request_raw(method, params.build()).await?;
let val = serde_json::from_str(res.get())?;
let val = serde_json::from_str(res.get()).map_err(Error::Deserialization)?;
Ok(val)
}
@@ -108,7 +107,7 @@ impl std::ops::Deref for RpcClient {
/// # Example
///
/// ```rust
/// use subxt::backend::rpc::{ rpc_params, RpcParams };
/// use subxt_rpcs::client::{ rpc_params, RpcParams };
///
/// // If you provide no params you get `None` back
/// let params: RpcParams = rpc_params![];
@@ -123,7 +122,7 @@ macro_rules! rpc_params {
($($p:expr), *) => {{
// May be unused if empty; no params.
#[allow(unused_mut)]
let mut params = $crate::backend::rpc::RpcParams::new();
let mut params = $crate::client::RpcParams::new();
$(
params.push($p).expect("values passed to rpc_params! must be serializable to JSON");
)*
@@ -140,7 +139,7 @@ pub use rpc_params;
/// # Example
///
/// ```rust
/// use subxt::backend::rpc::RpcParams;
/// use subxt_rpcs::client::RpcParams;
///
/// let mut params = RpcParams::new();
/// params.push(1).unwrap();
@@ -165,7 +164,7 @@ impl RpcParams {
} else {
self.0.push(b',')
}
serde_json::to_writer(&mut self.0, &param)?;
serde_json::to_writer(&mut self.0, &param).map_err(Error::Deserialization)?;
Ok(())
}
/// Build a [`RawValue`] from our params, returning `None` if no parameters
@@ -235,8 +234,9 @@ impl<Res: DeserializeOwned> Stream for RpcSubscription<Res> {
// Decode the inner RawValue to the type we're expecting and map
// any errors to the right shape:
let res = res.map(|r| {
r.map_err(|e| e.into())
.and_then(|raw_val| serde_json::from_str(raw_val.get()).map_err(|e| e.into()))
r.and_then(|raw_val| {
serde_json::from_str(raw_val.get()).map_err(Error::Deserialization)
})
});
Poll::Ready(res)
@@ -1,8 +1,8 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use crate::error::RpcError;
use crate::Error;
use futures::Stream;
use std::{future::Future, pin::Pin};
@@ -10,8 +10,8 @@ use std::{future::Future, pin::Pin};
pub use serde_json::value::RawValue;
/// A trait describing low level JSON-RPC interactions. Implementations of this can be
/// used to instantiate a [`super::RpcClient`], which can be passed to [`crate::OnlineClient`]
/// or used for lower level RPC calls via eg [`crate::backend::legacy::LegacyRpcMethods`].
/// used to instantiate a [`super::RpcClient`], used for lower level RPC calls via eg
/// [`crate::methods::LegacyRpcMethods`] and [`crate::methods::ChainHeadRpcMethods`].
///
/// This is a low level interface whose methods expect an already-serialized set of params,
/// and return an owned but still-serialized [`RawValue`], deferring deserialization to
@@ -54,12 +54,12 @@ pub trait RpcClientT: Send + Sync + 'static {
}
/// A boxed future that is returned from the [`RpcClientT`] methods.
pub type RawRpcFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, RpcError>> + Send + 'a>>;
pub type RawRpcFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, Error>> + Send + 'a>>;
/// The RPC subscription returned from [`RpcClientT`]'s `subscription` method.
pub struct RawRpcSubscription {
/// The subscription stream.
pub stream: Pin<Box<dyn Stream<Item = Result<Box<RawValue>, RpcError>> + Send + 'static>>,
pub stream: Pin<Box<dyn Stream<Item = Result<Box<RawValue>, Error>> + Send + 'static>>,
/// The ID associated with the subscription.
pub id: Option<String>,
}
+147
View File
@@ -0,0 +1,147 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! This crate provides a low level RPC interface to Substrate based nodes.
//!
//! See the [`client`] module for a [`client::RpcClient`] which is driven by implementations
//! of [`client::RpcClientT`] (several of which are provided behind feature flags).
//!
//! See the [`methods`] module for structs which implement sets of concrete RPC calls for
//! communicating with Substrate based nodes. These structs are all driven by a [`client::RpcClient`].
//!
//! The RPC clients/methods here are made use of in `subxt`. Enabling the `subxt` feature flag ensures
//! that all Subxt configurations are also valid RPC configurations.
//!
//! The provided RPC client implementations can be used natively (with the default `native` feature
//! flag) or in WASM based web apps (with the `web` feature flag).
#[cfg(any(
all(feature = "web", feature = "native"),
not(any(feature = "web", feature = "native"))
))]
compile_error!("subxt-rpcs: exactly one of the 'web' and 'native' features should be used.");
mod macros;
pub mod client;
pub mod methods;
pub mod utils;
// Used to enable the js feature for wasm.
#[cfg(feature = "web")]
#[allow(unused_imports)]
pub use getrandom as _;
// Expose the most common things at the top level:
pub use client::{RpcClient, RpcClientT};
pub use methods::{ChainHeadRpcMethods, LegacyRpcMethods};
/// Configuration used by some of the RPC methods to determine the shape of
/// some of the inputs or responses.
pub trait RpcConfig {
/// The block header type.
type Header: Header;
/// The block hash type.
type Hash: BlockHash;
/// The Account ID type.
type AccountId: AccountId;
}
/// A trait which is applied to any type that is a valid block header.
pub trait Header: std::fmt::Debug + codec::Decode + serde::de::DeserializeOwned {}
impl<T> Header for T where T: std::fmt::Debug + codec::Decode + serde::de::DeserializeOwned {}
/// A trait which is applied to any type that is a valid block hash.
pub trait BlockHash: serde::de::DeserializeOwned + serde::Serialize {}
impl<T> BlockHash for T where T: serde::de::DeserializeOwned + serde::Serialize {}
/// A trait which is applied to any type that is a valid Account ID.
pub trait AccountId: serde::Serialize {}
impl<T> AccountId for T where T: serde::Serialize {}
// When the subxt feature is enabled, ensure that any valid `subxt::Config`
// is also a valid `RpcConfig`.
#[cfg(feature = "subxt")]
mod impl_config {
use super::*;
impl<T> RpcConfig for T
where
T: subxt_core::Config,
{
type Header = T::Header;
type Hash = T::Hash;
type AccountId = T::AccountId;
}
}
/// This encapsulates any errors that could be emitted in this crate.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
/// An error which indicates a user fault.
#[error("User error: {0}")]
User(#[from] UserError),
// Dev note: We need the error to be safely sent between threads
// for `subscribe_to_block_headers_filling_in_gaps` and friends.
/// An error coming from the underlying RPC Client.
#[error("RPC error: client error: {0}")]
Client(Box<dyn std::error::Error + Send + Sync + 'static>),
/// The connection was lost and the client will automatically reconnect. Clients
/// should only emit this if they are internally reconnecting, and will buffer any
/// calls made to them in the meantime until the connection is re-established.
#[error("RPC error: the connection was lost ({0}); reconnect automatically initiated")]
DisconnectedWillReconnect(String),
/// Cannot deserialize the response.
#[error("RPC error: cannot deserialize response: {0}")]
Deserialization(serde_json::Error),
/// Cannot SCALE decode some part of the response.
#[error("RPC error: cannot SCALE decode some part of the response: {0}")]
Decode(codec::Error),
/// The requested URL is insecure.
#[error("RPC error: insecure URL: {0}")]
InsecureUrl(String),
}
impl Error {
/// Is the error the `DisconnectedWillReconnect` variant? This should be true
/// only if the underlying `RpcClient` implementation was disconnected and is
/// automatically reconnecting behind the scenes.
pub fn is_disconnected_will_reconnect(&self) -> bool {
matches!(self, Error::DisconnectedWillReconnect(_))
}
}
/// This error should be returned when the user is at fault making a call,
/// for instance because the method name was wrong, parameters invalid or some
/// invariant not upheld. Implementations of [`RpcClientT`] should turn any such
/// errors into this, so that they can be handled appropriately. By contrast,
/// [`Error::Client`] is emitted when the underlying RPC Client implementation
/// has some problem that isn't user specific (eg network issues or similar).
#[derive(Debug, Clone, serde::Deserialize, thiserror::Error)]
#[serde(deny_unknown_fields)]
pub struct UserError {
/// Code
pub code: i32,
/// Message
pub message: String,
/// Optional data
pub data: Option<Box<serde_json::value::RawValue>>,
}
impl UserError {
/// Returns a standard JSON-RPC "method not found" error.
pub fn method_not_found() -> UserError {
UserError {
code: -32601,
message: "Method not found".to_owned(),
data: None,
}
}
}
impl core::fmt::Display for UserError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", &self.message, &self.code)
}
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
macro_rules! cfg_feature {
($feature:literal, $($item:item)*) => {
$(
#[cfg(feature = $feature)]
#[cfg_attr(docsrs, doc(cfg(feature = $feature)))]
$item
)*
}
}
macro_rules! cfg_unstable_light_client {
($($item:item)*) => {
crate::macros::cfg_feature!("unstable-light-client", $($item)*);
};
}
macro_rules! cfg_jsonrpsee {
($($item:item)*) => {
crate::macros::cfg_feature!("jsonrpsee", $($item)*);
};
}
macro_rules! cfg_reconnecting_rpc_client {
($($item:item)*) => {
$(
#[cfg(all(feature = "reconnecting-rpc-client", any(feature = "native", feature = "web")))]
#[cfg_attr(docsrs, doc(cfg(feature = "reconnecting-rpc-client")))]
$item
)*
}
}
macro_rules! cfg_mock_rpc_client {
($($item:item)*) => {
crate::macros::cfg_feature!("mock-rpc-client", $($item)*);
};
}
pub(crate) use {
cfg_feature, cfg_jsonrpsee, cfg_mock_rpc_client, cfg_reconnecting_rpc_client,
cfg_unstable_light_client,
};
@@ -1,4 +1,4 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
@@ -6,9 +6,9 @@
//! <https://github.com/paritytech/json-rpc-interface-spec/> for details of the API
//! methods exposed here.
use crate::backend::rpc::{rpc_params, RpcClient, RpcSubscription};
use crate::config::BlockHash;
use crate::{Config, Error};
use crate::client::{rpc_params, RpcClient, RpcSubscription};
use crate::BlockHash;
use crate::{Error, RpcConfig};
use derive_where::derive_where;
use futures::{Stream, StreamExt};
use serde::{Deserialize, Deserializer, Serialize};
@@ -24,7 +24,7 @@ pub struct ChainHeadRpcMethods<T> {
_marker: std::marker::PhantomData<T>,
}
impl<T: Config> ChainHeadRpcMethods<T> {
impl<T: RpcConfig> ChainHeadRpcMethods<T> {
/// Instantiate the legacy RPC method interface.
pub fn new(client: RpcClient) -> Self {
ChainHeadRpcMethods {
@@ -139,7 +139,8 @@ impl<T: Config> ChainHeadRpcMethods<T> {
let header = header
.map(|h| codec::Decode::decode(&mut &*h.0))
.transpose()?;
.transpose()
.map_err(Error::Decode)?;
Ok(header)
}
@@ -1,14 +1,14 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! An interface to call the raw legacy RPC methods.
use crate::backend::rpc::{rpc_params, RpcClient, RpcSubscription};
use crate::metadata::Metadata;
use crate::{Config, Error};
use crate::client::{rpc_params, RpcClient, RpcSubscription};
use crate::{Error, RpcConfig};
use codec::Decode;
use derive_where::derive_where;
use frame_metadata::RuntimeMetadataPrefixed;
use primitive_types::U256;
use serde::{Deserialize, Serialize};
@@ -21,7 +21,7 @@ pub struct LegacyRpcMethods<T> {
_marker: std::marker::PhantomData<T>,
}
impl<T: Config> LegacyRpcMethods<T> {
impl<T: RpcConfig> LegacyRpcMethods<T> {
/// Instantiate the legacy RPC method interface.
pub fn new(client: RpcClient) -> Self {
LegacyRpcMethods {
@@ -97,17 +97,19 @@ impl<T: Config> LegacyRpcMethods<T> {
let params = rpc_params![block_zero];
let genesis_hash: Option<T::Hash> =
self.client.request("chain_getBlockHash", params).await?;
genesis_hash.ok_or_else(|| "Genesis hash not found".into())
genesis_hash.ok_or_else(|| Error::Client("Genesis hash not found".into()))
}
/// Fetch the metadata via the legacy `state_getMetadata` RPC method.
pub async fn state_get_metadata(&self, at: Option<T::Hash>) -> Result<Metadata, Error> {
pub async fn state_get_metadata(
&self,
at: Option<T::Hash>,
) -> Result<StateGetMetadataResponse, Error> {
let bytes: Bytes = self
.client
.request("state_getMetadata", rpc_params![at])
.await?;
let metadata = Metadata::decode(&mut &bytes[..])?;
Ok(metadata)
Ok(StateGetMetadataResponse(bytes.0))
}
/// Fetch system health
@@ -140,10 +142,7 @@ impl<T: Config> LegacyRpcMethods<T> {
/// Fetch next nonce for an Account
///
/// Return account nonce adjusted for extrinsics currently in transaction pool
pub async fn system_account_next_index(&self, account_id: &T::AccountId) -> Result<u64, Error>
where
T::AccountId: Serialize,
{
pub async fn system_account_next_index(&self, account_id: &T::AccountId) -> Result<u64, Error> {
self.client
.request("system_accountNextIndex", rpc_params![&account_id])
.await
@@ -398,6 +397,22 @@ impl<T: Config> LegacyRpcMethods<T> {
}
}
/// Response from the legacy `state_get_metadata` RPC call.
pub struct StateGetMetadataResponse(Vec<u8>);
impl StateGetMetadataResponse {
/// Return the raw SCALE encoded metadata bytes
pub fn into_raw(self) -> Vec<u8> {
self.0
}
/// Decode and return [`frame_metadata::RuntimeMetadataPrefixed`].
pub fn to_frame_metadata(
&self,
) -> Result<frame_metadata::RuntimeMetadataPrefixed, codec::Error> {
RuntimeMetadataPrefixed::decode(&mut &*self.0)
}
}
/// Storage key.
pub type StorageKey = Vec<u8>;
@@ -426,8 +441,8 @@ pub type BlockNumber = NumberOrHex;
/// The response from `chain_getBlock`
#[derive(Debug, Deserialize)]
#[serde(bound = "T: Config")]
pub struct BlockDetails<T: Config> {
#[serde(bound = "T: RpcConfig")]
pub struct BlockDetails<T: RpcConfig> {
/// The block itself.
pub block: Block<T>,
/// Block justification.
@@ -436,7 +451,7 @@ pub struct BlockDetails<T: Config> {
/// Block details in the [`BlockDetails`].
#[derive(Debug, Deserialize)]
pub struct Block<T: Config> {
pub struct Block<T: RpcConfig> {
/// The block header.
pub header: T::Header,
/// The accompanying extrinsics.
@@ -511,11 +526,19 @@ pub enum TransactionStatus<Hash> {
/// The decoded result returned from calling `system_dryRun` on some extrinsic.
#[derive(Debug, PartialEq, Eq)]
pub enum DryRunResult {
pub enum DryRunResult<'a> {
/// The transaction could be included in the block and executed.
Success,
/// The transaction could be included in the block, but the call failed to dispatch.
DispatchError(crate::error::DispatchError),
/// If Subxt is available, the bytes here can be further decoded by calling:
///
/// ```rust,ignore
/// subxt::error::DispatchError::decode_from(bytes, metadata)?;
/// ```
///
/// Where metadata is an instance of `subxt::Metadata` that is valid for the runtime
/// version which returned this error.
DispatchError(&'a [u8]),
/// The transaction could not be included in the block.
TransactionValidityError,
}
@@ -525,19 +548,16 @@ pub enum DryRunResult {
pub struct DryRunResultBytes(pub Vec<u8>);
impl DryRunResultBytes {
/// Attempt to decode the error bytes into a [`DryRunResult`] using the provided [`Metadata`].
pub fn into_dry_run_result(
self,
metadata: &crate::metadata::Metadata,
) -> Result<DryRunResult, crate::Error> {
/// Attempt to decode the error bytes into a [`DryRunResult`].
pub fn into_dry_run_result(&self) -> Result<DryRunResult<'_>, DryRunDecodeError> {
// dryRun returns an ApplyExtrinsicResult, which is basically a
// `Result<Result<(), DispatchError>, TransactionValidityError>`.
let bytes = self.0;
let bytes = &*self.0;
// We expect at least 2 bytes. In case we got a naff response back (or
// manually constructed this struct), just error to avoid a panic:
if bytes.len() < 2 {
return Err(crate::Error::Unknown(bytes));
return Err(DryRunDecodeError::WrongNumberOfBytes);
}
if bytes[0] == 0 && bytes[1] == 0 {
@@ -545,19 +565,25 @@ impl DryRunResultBytes {
Ok(DryRunResult::Success)
} else if bytes[0] == 0 && bytes[1] == 1 {
// Ok(Err(dispatch_error)); transaction is valid but execution failed
let dispatch_error =
crate::error::DispatchError::decode_from(&bytes[2..], metadata.clone())?;
Ok(DryRunResult::DispatchError(dispatch_error))
Ok(DryRunResult::DispatchError(&bytes[2..]))
} else if bytes[0] == 1 {
// Err(transaction_error); some transaction validity error (we ignore the details at the moment)
Ok(DryRunResult::TransactionValidityError)
} else {
// unable to decode the bytes; they aren't what we expect.
Err(crate::Error::Unknown(bytes))
Err(DryRunDecodeError::InvalidBytes)
}
}
}
/// An error which can be emitted when calling [`DryRunResultBytes::into_dry_run_result`].
pub enum DryRunDecodeError {
/// The dry run result was less than 2 bytes, which is invalid.
WrongNumberOfBytes,
/// The dry run bytes are not valid.
InvalidBytes,
}
/// Storage change set
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
#[serde(rename_all = "camelCase")]
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! RPC methods are defined in this module. At the moment we have:
//!
//! - [`ChainHeadRpcMethods`] (and the types in [`chain_head`]): these methods
//! implement the RPC spec at <https://paritytech.github.io/json-rpc-interface-spec/api/chainHead.html>
//!
//! We also have (although their use is not advised):
//!
//! - [`LegacyRpcMethods`] (and the types in [`legacy`]): a collection of legacy RPCs.
//! These are not well specified and may change in implementations without warning,
//! but for those methods we expose, we make a best effort to work against latest Substrate versions.
pub mod chain_head;
pub mod legacy;
pub use chain_head::ChainHeadRpcMethods;
pub use legacy::LegacyRpcMethods;
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! A couple of utility methods that we make use of.
use crate::Error;
use url::Url;
/// A URL is considered secure if it uses a secure scheme ("https" or "wss") or is referring to localhost.
///
/// Returns an error if the string could not be parsed into a URL.
pub fn url_is_secure(url: &str) -> Result<bool, Error> {
let url = Url::parse(url).map_err(|e| Error::Client(Box::new(e)))?;
let secure_scheme = url.scheme() == "https" || url.scheme() == "wss";
let is_localhost = url.host().is_some_and(|e| match e {
url::Host::Domain(e) => e == "localhost",
url::Host::Ipv4(e) => e.is_loopback(),
url::Host::Ipv6(e) => e.is_loopback(),
});
Ok(secure_scheme || is_localhost)
}
/// Validates, that the given Url is secure ("https" or "wss" scheme) or is referring to localhost.
pub fn validate_url_is_secure(url: &str) -> Result<(), Error> {
if !url_is_secure(url)? {
Err(Error::InsecureUrl(url.into()))
} else {
Ok(())
}
}
+1 -1
View File
@@ -54,7 +54,7 @@ web = ["getrandom/js"]
subxt-core = { workspace = true, optional = true, default-features = false }
secrecy = { workspace = true }
regex = { workspace = true, features = ["unicode"] }
hex = { workspace = true, features = ["alloc"] }
hex = { workspace = true }
cfg-if = { workspace = true }
codec = { package = "parity-scale-codec", workspace = true, features = [
"derive",
-1
View File
@@ -1,2 +1 @@
/target
Cargo.lock
+1194
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -1,2 +1 @@
/target
Cargo.lock
+1507
View File
File diff suppressed because it is too large Load Diff
+12 -19
View File
@@ -25,10 +25,8 @@ default = ["jsonrpsee", "native"]
# Enable this for native (ie non web/wasm builds).
# Exactly 1 of "web" and "native" is expected.
native = [
"jsonrpsee?/async-client",
"jsonrpsee?/client-ws-transport-tls",
"jsonrpsee?/ws-client",
"subxt-lightclient?/native",
"subxt-rpcs/native",
"tokio-util",
"tokio?/sync",
"polkadot-sdk/std",
@@ -37,14 +35,10 @@ native = [
# Enable this for web/wasm builds.
# Exactly 1 of "web" and "native" is expected.
web = [
"jsonrpsee?/async-wasm-client",
"jsonrpsee?/client-web-transport",
"jsonrpsee?/wasm-client",
"getrandom/js",
"subxt-lightclient?/web",
"subxt-macro/web",
"subxt-rpcs/web",
"tokio?/sync",
"finito?/wasm-bindgen",
]
# Feature flag to enable the default future executor.
@@ -56,11 +50,13 @@ web = [
runtime = ["tokio/rt", "wasm-bindgen-futures"]
# Enable this to use the reconnecting rpc client
reconnecting-rpc-client = ["dep:finito", "jsonrpsee"]
reconnecting-rpc-client = ["subxt-rpcs/reconnecting-rpc-client"]
# Enable this to use jsonrpsee (allowing for example `OnlineClient::from_url`).
# Enable this to use jsonrpsee, which enables the jsonrpsee RPC client, and
# a couple of util functions which rely on jsonrpsee.
jsonrpsee = [
"dep:jsonrpsee",
"subxt-rpcs/jsonrpsee",
"runtime"
]
@@ -72,7 +68,7 @@ unstable-metadata = []
# Activate this to expose the Light Client functionality.
# Note that this feature is experimental and things may break or not work as expected.
unstable-light-client = ["subxt-lightclient"]
unstable-light-client = ["subxt-lightclient", "subxt-rpcs/unstable-light-client"]
# Activate this to expose the ability to generate metadata from Wasm runtime files.
runtime-metadata-path = ["subxt-macro/runtime-metadata-path"]
@@ -98,7 +94,6 @@ either = { workspace = true }
web-time = { workspace = true }
# Provides some deserialization, types like U256/H256 and hashing impls like twox/blake256:
impl-serde = { workspace = true }
primitive-types = { workspace = true, features = ["codec", "scale-info", "serde"] }
# Included if the "jsonrpsee" feature is enabled.
@@ -109,13 +104,11 @@ subxt-macro = { workspace = true }
subxt-core = { workspace = true, features = ["std"] }
subxt-metadata = { workspace = true, features = ["std"] }
subxt-lightclient = { workspace = true, optional = true, default-features = false }
subxt-rpcs = { workspace = true, features = ["subxt"] }
# For parsing urls to disallow insecure schemes
url = { workspace = true }
# Included if "web" feature is enabled, to enable its js feature.
getrandom = { workspace = true, optional = true }
# Included if "native" feature is enabled
tokio-util = { workspace = true, features = ["compat"], optional = true }
@@ -123,7 +116,6 @@ tokio-util = { workspace = true, features = ["compat"], optional = true }
# Only the `tokio/sync` is used in the reconnecting rpc client
# and that compiles both for native and web.
tokio = { workspace = true, optional = true }
finito = { workspace = true, optional = true }
wasm-bindgen-futures = { workspace = true, optional = true }
[dev-dependencies]
@@ -134,6 +126,7 @@ tokio = { workspace = true, features = ["macros", "time", "rt-multi-thread", "sy
polkadot-sdk = { workspace = true, features = ["sp-core", "sp-keyring", "sp-runtime", "std"] }
assert_matches = { workspace = true }
subxt-signer = { path = "../signer", features = ["unstable-eth"] }
subxt-rpcs = { workspace = true, features = ["subxt", "mock-rpc-client"] }
# Tracing subscriber is useful for light-client examples to ensure that
# the `bootNodes` and chain spec are configured correctly. If all is fine, then
# the light-client wlll emit INFO logs with
@@ -141,9 +134,9 @@ subxt-signer = { path = "../signer", features = ["unstable-eth"] }
tracing-subscriber = { workspace = true }
# These deps are needed to test the reconnecting rpc client
jsonrpsee = { workspace = true, features = ["server"] }
tower = "0.4"
hyper = "1"
http-body = "1"
tower = { workspace = true }
hyper = { workspace = true }
http-body = { workspace = true }
[[example]]
name = "light_client_basic"
@@ -2,13 +2,13 @@
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use super::rpc_methods::{ChainHeadRpcMethods, FollowEvent};
use crate::config::Config;
use crate::error::Error;
use futures::{FutureExt, Stream, StreamExt};
use futures::{FutureExt, Stream, StreamExt, TryStreamExt};
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use subxt_rpcs::methods::chain_head::{ChainHeadRpcMethods, FollowEvent};
/// A `Stream` whose goal is to remain subscribed to `chainHead_follow`. It will re-subscribe if the subscription
/// is ended for any reason, and it will return the current `subscription_id` as an event, along with the other
@@ -113,8 +113,10 @@ impl<Hash> FollowStream<Hash> {
.to_owned(),
));
};
// Return both:
// Map stream errors into the higher level subxt one:
let stream = stream.map_err(|e| e.into());
let stream: FollowEventStream<T::Hash> = Box::pin(stream);
// Return both:
Ok((stream, sub_id))
})
}),
@@ -215,12 +217,10 @@ impl<Hash> Stream for FollowStream<Hash> {
#[cfg(test)]
pub(super) mod test_utils {
use super::*;
use crate::backend::chain_head::rpc_methods::{
BestBlockChanged, Finalized, Initialized, NewBlock,
};
use crate::config::substrate::H256;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use subxt_rpcs::methods::chain_head::{BestBlockChanged, Finalized, Initialized, NewBlock};
/// Given some events, returns a follow stream getter that we can use in
/// place of the usual RPC method.
@@ -3,7 +3,6 @@
// see LICENSE for license details.
use super::follow_stream_unpin::{BlockRef, FollowStreamMsg, FollowStreamUnpin};
use crate::backend::chain_head::rpc_methods::{FollowEvent, Initialized, RuntimeEvent};
use crate::config::BlockHash;
use crate::error::{Error, RpcError};
use futures::stream::{Stream, StreamExt};
@@ -12,6 +11,7 @@ use std::ops::DerefMut;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};
use subxt_rpcs::methods::chain_head::{FollowEvent, Initialized, RuntimeEvent};
/// A `Stream` which builds on `FollowStreamDriver`, and allows multiple subscribers to obtain events
/// from the single underlying subscription (each being provided an `Initialized` message and all new
@@ -454,8 +454,11 @@ where
.iter()
.position(|b| b.hash() == p.hash())
else {
return Poll::Ready(Some(Err(RpcError::DisconnectedWillReconnect(
"Missed at least one block when the connection was lost".to_owned(),
return Poll::Ready(Some(Err(RpcError::ClientError(
subxt_rpcs::Error::DisconnectedWillReconnect(
"Missed at least one block when the connection was lost"
.to_owned(),
),
)
.into())));
};
@@ -739,7 +742,7 @@ mod test {
)
);
assert!(
matches!(&evs[1], Err(Error::Rpc(RpcError::DisconnectedWillReconnect(e))) if e.contains("Missed at least one block when the connection was lost"))
matches!(&evs[1], Err(Error::Rpc(RpcError::ClientError(subxt_rpcs::Error::DisconnectedWillReconnect(e)))) if e.contains("Missed at least one block when the connection was lost"))
);
assert_eq!(
evs[2].as_ref().unwrap(),
@@ -4,12 +4,12 @@
use super::follow_stream::FollowStream;
use super::ChainHeadRpcMethods;
use crate::backend::chain_head::rpc_methods::{
BestBlockChanged, Finalized, FollowEvent, Initialized, NewBlock,
};
use crate::config::{BlockHash, Config};
use crate::error::Error;
use futures::stream::{FuturesUnordered, Stream, StreamExt};
use subxt_rpcs::methods::chain_head::{
BestBlockChanged, Finalized, FollowEvent, Initialized, NewBlock,
};
use std::collections::{HashMap, HashSet};
use std::future::Future;
+32 -28
View File
@@ -16,15 +16,10 @@ mod follow_stream_driver;
mod follow_stream_unpin;
mod storage_items;
pub mod rpc_methods;
use self::follow_stream_driver::FollowStreamFinalizedHeads;
use self::rpc_methods::{
FollowEvent, MethodResponse, RuntimeEvent, StorageQuery, StorageQueryType, StorageResultType,
};
use crate::backend::{
rpc::RpcClient, utils::retry, Backend, BlockRef, BlockRefT, RuntimeVersion, StorageResponse,
StreamOf, StreamOfResults, TransactionStatus,
utils::retry, Backend, BlockRef, BlockRefT, RuntimeVersion, StorageResponse, StreamOf,
StreamOfResults, TransactionStatus,
};
use crate::config::BlockHash;
use crate::error::{Error, RpcError};
@@ -36,9 +31,18 @@ use futures::{Stream, StreamExt};
use std::collections::HashMap;
use std::task::Poll;
use storage_items::StorageItems;
use subxt_rpcs::methods::chain_head::{
FollowEvent, MethodResponse, RuntimeEvent, StorageQuery, StorageQueryType, StorageResultType,
};
use subxt_rpcs::RpcClient;
/// Re-export RPC types and methods from [`subxt_rpcs::methods::chain_head`].
pub mod rpc_methods {
pub use subxt_rpcs::methods::legacy::*;
}
// Expose the RPC methods.
pub use rpc_methods::ChainHeadRpcMethods;
pub use subxt_rpcs::methods::chain_head::ChainHeadRpcMethods;
/// Configure and build an [`ChainHeadBackend`].
pub struct ChainHeadBackendBuilder<T> {
@@ -213,7 +217,7 @@ impl<T: Config> ChainHeadBackend<T> {
let header = match res {
Ok(header) => header,
Err(e) => return Some(Err(e)),
Err(e) => return Some(Err(e.into())),
};
Some(Ok((header, block_ref.into())))
@@ -338,13 +342,18 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for ChainHeadBackend<T> {
}
async fn genesis_hash(&self) -> Result<T::Hash, Error> {
retry(|| self.methods.chainspec_v1_genesis_hash()).await
retry(|| async {
let genesis_hash = self.methods.chainspec_v1_genesis_hash().await?;
Ok(genesis_hash)
})
.await
}
async fn block_header(&self, at: T::Hash) -> Result<Option<T::Header>, Error> {
retry(|| async {
let sub_id = get_subscription_id(&self.follow_handle).await?;
self.methods.chainhead_v1_header(&sub_id, at).await
let header = self.methods.chainhead_v1_header(&sub_id, at).await?;
Ok(header)
})
.await
}
@@ -357,9 +366,7 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for ChainHeadBackend<T> {
let follow_events = self.follow_handle.subscribe().events();
let status = self.methods.chainhead_v1_body(&sub_id, at).await?;
let operation_id = match status {
MethodResponse::LimitReached => {
return Err(RpcError::request_rejected("limit reached").into())
}
MethodResponse::LimitReached => return Err(RpcError::LimitReached.into()),
MethodResponse::Started(s) => s.operation_id,
};
@@ -653,22 +660,21 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for ChainHeadBackend<T> {
let tx_progress_ev = match tx_progress.poll_next_unpin(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(None) => return Poll::Ready(err_other("No more transaction progress events, but we haven't seen a Finalized one yet")),
Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e.into()))),
Poll::Ready(Some(Ok(ev))) => ev,
};
// When we get one, map it to the correct format (or for finalized ev, wait for the pinned block):
use subxt_rpcs::methods::chain_head::TransactionStatus as RpcTransactionStatus;
let tx_progress_ev = match tx_progress_ev {
rpc_methods::TransactionStatus::Finalized { block } => {
RpcTransactionStatus::Finalized { block } => {
// We'll wait until we have seen this hash, to try to guarantee
// that when we return this event, the corresponding block is
// pinned and accessible.
finalized_hash = Some(block.hash);
continue;
}
rpc_methods::TransactionStatus::BestChainBlockIncluded {
block: Some(block),
} => {
RpcTransactionStatus::BestChainBlockIncluded { block: Some(block) } => {
// Look up a pinned block ref if we can, else return a non-pinned
// block that likely isn't accessible. We have no guarantee that a best
// block on the node a tx was sent to will ever be known about on the
@@ -679,20 +685,20 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for ChainHeadBackend<T> {
};
TransactionStatus::InBestBlock { hash: block_ref }
}
rpc_methods::TransactionStatus::BestChainBlockIncluded { block: None } => {
RpcTransactionStatus::BestChainBlockIncluded { block: None } => {
TransactionStatus::NoLongerInBestBlock
}
rpc_methods::TransactionStatus::Broadcasted => TransactionStatus::Broadcasted,
rpc_methods::TransactionStatus::Dropped { error, .. } => {
RpcTransactionStatus::Broadcasted => TransactionStatus::Broadcasted,
RpcTransactionStatus::Dropped { error, .. } => {
TransactionStatus::Dropped { message: error }
}
rpc_methods::TransactionStatus::Error { error } => {
RpcTransactionStatus::Error { error } => {
TransactionStatus::Error { message: error }
}
rpc_methods::TransactionStatus::Invalid { error } => {
RpcTransactionStatus::Invalid { error } => {
TransactionStatus::Invalid { message: error }
}
rpc_methods::TransactionStatus::Validated => TransactionStatus::Validated,
RpcTransactionStatus::Validated => TransactionStatus::Validated,
};
return Poll::Ready(Some(Ok(tx_progress_ev)));
}
@@ -718,9 +724,7 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for ChainHeadBackend<T> {
.chainhead_v1_call(&sub_id, at, method, call_parameters)
.await?;
let operation_id = match status {
MethodResponse::LimitReached => {
return Err(RpcError::request_rejected("limit reached").into())
}
MethodResponse::LimitReached => return Err(RpcError::LimitReached.into()),
MethodResponse::Started(s) => s.operation_id,
};
+10 -7
View File
@@ -4,9 +4,6 @@
use super::follow_stream_driver::FollowStreamDriverHandle;
use super::follow_stream_unpin::BlockRef;
use super::rpc_methods::{
ChainHeadRpcMethods, FollowEvent, MethodResponse, StorageQuery, StorageResult,
};
use crate::config::Config;
use crate::error::{Error, RpcError};
use futures::{FutureExt, Stream, StreamExt};
@@ -15,6 +12,9 @@ use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use subxt_rpcs::methods::chain_head::{
ChainHeadRpcMethods, FollowEvent, MethodResponse, StorageQuery, StorageResult,
};
/// Obtain a stream of storage items given some query. this handles continuing
/// and stopping under the hood, and returns a stream of `StorageResult`s.
@@ -45,9 +45,7 @@ impl<T: Config> StorageItems<T> {
.chainhead_v1_storage(&sub_id, at, queries, None)
.await?;
let operation_id: Arc<str> = match status {
MethodResponse::LimitReached => {
return Err(RpcError::request_rejected("limit reached").into())
}
MethodResponse::LimitReached => return Err(RpcError::LimitReached.into()),
MethodResponse::Started(s) => s.operation_id.into(),
};
@@ -59,7 +57,12 @@ impl<T: Config> StorageItems<T> {
let operation_id = operation_id.clone();
let methods = methods.clone();
Box::pin(async move { methods.chainhead_v1_continue(&sub_id, &operation_id).await })
Box::pin(async move {
methods
.chainhead_v1_continue(&sub_id, &operation_id)
.await?;
Ok(())
})
})
};
@@ -5,21 +5,25 @@
//! This module exposes a legacy backend implementation, which relies
//! on the legacy RPC API methods.
pub mod rpc_methods;
use self::rpc_methods::TransactionStatus as RpcTransactionStatus;
use crate::backend::utils::{retry, retry_stream};
use crate::backend::{
rpc::RpcClient, Backend, BlockRef, RuntimeVersion, StorageResponse, StreamOf, StreamOfResults,
Backend, BlockRef, RuntimeVersion, StorageResponse, StreamOf, StreamOfResults,
TransactionStatus,
};
use crate::error::RpcError;
use crate::{config::Header, Config, Error};
use async_trait::async_trait;
use futures::TryStreamExt;
use futures::{future, future::Either, stream, Future, FutureExt, Stream, StreamExt};
use std::collections::VecDeque;
use std::pin::Pin;
use std::task::{Context, Poll};
use subxt_rpcs::RpcClient;
/// Re-export legacy RPC types and methods from [`subxt_rpcs::methods::legacy`].
pub mod rpc_methods {
pub use subxt_rpcs::methods::legacy::*;
}
// Expose the RPC methods.
pub use rpc_methods::LegacyRpcMethods;
@@ -181,11 +185,19 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
}
async fn genesis_hash(&self) -> Result<T::Hash, Error> {
retry(|| self.methods.genesis_hash()).await
retry(|| async {
let hash = self.methods.genesis_hash().await?;
Ok(hash)
})
.await
}
async fn block_header(&self, at: T::Hash) -> Result<Option<T::Header>, Error> {
retry(|| self.methods.chain_get_header(Some(at))).await
retry(|| async {
let header = self.methods.chain_get_header(Some(at)).await?;
Ok(header)
})
.await
}
async fn block_body(&self, at: T::Hash) -> Result<Option<Vec<Vec<u8>>>, Error> {
@@ -227,7 +239,7 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
Box::pin(async move {
let sub = methods.state_subscribe_runtime_version().await?;
let sub = sub.map(|r| {
let sub = sub.map_err(|e| e.into()).map(|r| {
r.map(|v| RuntimeVersion {
spec_version: v.spec_version,
transaction_version: v.transaction_version,
@@ -244,8 +256,13 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
// Thus, it's technically possible that a runtime version can be missed if
// two runtime upgrades happen in quick succession, but this is very unlikely.
let stream = retry_sub.filter(|r| {
let forward = !matches!(r, Err(Error::Rpc(RpcError::DisconnectedWillReconnect(_))));
async move { forward }
let mut keep = true;
if let Err(e) = r {
if e.is_disconnected_will_reconnect() {
keep = false;
}
}
async move { keep }
});
Ok(StreamOf(Box::pin(stream)))
@@ -260,7 +277,7 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
let methods = methods.clone();
Box::pin(async move {
let sub = methods.chain_subscribe_all_heads().await?;
let sub = sub.map(|r| {
let sub = sub.map_err(|e| e.into()).map(|r| {
r.map(|h| {
let hash = h.hash();
(h, BlockRef::from_hash(hash))
@@ -283,7 +300,7 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
let methods = methods.clone();
Box::pin(async move {
let sub = methods.chain_subscribe_new_heads().await?;
let sub = sub.map(|r| {
let sub = sub.map_err(|e| e.into()).map(|r| {
r.map(|h| {
let hash = h.hash();
(h, BlockRef::from_hash(hash))
@@ -347,6 +364,7 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
let sub = sub.filter_map(|r| {
let mapped = r
.map_err(|e| e.into())
.map(|tx| {
match tx {
// We ignore these because they don't map nicely to the new API. They don't signal "end states" so this should be fine.
@@ -401,7 +419,14 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
call_parameters: Option<&[u8]>,
at: T::Hash,
) -> Result<Vec<u8>, Error> {
retry(|| self.methods.state_call(method, call_parameters, Some(at))).await
retry(|| async {
let res = self
.methods
.state_call(method, call_parameters, Some(at))
.await?;
Ok(res)
})
.await
}
}
@@ -530,14 +555,15 @@ impl<T: Config> Stream for StorageFetchDescendantKeysStream<T> {
let storage_page_size = this.storage_page_size;
let pagination_start_key = this.pagination_start_key.clone();
let keys_fut = async move {
methods
let keys = methods
.state_get_keys_paged(
&key,
storage_page_size,
pagination_start_key.as_deref(),
Some(at),
)
.await
.await?;
Ok(keys)
};
this.keys_fut = Some(Box::pin(keys_fut));
}
@@ -599,9 +625,13 @@ impl<T: Config> Stream for StorageFetchDescendantValuesStream<T> {
let at = this.keys.at;
let results_fut = async move {
let keys = keys.iter().map(|k| &**k);
let values =
retry(|| methods.state_query_storage_at(keys.clone(), Some(at)))
let values = retry(|| async {
let res = methods
.state_query_storage_at(keys.clone(), Some(at))
.await?;
Ok(res)
})
.await?;
let values: VecDeque<_> = values
.into_iter()
.flat_map(|v| {
+412 -785
View File
File diff suppressed because it is too large Load Diff
-76
View File
@@ -1,76 +0,0 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! RPC types and client for interacting with a substrate node.
//!
//! These are used behind the scenes by Subxt backend implementations, for
//! example [`crate::backend::legacy::LegacyBackend`]. If you need an RPC client,
//! then you can manually instantiate one, and then hand it to Subxt if you'd like
//! to re-use it for the Subxt connection.
//!
//! - [`RpcClientT`] is the underlying dynamic RPC implementation. This provides
//! the low level [`RpcClientT::request_raw`] and [`RpcClientT::subscribe_raw`]
//! methods.
//! - [`RpcClient`] is the higher level wrapper around this, offering
//! the [`RpcClient::request`] and [`RpcClient::subscribe`] methods.
//!
//! # Example
//!
//! Fetching the genesis hash.
//!
//! ```no_run
//! # #[tokio::main]
//! # async fn main() {
//! use subxt::{
//! client::OnlineClient,
//! config::SubstrateConfig,
//! backend::rpc::RpcClient,
//! backend::legacy::LegacyRpcMethods,
//! };
//!
//! // Instantiate a default RPC client pointing at some URL.
//! let rpc_client = RpcClient::from_url("ws://localhost:9944")
//! .await
//! .unwrap();
//!
//! // Instantiate the legacy RPC interface, providing an appropriate
//! // config so that it uses the correct types for your chain.
//! let rpc_methods = LegacyRpcMethods::<SubstrateConfig>::new(rpc_client.clone());
//!
//! // Use it to make RPC calls, here using the legacy genesis_hash method.
//! let genesis_hash = rpc_methods
//! .genesis_hash()
//! .await
//! .unwrap();
//!
//! println!("{genesis_hash}");
//!
//! // Instantiate the Subxt interface using the same client and config if you
//! // want to reuse the same connection:
//! let client = OnlineClient::<SubstrateConfig>::from_rpc_client(rpc_client);
//! # }
//! ```
// Allow an `rpc.rs` file in the `rpc` folder to align better
// with other file names for their types.
#![allow(clippy::module_inception)]
crate::macros::cfg_jsonrpsee! {
mod jsonrpsee_impl;
}
crate::macros::cfg_unstable_light_client! {
mod lightclient_impl;
}
crate::macros::cfg_reconnecting_rpc_client! {
/// reconnecting rpc client.
pub mod reconnecting_rpc_client;
}
mod rpc_client;
mod rpc_client_t;
pub use rpc_client::{rpc_params, RpcClient, RpcParams, RpcSubscription};
pub use rpc_client_t::{RawRpcFuture, RawRpcSubscription, RawValue, RpcClientT};
+13 -9
View File
@@ -118,14 +118,20 @@ where
}
// TODO: https://github.com/paritytech/subxt/issues/1567
// This is a hack because if a reconnection occurs
// the order of pending calls is not guaranteed.
// This is a hack because, in the event of a disconnection,
// we may not get the correct subscription ID back on reconnecting.
//
// Such that it's possible the a pending future completes
// before `chainHead_follow` is established with fresh
// subscription id.
// This is because we have a race between this future and the
// separate chainHead subscription, which runs in a different task.
// if this future is too quick, it'll be given back an old
// subscription ID from the chainHead subscription which has yet
// to reconnect and establish a new subscription ID.
//
if e.is_rejected() && rejected_retries < REJECTED_MAX_RETRIES {
// In the event of a wrong subscription Id being used, we happen to
// hand back an `RpcError::LimitReached`, and so can retry when we
// specifically hit that error to see if we get a new subscription ID
// eventually.
if e.is_rpc_limit_reached() && rejected_retries < REJECTED_MAX_RETRIES {
rejected_retries += 1;
continue;
}
@@ -182,9 +188,7 @@ mod tests {
use crate::backend::StreamOf;
fn disconnect_err() -> Error {
Error::Rpc(crate::error::RpcError::DisconnectedWillReconnect(
String::new(),
))
Error::Rpc(subxt_rpcs::Error::DisconnectedWillReconnect(String::new()).into())
}
fn custom_err() -> Error {
+7 -5
View File
@@ -4,12 +4,14 @@
//! # RPC calls
//!
//! Subxt exposes low level interfaces that can be used to make RPC requests; [`crate::backend::legacy::rpc_methods`]
//! and [`crate::backend::chain_head::rpc_methods`].
//! The RPC interface is provided by the [`subxt_rpcs`] crate but re-exposed here. We have:
//!
//! These interfaces cannot be accessed directly through an [`crate::OnlineClient`]; this is so that the high level
//! Subxt APIs can target either the "legacy" or the more modern "unstable" sets of RPC methods by selecting an appropriate
//! [`crate::backend::Backend`]. It also means that there could exist a backend in the future that doesn't use JSON-RPC at all.
//! - [`crate::backend::rpc::RpcClient`] and [`crate::backend::rpc::RpcClientT`]: the underlying type and trait
//! which provides a basic RPC client.
//! - [`crate::backend::legacy::rpc_methods`] and [`crate::backend::chain_head::rpc_methods`]: RPc methods that
//! can be instantiated with an RPC client.
//!
//! See [`subxt_rpcs`] or [`crate::ext::subxt_rpcs`] for more.
//!
//! # Example
//!
+1 -1
View File
@@ -64,7 +64,7 @@ impl<T: Config> OnlineClient<T> {
/// Construct a new [`OnlineClient`], providing a URL to connect to.
pub async fn from_url(url: impl AsRef<str>) -> Result<OnlineClient<T>, Error> {
crate::utils::validate_url_is_secure(url.as_ref())?;
subxt_rpcs::utils::validate_url_is_secure(url.as_ref())?;
OnlineClient::from_insecure_url(url).await
}
+19 -21
View File
@@ -120,15 +120,26 @@ impl From<scale_decode::visitor::DecodeError> for Error {
}
}
impl From<subxt_rpcs::Error> for Error {
fn from(value: subxt_rpcs::Error) -> Self {
Error::Rpc(value.into())
}
}
impl Error {
/// Checks whether the error was caused by a RPC re-connection.
pub fn is_disconnected_will_reconnect(&self) -> bool {
matches!(self, Error::Rpc(RpcError::DisconnectedWillReconnect(_)))
matches!(
self,
Error::Rpc(RpcError::ClientError(
subxt_rpcs::Error::DisconnectedWillReconnect(_)
))
)
}
/// Checks whether the error was caused by a RPC request being rejected.
pub fn is_rejected(&self) -> bool {
matches!(self, Error::Rpc(RpcError::RequestRejected(_)))
pub fn is_rpc_limit_reached(&self) -> bool {
matches!(self, Error::Rpc(RpcError::LimitReached))
}
}
@@ -141,27 +152,14 @@ pub enum RpcError {
// for `subscribe_to_block_headers_filling_in_gaps` and friends.
/// Error related to the RPC client.
#[error("RPC error: {0}")]
ClientError(Box<dyn std::error::Error + Send + Sync + 'static>),
/// This error signals that the request was rejected for some reason.
/// The specific reason is provided.
#[error("RPC error: request rejected: {0}")]
RequestRejected(String),
ClientError(#[from] subxt_rpcs::Error),
/// This error signals that we got back a [`subxt_rpcs::methods::chain_head::MethodResponse::LimitReached`],
/// which is not technically an RPC error but is treated as an error in our own APIs.
#[error("RPC error: limit reached")]
LimitReached,
/// The RPC subscription dropped.
#[error("RPC error: subscription dropped.")]
SubscriptionDropped,
/// The requested URL is insecure.
#[error("RPC error: insecure URL: {0}")]
InsecureUrl(String),
/// The connection was lost and automatically reconnected.
#[error("RPC error: the connection was lost `{0}`; reconnect automatically initiated")]
DisconnectedWillReconnect(String),
}
impl RpcError {
/// Create a `RequestRejected` error from anything that can be turned into a string.
pub fn request_rejected<S: Into<String>>(s: S) -> RpcError {
RpcError::RequestRejected(s.into())
}
}
/// Block error
+5 -9
View File
@@ -18,6 +18,10 @@
))]
compile_error!("subxt: exactly one of the 'web' and 'native' features should be used.");
// Internal helper macros
#[macro_use]
mod macros;
// The guide is here.
pub mod book;
@@ -34,11 +38,6 @@ mod only_used_in_docs_or_tests {
#[cfg(test)]
use tracing_subscriber as _;
// Used to enable the js feature for wasm.
#[cfg(feature = "web")]
#[allow(unused_imports)]
pub use getrandom as _;
pub mod backend;
pub mod blocks;
pub mod client;
@@ -80,10 +79,6 @@ pub mod dynamic {
};
}
// Internal helper macros
#[macro_use]
mod macros;
// Expose light client bits
cfg_unstable_light_client! {
pub use subxt_lightclient as lightclient;
@@ -108,6 +103,7 @@ pub mod ext {
pub use scale_encode;
pub use scale_value;
pub use subxt_core;
pub use subxt_rpcs;
cfg_jsonrpsee! {
pub use jsonrpsee;
+8 -15
View File
@@ -18,6 +18,12 @@ macro_rules! cfg_unstable_light_client {
};
}
macro_rules! cfg_reconnecting_rpc_client {
($($item:item)*) => {
crate::macros::cfg_feature!("reconnecting-rpc-client", $($item)*);
};
}
macro_rules! cfg_jsonrpsee {
($($item:item)*) => {
crate::macros::cfg_feature!("jsonrpsee", $($item)*);
@@ -46,21 +52,8 @@ macro_rules! cfg_jsonrpsee_web {
}
}
#[allow(unused)]
macro_rules! cfg_reconnecting_rpc_client {
($($item:item)*) => {
$(
#[cfg(all(feature = "reconnecting-rpc-client", any(feature = "native", feature = "web")))]
#[cfg_attr(docsrs, doc(cfg(feature = "reconnecting-rpc-client")))]
$item
)*
}
}
pub(crate) use {
cfg_feature, cfg_jsonrpsee, cfg_reconnecting_rpc_client, cfg_unstable_light_client,
};
pub(crate) use {cfg_feature, cfg_jsonrpsee, cfg_unstable_light_client};
// Only used by light-client.
#[allow(unused)]
pub(crate) use {cfg_jsonrpsee_native, cfg_jsonrpsee_web};
pub(crate) use {cfg_jsonrpsee_native, cfg_jsonrpsee_web, cfg_reconnecting_rpc_client};
+2 -27
View File
@@ -5,8 +5,6 @@
//! Miscellaneous utility helpers.
use crate::macros::cfg_jsonrpsee;
use crate::{error::RpcError, Error};
use url::Url;
pub use subxt_core::utils::{
bits, strip_compact_prefix, to_hex, AccountId32, Encoded, Era, KeyedVec, MultiAddress,
@@ -14,32 +12,9 @@ pub use subxt_core::utils::{
H256, H512,
};
pub use subxt_rpcs::utils::url_is_secure;
cfg_jsonrpsee! {
mod fetch_chain_spec;
pub use fetch_chain_spec::{fetch_chainspec_from_rpc_node, FetchChainspecError};
}
/// A URL is considered secure if it uses a secure scheme ("https" or "wss") or is referring to localhost.
///
/// Returns an error if the string could not be parsed into a URL.
pub fn url_is_secure(url: &str) -> Result<bool, Error> {
let url = Url::parse(url).map_err(|e| Error::Rpc(RpcError::ClientError(Box::new(e))))?;
let secure_scheme = url.scheme() == "https" || url.scheme() == "wss";
let is_localhost = url.host().is_some_and(|e| match e {
url::Host::Domain(e) => e == "localhost",
url::Host::Ipv4(e) => e.is_loopback(),
url::Host::Ipv6(e) => e.is_loopback(),
});
Ok(secure_scheme || is_localhost)
}
/// Validates, that the given Url is secure ("https" or "wss" scheme) or is referring to localhost.
pub fn validate_url_is_secure(url: &str) -> Result<(), Error> {
if !url_is_secure(url)? {
Err(Error::Rpc(crate::error::RpcError::InsecureUrl(url.into())))
} else {
Ok(())
}
}
+1
View File
@@ -40,6 +40,7 @@ subxt = { workspace = true, features = ["unstable-metadata", "native", "jsonrpse
subxt-signer = { workspace = true, features = ["default"] }
subxt-codegen = { workspace = true }
subxt-metadata = { workspace = true }
subxt-rpcs = { workspace = true }
test-runtime = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
@@ -21,8 +21,6 @@ use subxt::{
#[cfg(fullclient)]
use subxt_signer::sr25519::dev;
use subxt_metadata::Metadata;
#[cfg(fullclient)]
#[subxt_test]
async fn block_subscriptions_are_consistent_with_eachother() -> Result<(), subxt::Error> {
@@ -164,14 +162,17 @@ async fn runtime_api_call() -> Result<(), subxt::Error> {
// get metadata via state_call.
let (_, meta1) = rt
.call_raw::<(Compact<u32>, Metadata)>("Metadata_metadata", None)
.call_raw::<(Compact<u32>, frame_metadata::RuntimeMetadataPrefixed)>(
"Metadata_metadata",
None,
)
.await?;
// get metadata via `state_getMetadata`.
let meta2 = rpc.state_get_metadata(Some(block.hash())).await?;
let meta2_bytes = rpc.state_get_metadata(Some(block.hash())).await?.into_raw();
// They should be the same.
assert_eq!(meta1.encode(), meta2.encode());
assert_eq!(meta1.encode(), meta2_bytes);
Ok(())
}
@@ -2,10 +2,8 @@
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use std::collections::HashSet;
use crate::{
subxt_test, test_context, test_context_reconnecting_rpc_client,
subxt_test, test_context,
utils::{node_runtime, wait_for_blocks},
};
use codec::{Decode, Encode};
@@ -412,10 +410,20 @@ async fn partial_fee_estimate_correct() {
assert_eq!(partial_fee_1, partial_fee_2);
}
// This test runs OK locally but fails sporadically in CI eg:
//
// https://github.com/paritytech/subxt/actions/runs/13374953009/job/37353887719?pr=1910#step:7:178
// https://github.com/paritytech/subxt/actions/runs/13385878645/job/37382498200#step:6:163
//
// While those errors were timeouts, I also saw errors like "intersections size is 1".
/*
#[subxt_test(timeout = 300)]
async fn chainhead_block_subscription_reconnect() {
use std::collections::HashSet;
use crate::test_context_reconnecting_rpc_client;
let ctx = test_context_reconnecting_rpc_client().await;
let api = ctx.chainhead_backend().await;
let api = ctx.chainhead_backend().await;ccc
let chainhead_client_blocks = move |num: usize| {
let api = api.clone();
async move {
@@ -428,7 +436,7 @@ async fn chainhead_block_subscription_reconnect() {
let disconnected = match item {
Ok(_) => false,
Err(e) => {
if matches!(e, Error::Rpc(subxt::error::RpcError::DisconnectedWillReconnect(e)) if e.contains("Missed at least one block when the connection was lost")) {
if e.is_disconnected_will_reconnect() && e.to_string().contains("Missed at least one block when the connection was lost") {
missed_blocks = true;
}
e.is_disconnected_will_reconnect()
@@ -463,3 +471,4 @@ async fn chainhead_block_subscription_reconnect() {
assert!(intersection >= 3, "intersections size is {}", intersection);
}
}
*/
@@ -13,14 +13,14 @@ use assert_matches::assert_matches;
use codec::Encode;
use futures::Stream;
use subxt::{
backend::chain_head::rpc_methods::{
FollowEvent, Initialized, MethodResponse, RuntimeEvent, RuntimeVersionEvent, StorageQuery,
StorageQueryType,
},
config::Hasher,
utils::{AccountId32, MultiAddress},
SubstrateConfig,
};
use subxt_rpcs::methods::chain_head::{
FollowEvent, Initialized, MethodResponse, RuntimeEvent, RuntimeVersionEvent, StorageQuery,
StorageQueryType,
};
use subxt_signer::sr25519::dev;
@@ -293,7 +293,8 @@ async fn transactionwatch_v1_submit_and_watch() {
/// Ignore block related events and obtain the next event related to an operation.
async fn next_operation_event<
T: serde::de::DeserializeOwned,
S: Unpin + Stream<Item = Result<FollowEvent<T>, subxt::Error>>,
S: Unpin + Stream<Item = Result<FollowEvent<T>, E>>,
E: core::fmt::Debug,
>(
sub: &mut S,
) -> FollowEvent<T> {