mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-13 02:21:14 +00:00
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:
@@ -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
@@ -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
@@ -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" }
|
||||
|
||||
@@ -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
@@ -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 }
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
+26
-46
@@ -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>,
|
||||
+2
-2
@@ -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
-1
@@ -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.
|
||||
|
||||
+2
-2
@@ -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, ¶m)?;
|
||||
serde_json::to_writer(&mut self.0, ¶m).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
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")]
|
||||
@@ -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;
|
||||
@@ -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
@@ -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,2 +1 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
Generated
+1194
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
Generated
+1507
File diff suppressed because it is too large
Load Diff
+12
-19
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
//!
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user