feat: Vendor pezkuwi-subxt and pezkuwi-zombienet-sdk into monorepo
- Add pezkuwi-subxt crates to vendor/pezkuwi-subxt - Add pezkuwi-zombienet-sdk crates to vendor/pezkuwi-zombienet-sdk - Convert git dependencies to path dependencies - Add vendor crates to workspace members - Remove test/example crates from vendor (not needed for SDK) - Fix feature propagation issues detected by zepter - Fix workspace inheritance for internal dependencies - All 606 crates now in workspace - All 6919 internal dependency links verified correct - No git dependencies remaining
This commit is contained in:
Vendored
+101
@@ -0,0 +1,101 @@
|
||||
[package]
|
||||
name = "pezkuwi-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 Bizinikiwi based nodes"
|
||||
keywords = ["parity", "rpcs", "subxt"]
|
||||
|
||||
[features]
|
||||
default = ["jsonrpsee", "native"]
|
||||
|
||||
subxt = ["dep:pezkuwi-subxt-core"]
|
||||
jsonrpsee = ["dep:jsonrpsee", "dep:tokio-util"]
|
||||
|
||||
unstable-light-client = ["dep:pezkuwi-subxt-lightclient"]
|
||||
|
||||
reconnecting-rpc-client = [
|
||||
"dep:finito",
|
||||
"dep:tokio",
|
||||
"jsonrpsee",
|
||||
"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",
|
||||
"pezkuwi-subxt-lightclient?/native",
|
||||
]
|
||||
|
||||
# Enable this for web/wasm builds.
|
||||
# Exactly 1 of "web" and "native" is expected.
|
||||
web = [
|
||||
"dep:wasm-bindgen-futures",
|
||||
"finito?/wasm-bindgen",
|
||||
"getrandom/js",
|
||||
"jsonrpsee?/async-wasm-client",
|
||||
"jsonrpsee?/client-web-transport",
|
||||
"jsonrpsee?/wasm-client",
|
||||
"pezkuwi-subxt-lightclient?/web",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
codec = { workspace = true }
|
||||
derive-where = { workspace = true }
|
||||
frame-metadata = { workspace = true, features = ["decode"] }
|
||||
futures = { workspace = true }
|
||||
getrandom = { workspace = true, optional = 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 }
|
||||
tracing = { workspace = true }
|
||||
url = { workspace = 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 unstable-light-client feature
|
||||
pezkuwi-subxt-lightclient = { workspace = true, optional = true, default-features = false }
|
||||
|
||||
# Included with the pezkuwi-subxt-core feature to impl Config for RpcConfig
|
||||
pezkuwi-subxt-core = { workspace = true, optional = true }
|
||||
|
||||
# Included with WASM feature
|
||||
wasm-bindgen-futures = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
http-body = { workspace = true }
|
||||
hyper = { workspace = true }
|
||||
jsonrpsee = { workspace = true, features = ["server"] }
|
||||
tower = { workspace = true }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
default-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
# subxt-rpcs
|
||||
|
||||
This crate provides an interface for interacting with Bizinikiwi nodes via the available RPC methods.
|
||||
|
||||
```rust
|
||||
use subxt_rpcs::{RpcClient, ChainHeadRpcMethods};
|
||||
|
||||
// Connect to a local node:
|
||||
let client = RpcClient::from_url("ws://127.0.0.1:9944").await?;
|
||||
// Use a set of methods, here the V2 "chainHead" ones:
|
||||
let methods = ChainHeadRpcMethods::new(client);
|
||||
|
||||
// Call some RPC methods (in this case a subscription):
|
||||
let mut follow_subscription = methods.chainhead_v1_follow(false).await.unwrap();
|
||||
while let Some(follow_event) = follow_subscription.next().await {
|
||||
// do something with events..
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,137 @@
|
||||
// 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;
|
||||
use futures::stream::{StreamExt, TryStreamExt};
|
||||
use jsonrpsee::{
|
||||
core::{
|
||||
client::{Error as JsonrpseeError, Client, ClientT, SubscriptionClientT, SubscriptionKind},
|
||||
traits::ToRpcParams,
|
||||
},
|
||||
types::SubscriptionId,
|
||||
};
|
||||
use serde_json::value::RawValue;
|
||||
|
||||
/// Construct a `jsonrpsee` RPC client with some sane defaults.
|
||||
pub async fn client(url: &str) -> Result<Client, Error> {
|
||||
jsonrpsee_helpers::client(url).await.map_err(|e| Error::Client(Box::new(e)))
|
||||
}
|
||||
|
||||
struct Params(Option<Box<RawValue>>);
|
||||
|
||||
impl ToRpcParams for Params {
|
||||
fn to_rpc_params(self) -> Result<Option<Box<RawValue>>, serde_json::Error> {
|
||||
Ok(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl RpcClientT for Client {
|
||||
fn request_raw<'a>(
|
||||
&'a self,
|
||||
method: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
) -> RawRpcFuture<'a, Box<RawValue>> {
|
||||
Box::pin(async move {
|
||||
let res = ClientT::request(self, method, Params(params)).await?;
|
||||
Ok(res)
|
||||
})
|
||||
}
|
||||
|
||||
fn subscribe_raw<'a>(
|
||||
&'a self,
|
||||
sub: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
unsub: &'a str,
|
||||
) -> RawRpcFuture<'a, RawRpcSubscription> {
|
||||
Box::pin(async move {
|
||||
let stream = SubscriptionClientT::subscribe::<Box<RawValue>, _>(
|
||||
self,
|
||||
sub,
|
||||
Params(params),
|
||||
unsub,
|
||||
).await?;
|
||||
|
||||
let id = match stream.kind() {
|
||||
SubscriptionKind::Subscription(SubscriptionId::Str(id)) => {
|
||||
Some(id.clone().into_owned())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let stream = stream
|
||||
.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// helpers for a jsonrpsee specific RPC client.
|
||||
#[cfg(all(feature = "jsonrpsee", feature = "native"))]
|
||||
mod jsonrpsee_helpers {
|
||||
pub use jsonrpsee::{
|
||||
client_transport::ws::{self, EitherStream, Url, WsTransportClientBuilder},
|
||||
core::client::{Client, Error},
|
||||
};
|
||||
use tokio_util::compat::Compat;
|
||||
|
||||
pub type Sender = ws::Sender<Compat<EitherStream>>;
|
||||
pub type Receiver = ws::Receiver<Compat<EitherStream>>;
|
||||
|
||||
/// Build WS RPC client from URL
|
||||
pub async fn client(url: &str) -> Result<Client, Error> {
|
||||
let (sender, receiver) = ws_transport(url).await?;
|
||||
Ok(Client::builder()
|
||||
.max_buffer_capacity_per_subscription(4096)
|
||||
.build_with_tokio(sender, receiver))
|
||||
}
|
||||
|
||||
async fn ws_transport(url: &str) -> Result<(Sender, Receiver), Error> {
|
||||
let url = Url::parse(url).map_err(|e| Error::Transport(e.into()))?;
|
||||
WsTransportClientBuilder::default()
|
||||
.build(url)
|
||||
.await
|
||||
.map_err(|e| Error::Transport(e.into()))
|
||||
}
|
||||
}
|
||||
|
||||
// helpers for a jsonrpsee specific RPC client.
|
||||
#[cfg(all(feature = "jsonrpsee", feature = "web", target_arch = "wasm32"))]
|
||||
mod jsonrpsee_helpers {
|
||||
pub use jsonrpsee::{
|
||||
client_transport::web,
|
||||
core::client::{Client, ClientBuilder, Error},
|
||||
};
|
||||
|
||||
/// Build web RPC client from URL
|
||||
pub async fn client(url: &str) -> Result<Client, Error> {
|
||||
let (sender, receiver) = web::connect(url)
|
||||
.await
|
||||
.map_err(|e| Error::Transport(e.into()))?;
|
||||
Ok(ClientBuilder::default()
|
||||
.max_buffer_capacity_per_subscription(4096)
|
||||
.build_with_wasm(sender, receiver))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// 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;
|
||||
use futures::stream::{StreamExt, TryStreamExt};
|
||||
use serde_json::value::RawValue;
|
||||
use pezkuwi_subxt_lightclient::{LightClientRpc, LightClientRpcError};
|
||||
|
||||
impl RpcClientT for LightClientRpc {
|
||||
fn request_raw<'a>(
|
||||
&'a self,
|
||||
method: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
) -> RawRpcFuture<'a, Box<RawValue>> {
|
||||
Box::pin(async move {
|
||||
let res = self.request(method.to_owned(), params)
|
||||
.await?;
|
||||
|
||||
Ok(res)
|
||||
})
|
||||
}
|
||||
|
||||
fn subscribe_raw<'a>(
|
||||
&'a self,
|
||||
sub: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
unsub: &'a str,
|
||||
) -> RawRpcFuture<'a, RawRpcSubscription> {
|
||||
Box::pin(async move {
|
||||
let sub = self.subscribe(sub.to_owned(), params, unsub.to_owned())
|
||||
.await?;
|
||||
|
||||
let id = Some(sub.id().to_owned());
|
||||
let stream = sub
|
||||
.map_err(|e| Error::Client(Box::new(e)))
|
||||
.boxed();
|
||||
|
||||
Ok(RawRpcSubscription { id, stream })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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,632 @@
|
||||
// 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 pezkuwi_subxt_rpcs::client::{ RpcClient, MockRpcClient };
|
||||
//! use pezkuwi_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", async move |params| {
|
||||
//! // Return each item from our state, and then null afterwards.
|
||||
//! state.pop()
|
||||
//! })
|
||||
//! .subscription_handler("bar", async move |params, unsub| {
|
||||
//! // 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 subscriptions 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 heterogeneous.
|
||||
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", async |params| {
|
||||
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", async |_params| {
|
||||
Json(1)
|
||||
})
|
||||
.method_fallback(async |name, _params| {
|
||||
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", async |_params| {
|
||||
Json(1)
|
||||
})
|
||||
.method_handler("foo", async |_params| {
|
||||
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", async |_params| {
|
||||
Json(1)
|
||||
})
|
||||
.method_handler_once("foo", async |_params| {
|
||||
Json(2)
|
||||
})
|
||||
.method_handler_once("foo", async |_params| {
|
||||
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", async |_params, _unsub| {
|
||||
vec![Json(0), Json(0)]
|
||||
})
|
||||
.subscription_handler("foo", async |_params, _unsub| {
|
||||
vec![Json(1), Json(2), Json(3)]
|
||||
})
|
||||
.subscription_fallback(async |_name, _params, _unsub| {
|
||||
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", async move |_params, _unsub| {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
// 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 bizinikiwi 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;
|
||||
pub use jsonrpsee_impl::client as jsonrpsee_client;
|
||||
}
|
||||
|
||||
crate::macros::cfg_unstable_light_client! {
|
||||
mod lightclient_impl;
|
||||
pub use pezkuwi_subxt_lightclient::LightClientRpc as LightClientRpcClient;
|
||||
pub use pezkuwi_subxt_lightclient::LightClient;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
pub mod round_robin_rpc_client;
|
||||
pub use round_robin_rpc_client::RoundRobinRpcClient;
|
||||
|
||||
mod rpc_client;
|
||||
mod rpc_client_t;
|
||||
|
||||
pub use rpc_client::{RpcClient, RpcParams, RpcSubscription, rpc_params};
|
||||
pub use rpc_client_t::{RawRpcFuture, RawRpcSubscription, RawValue, RpcClientT};
|
||||
@@ -0,0 +1,632 @@
|
||||
// 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.
|
||||
|
||||
//! # reconnecting-jsonrpsee-ws-client
|
||||
//!
|
||||
//! A simple reconnecting JSON-RPC WebSocket client for subxt which
|
||||
//! automatically reconnects when the connection is lost but
|
||||
//! it doesn't retain subscriptions and pending method calls when it reconnects.
|
||||
//!
|
||||
//! The logic which action to take for individual calls and subscriptions are
|
||||
//! handled by the subxt backend implementations.
|
||||
//!
|
||||
|
||||
mod platform;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod utils;
|
||||
|
||||
use std::{
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{self, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use super::{RawRpcFuture, RawRpcSubscription, RpcClientT};
|
||||
use crate::Error as SubxtRpcError;
|
||||
|
||||
use finito::Retry;
|
||||
use futures::{FutureExt, Stream, StreamExt, TryStreamExt};
|
||||
use jsonrpsee::core::{
|
||||
client::{
|
||||
Client as WsClient, ClientT, Subscription as RpcSubscription, SubscriptionClientT,
|
||||
SubscriptionKind,
|
||||
},
|
||||
traits::ToRpcParams,
|
||||
};
|
||||
use platform::spawn;
|
||||
use serde_json::value::RawValue;
|
||||
use tokio::sync::{
|
||||
mpsc::{self, UnboundedReceiver, UnboundedSender},
|
||||
oneshot, Notify,
|
||||
};
|
||||
use url::Url;
|
||||
use utils::display_close_reason;
|
||||
|
||||
// re-exports
|
||||
pub use finito::{ExponentialBackoff, FibonacciBackoff, FixedInterval};
|
||||
pub use jsonrpsee::core::client::IdKind;
|
||||
pub use jsonrpsee::{core::client::error::Error as RpcError, rpc_params, types::SubscriptionId};
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
pub use jsonrpsee::ws_client::{HeaderMap, PingConfig};
|
||||
|
||||
const LOG_TARGET: &str = "subxt-reconnecting-rpc-client";
|
||||
|
||||
/// Method result.
|
||||
pub type MethodResult = Result<Box<RawValue>, Error>;
|
||||
/// Subscription result.
|
||||
pub type SubscriptionResult = Result<Box<RawValue>, DisconnectedWillReconnect>;
|
||||
|
||||
/// The connection was closed, reconnect initiated and the subscription was dropped.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("The connection was closed because of `{0:?}` and reconnect initiated")]
|
||||
pub struct DisconnectedWillReconnect(String);
|
||||
|
||||
/// New-type pattern which implements [`ToRpcParams`] that is required by jsonrpsee.
|
||||
#[derive(Debug, Clone)]
|
||||
struct RpcParams(Option<Box<RawValue>>);
|
||||
|
||||
impl ToRpcParams for RpcParams {
|
||||
fn to_rpc_params(self) -> Result<Option<Box<RawValue>>, serde_json::Error> {
|
||||
Ok(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Op {
|
||||
Call {
|
||||
method: String,
|
||||
params: RpcParams,
|
||||
send_back: oneshot::Sender<MethodResult>,
|
||||
},
|
||||
Subscription {
|
||||
subscribe_method: String,
|
||||
params: RpcParams,
|
||||
unsubscribe_method: String,
|
||||
send_back: oneshot::Sender<Result<Subscription, Error>>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Error that can occur when for a RPC call or subscription.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// The client was dropped by the user.
|
||||
#[error("The client was dropped")]
|
||||
Dropped,
|
||||
/// The connection was closed and reconnect initiated.
|
||||
#[error(transparent)]
|
||||
DisconnectedWillReconnect(#[from] DisconnectedWillReconnect),
|
||||
/// Other rpc error.
|
||||
#[error(transparent)]
|
||||
RpcError(RpcError),
|
||||
}
|
||||
|
||||
/// Represent a single subscription.
|
||||
pub struct Subscription {
|
||||
id: SubscriptionId<'static>,
|
||||
stream: mpsc::UnboundedReceiver<SubscriptionResult>,
|
||||
}
|
||||
|
||||
impl Subscription {
|
||||
/// Returns the next notification from the stream.
|
||||
/// This may return `None` if the subscription has been terminated,
|
||||
/// which may happen if the channel becomes full or is dropped.
|
||||
///
|
||||
/// **Note:** This has an identical signature to the [`StreamExt::next`]
|
||||
/// method (and delegates to that). Import [`StreamExt`] if you'd like
|
||||
/// access to other stream combinator methods.
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub async fn next(&mut self) -> Option<SubscriptionResult> {
|
||||
StreamExt::next(self).await
|
||||
}
|
||||
|
||||
/// Get the subscription ID.
|
||||
pub fn id(&self) -> SubscriptionId<'static> {
|
||||
self.id.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for Subscription {
|
||||
type Item = SubscriptionResult;
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut task::Context<'_>,
|
||||
) -> task::Poll<Option<Self::Item>> {
|
||||
match self.stream.poll_recv(cx) {
|
||||
Poll::Ready(Some(msg)) => Poll::Ready(Some(msg)),
|
||||
Poll::Ready(None) => Poll::Ready(None),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Subscription {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Subscription")
|
||||
.field("id", &self.id)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// JSON-RPC client that reconnects automatically and may loose
|
||||
/// subscription notifications when it reconnects.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RpcClient {
|
||||
tx: mpsc::UnboundedSender<Op>,
|
||||
}
|
||||
|
||||
/// Builder for [`Client`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RpcClientBuilder<P> {
|
||||
max_request_size: u32,
|
||||
max_response_size: u32,
|
||||
retry_policy: P,
|
||||
#[cfg(feature = "native")]
|
||||
ping_config: Option<PingConfig>,
|
||||
#[cfg(feature = "native")]
|
||||
// web doesn't support custom headers
|
||||
// https://stackoverflow.com/a/4361358/6394734
|
||||
headers: HeaderMap,
|
||||
max_redirections: u32,
|
||||
id_kind: IdKind,
|
||||
max_log_len: u32,
|
||||
max_concurrent_requests: u32,
|
||||
request_timeout: Duration,
|
||||
connection_timeout: Duration,
|
||||
}
|
||||
|
||||
impl Default for RpcClientBuilder<ExponentialBackoff> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_request_size: 50 * 1024 * 1024,
|
||||
max_response_size: 50 * 1024 * 1024,
|
||||
retry_policy: ExponentialBackoff::from_millis(10).max_delay(Duration::from_secs(60)),
|
||||
#[cfg(feature = "native")]
|
||||
ping_config: Some(PingConfig::new()),
|
||||
#[cfg(feature = "native")]
|
||||
headers: HeaderMap::new(),
|
||||
max_redirections: 5,
|
||||
id_kind: IdKind::Number,
|
||||
max_log_len: 1024,
|
||||
max_concurrent_requests: 1024,
|
||||
request_timeout: Duration::from_secs(60),
|
||||
connection_timeout: Duration::from_secs(10),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RpcClientBuilder<ExponentialBackoff> {
|
||||
/// Create a new builder.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> RpcClientBuilder<P>
|
||||
where
|
||||
P: Iterator<Item = Duration> + Send + Sync + 'static + Clone,
|
||||
{
|
||||
/// Configure the max request size a for websocket message.
|
||||
///
|
||||
/// Default: 50MB
|
||||
pub fn max_request_size(mut self, max: u32) -> Self {
|
||||
self.max_request_size = max;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure the max response size a for websocket message.
|
||||
///
|
||||
/// Default: 50MB
|
||||
pub fn max_response_size(mut self, max: u32) -> Self {
|
||||
self.max_response_size = max;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the max number of redirections to perform until a connection is regarded as failed.
|
||||
///
|
||||
/// Default: 5
|
||||
pub fn max_redirections(mut self, redirect: u32) -> Self {
|
||||
self.max_redirections = redirect;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure how many concurrent method calls are allowed.
|
||||
///
|
||||
/// Default: 1024
|
||||
pub fn max_concurrent_requests(mut self, max: u32) -> Self {
|
||||
self.max_concurrent_requests = max;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure how long until a method call is regarded as failed.
|
||||
///
|
||||
/// Default: 1 minute
|
||||
pub fn request_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.request_timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set connection timeout for the WebSocket handshake
|
||||
///
|
||||
/// Default: 10 seconds
|
||||
pub fn connection_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.connection_timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure the data type of the request object ID
|
||||
///
|
||||
/// Default: number
|
||||
pub fn id_format(mut self, kind: IdKind) -> Self {
|
||||
self.id_kind = kind;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set maximum length for logging calls and responses.
|
||||
/// Logs bigger than this limit will be truncated.
|
||||
///
|
||||
/// Default: 1024
|
||||
pub fn set_max_logging_length(mut self, max: u32) -> Self {
|
||||
self.max_log_len = max;
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native")))]
|
||||
/// Configure custom headers to use in the WebSocket handshake.
|
||||
pub fn set_headers(mut self, headers: HeaderMap) -> Self {
|
||||
self.headers = headers;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure which retry policy to use when a connection is lost.
|
||||
///
|
||||
/// Default: Exponential backoff 10ms
|
||||
pub fn retry_policy<T>(self, retry_policy: T) -> RpcClientBuilder<T> {
|
||||
RpcClientBuilder {
|
||||
max_request_size: self.max_request_size,
|
||||
max_response_size: self.max_response_size,
|
||||
retry_policy,
|
||||
#[cfg(feature = "native")]
|
||||
ping_config: self.ping_config,
|
||||
#[cfg(feature = "native")]
|
||||
headers: self.headers,
|
||||
max_redirections: self.max_redirections,
|
||||
max_log_len: self.max_log_len,
|
||||
id_kind: self.id_kind,
|
||||
max_concurrent_requests: self.max_concurrent_requests,
|
||||
request_timeout: self.request_timeout,
|
||||
connection_timeout: self.connection_timeout,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native")))]
|
||||
/// Configure the WebSocket ping/pong interval.
|
||||
///
|
||||
/// Default: 30 seconds.
|
||||
pub fn enable_ws_ping(mut self, ping_config: PingConfig) -> Self {
|
||||
self.ping_config = Some(ping_config);
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native")))]
|
||||
/// Disable WebSocket ping/pongs.
|
||||
///
|
||||
/// Default: 30 seconds.
|
||||
pub fn disable_ws_ping(mut self) -> Self {
|
||||
self.ping_config = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build and connect to the target.
|
||||
pub async fn build(self, url: impl AsRef<str>) -> Result<RpcClient, RpcError> {
|
||||
let url = Url::parse(url.as_ref()).map_err(|e| RpcError::Transport(Box::new(e)))?;
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
let client = Retry::new(self.retry_policy.clone(), || {
|
||||
platform::ws_client(&url, &self)
|
||||
})
|
||||
.await?;
|
||||
|
||||
platform::spawn(background_task(client, rx, url, self));
|
||||
|
||||
Ok(RpcClient { tx })
|
||||
}
|
||||
}
|
||||
|
||||
impl RpcClient {
|
||||
/// Create a builder.
|
||||
pub fn builder() -> RpcClientBuilder<ExponentialBackoff> {
|
||||
RpcClientBuilder::new()
|
||||
}
|
||||
|
||||
/// Perform a JSON-RPC method call.
|
||||
pub async fn request(
|
||||
&self,
|
||||
method: String,
|
||||
params: Option<Box<RawValue>>,
|
||||
) -> Result<Box<RawValue>, Error> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.tx
|
||||
.send(Op::Call {
|
||||
method,
|
||||
params: RpcParams(params),
|
||||
send_back: tx,
|
||||
})
|
||||
.map_err(|_| Error::Dropped)?;
|
||||
|
||||
rx.await.map_err(|_| Error::Dropped)?
|
||||
}
|
||||
|
||||
/// Perform a JSON-RPC subscription.
|
||||
pub async fn subscribe(
|
||||
&self,
|
||||
subscribe_method: String,
|
||||
params: Option<Box<RawValue>>,
|
||||
unsubscribe_method: String,
|
||||
) -> Result<Subscription, Error> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.tx
|
||||
.send(Op::Subscription {
|
||||
subscribe_method,
|
||||
params: RpcParams(params),
|
||||
unsubscribe_method,
|
||||
send_back: tx,
|
||||
})
|
||||
.map_err(|_| Error::Dropped)?;
|
||||
rx.await.map_err(|_| Error::Dropped)?
|
||||
}
|
||||
}
|
||||
|
||||
impl RpcClientT for RpcClient {
|
||||
fn request_raw<'a>(
|
||||
&'a self,
|
||||
method: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
) -> RawRpcFuture<'a, Box<RawValue>> {
|
||||
async {
|
||||
self.request(method.to_string(), params)
|
||||
.await
|
||||
.map_err(error_to_rpc_error)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn subscribe_raw<'a>(
|
||||
&'a self,
|
||||
sub: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
unsub: &'a str,
|
||||
) -> RawRpcFuture<'a, RawRpcSubscription> {
|
||||
async {
|
||||
let sub = self
|
||||
.subscribe(sub.to_string(), params, unsub.to_string())
|
||||
.await
|
||||
.map_err(error_to_rpc_error)?;
|
||||
|
||||
let id = match sub.id() {
|
||||
SubscriptionId::Num(n) => n.to_string(),
|
||||
SubscriptionId::Str(s) => s.to_string(),
|
||||
};
|
||||
let stream = sub
|
||||
// NOTE: The stream emits only one error `DisconnectWillReconnect if the connection was lost
|
||||
// and safe to wrap it in a `SubxtRpcError::DisconnectWillReconnect` here
|
||||
.map_err(|e: DisconnectedWillReconnect| {
|
||||
SubxtRpcError::DisconnectedWillReconnect(e.to_string())
|
||||
})
|
||||
.boxed();
|
||||
|
||||
Ok(RawRpcSubscription {
|
||||
stream,
|
||||
id: Some(id),
|
||||
})
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
url: Url,
|
||||
client_builder: RpcClientBuilder<P>,
|
||||
) where
|
||||
P: Iterator<Item = Duration> + Send + 'static + Clone,
|
||||
{
|
||||
let disconnect = Arc::new(tokio::sync::Notify::new());
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// An incoming JSON-RPC call to dispatch.
|
||||
next_message = rx.recv() => {
|
||||
match next_message {
|
||||
None => break,
|
||||
Some(op) => {
|
||||
spawn(dispatch_call(client.clone(), op, disconnect.clone()));
|
||||
}
|
||||
};
|
||||
}
|
||||
// The connection was terminated and try to reconnect.
|
||||
_ = client.on_disconnect() => {
|
||||
let params = ReconnectParams {
|
||||
url: &url,
|
||||
client_builder: &client_builder,
|
||||
close_reason: client.disconnect_reason().await,
|
||||
};
|
||||
|
||||
client = match reconnect(params).await {
|
||||
Ok(client) => client,
|
||||
Err(e) => {
|
||||
tracing::debug!(target: LOG_TARGET, "Failed to reconnect: {e}; terminating the connection");
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disconnect.notify_waiters();
|
||||
}
|
||||
|
||||
async fn dispatch_call(client: Arc<WsClient>, op: Op, on_disconnect: Arc<tokio::sync::Notify>) {
|
||||
match op {
|
||||
Op::Call {
|
||||
method,
|
||||
params,
|
||||
send_back,
|
||||
} => {
|
||||
match client.request::<Box<RawValue>, _>(&method, params).await {
|
||||
Ok(rp) => {
|
||||
// Fails only if the request is dropped by the client.
|
||||
let _ = send_back.send(Ok(rp));
|
||||
}
|
||||
Err(RpcError::RestartNeeded(e)) => {
|
||||
// Fails only if the request is dropped by the client.
|
||||
let _ = send_back.send(Err(DisconnectedWillReconnect(e.to_string()).into()));
|
||||
}
|
||||
Err(e) => {
|
||||
// Fails only if the request is dropped by the client.
|
||||
let _ = send_back.send(Err(Error::RpcError(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Op::Subscription {
|
||||
subscribe_method,
|
||||
params,
|
||||
unsubscribe_method,
|
||||
send_back,
|
||||
} => {
|
||||
match client
|
||||
.subscribe::<Box<RawValue>, _>(
|
||||
&subscribe_method,
|
||||
params.clone(),
|
||||
&unsubscribe_method,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(sub) => {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
let sub_id = match sub.kind() {
|
||||
SubscriptionKind::Subscription(id) => id.clone().into_owned(),
|
||||
_ => unreachable!("No method subscriptions possible in this crate; qed"),
|
||||
};
|
||||
|
||||
platform::spawn(subscription_handler(
|
||||
tx.clone(),
|
||||
sub,
|
||||
on_disconnect.clone(),
|
||||
client.clone(),
|
||||
));
|
||||
|
||||
let stream = Subscription {
|
||||
id: sub_id,
|
||||
stream: rx,
|
||||
};
|
||||
|
||||
// Fails only if the request is dropped by the client.
|
||||
let _ = send_back.send(Ok(stream));
|
||||
}
|
||||
Err(RpcError::RestartNeeded(e)) => {
|
||||
// Fails only if the request is dropped by the client.
|
||||
let _ = send_back.send(Err(DisconnectedWillReconnect(e.to_string()).into()));
|
||||
}
|
||||
Err(e) => {
|
||||
// Fails only if the request is dropped.
|
||||
let _ = send_back.send(Err(Error::RpcError(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for each individual subscription.
|
||||
async fn subscription_handler(
|
||||
sub_tx: UnboundedSender<SubscriptionResult>,
|
||||
mut rpc_sub: RpcSubscription<Box<RawValue>>,
|
||||
client_closed: Arc<Notify>,
|
||||
client: Arc<WsClient>,
|
||||
) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
next_msg = rpc_sub.next() => {
|
||||
let Some(notif) = next_msg else {
|
||||
let close = client.disconnect_reason().await;
|
||||
_ = sub_tx.send(Err(DisconnectedWillReconnect(close.to_string())));
|
||||
break;
|
||||
};
|
||||
|
||||
let msg = notif.expect("RawValue is valid JSON; qed");
|
||||
|
||||
// Fails only if subscription was closed by the user.
|
||||
if sub_tx.send(Ok(msg)).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// This channel indices whether the subscription was closed by user.
|
||||
_ = sub_tx.closed() => {
|
||||
break;
|
||||
}
|
||||
// This channel indicates whether the main task has been closed.
|
||||
// at this point no further messages are processed.
|
||||
_ = client_closed.notified() => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ReconnectParams<'a, P> {
|
||||
url: &'a Url,
|
||||
client_builder: &'a RpcClientBuilder<P>,
|
||||
close_reason: RpcError,
|
||||
}
|
||||
|
||||
async fn reconnect<P>(params: ReconnectParams<'_, P>) -> Result<Arc<WsClient>, RpcError>
|
||||
where
|
||||
P: Iterator<Item = Duration> + Send + 'static + Clone,
|
||||
{
|
||||
let ReconnectParams {
|
||||
url,
|
||||
client_builder,
|
||||
close_reason,
|
||||
} = params;
|
||||
|
||||
let retry_policy = client_builder.retry_policy.clone();
|
||||
|
||||
tracing::debug!(target: LOG_TARGET, "Connection to {url} was closed: `{}`; starting to reconnect", display_close_reason(&close_reason));
|
||||
|
||||
let client = Retry::new(retry_policy.clone(), || {
|
||||
platform::ws_client(url, client_builder)
|
||||
})
|
||||
.await?;
|
||||
|
||||
tracing::debug!(target: LOG_TARGET, "Connection to {url} was successfully re-established");
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// 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::{RpcClientBuilder, RpcError};
|
||||
use jsonrpsee::core::client::Client;
|
||||
use std::sync::Arc;
|
||||
use url::Url;
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
pub use tokio::spawn;
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
pub use wasm_bindgen_futures::spawn_local as spawn;
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
pub async fn ws_client<P>(
|
||||
url: &Url,
|
||||
builder: &RpcClientBuilder<P>,
|
||||
) -> Result<Arc<Client>, RpcError> {
|
||||
use jsonrpsee::ws_client::WsClientBuilder;
|
||||
|
||||
let RpcClientBuilder {
|
||||
max_request_size,
|
||||
max_response_size,
|
||||
ping_config,
|
||||
headers,
|
||||
max_redirections,
|
||||
id_kind,
|
||||
max_concurrent_requests,
|
||||
max_log_len,
|
||||
request_timeout,
|
||||
connection_timeout,
|
||||
..
|
||||
} = builder;
|
||||
|
||||
let mut ws_client_builder = WsClientBuilder::new()
|
||||
.max_request_size(*max_request_size)
|
||||
.max_response_size(*max_response_size)
|
||||
.set_headers(headers.clone())
|
||||
.max_redirections(*max_redirections as usize)
|
||||
.max_buffer_capacity_per_subscription(tokio::sync::Semaphore::MAX_PERMITS)
|
||||
.max_concurrent_requests(*max_concurrent_requests as usize)
|
||||
.set_max_logging_length(*max_log_len)
|
||||
.set_tcp_no_delay(true)
|
||||
.request_timeout(*request_timeout)
|
||||
.connection_timeout(*connection_timeout)
|
||||
.id_format(*id_kind);
|
||||
|
||||
if let Some(ping) = ping_config {
|
||||
ws_client_builder = ws_client_builder.enable_ws_ping(*ping);
|
||||
}
|
||||
|
||||
let client = ws_client_builder.build(url.as_str()).await?;
|
||||
|
||||
Ok(Arc::new(client))
|
||||
}
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
pub async fn ws_client<P>(
|
||||
url: &Url,
|
||||
builder: &RpcClientBuilder<P>,
|
||||
) -> Result<Arc<Client>, RpcError> {
|
||||
use jsonrpsee::wasm_client::WasmClientBuilder;
|
||||
|
||||
let RpcClientBuilder {
|
||||
id_kind,
|
||||
max_concurrent_requests,
|
||||
max_log_len,
|
||||
request_timeout,
|
||||
..
|
||||
} = builder;
|
||||
|
||||
let ws_client_builder = WasmClientBuilder::new()
|
||||
.max_buffer_capacity_per_subscription(tokio::sync::Semaphore::MAX_PERMITS)
|
||||
.max_concurrent_requests(*max_concurrent_requests as usize)
|
||||
.set_max_logging_length(*max_log_len)
|
||||
.request_timeout(*request_timeout)
|
||||
.id_format(*id_kind);
|
||||
|
||||
let client = ws_client_builder.build(url.as_str()).await?;
|
||||
|
||||
Ok(Arc::new(client))
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
// 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::*;
|
||||
use futures::{future::Either, FutureExt};
|
||||
|
||||
use jsonrpsee::core::BoxError;
|
||||
use jsonrpsee::server::{
|
||||
http, stop_channel, ws, ConnectionGuard, ConnectionState, HttpRequest, HttpResponse, RpcModule,
|
||||
RpcServiceBuilder, ServerConfig, SubscriptionMessage,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn call_works() {
|
||||
let (_handle, addr) = run_server().await.unwrap();
|
||||
let client = RpcClient::builder().build(addr).await.unwrap();
|
||||
assert!(client.request("say_hello".to_string(), None).await.is_ok(),)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sub_works() {
|
||||
let (_handle, addr) = run_server().await.unwrap();
|
||||
|
||||
let client = RpcClient::builder()
|
||||
.retry_policy(ExponentialBackoff::from_millis(50))
|
||||
.build(addr)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut sub = client
|
||||
.subscribe(
|
||||
"subscribe_lo".to_string(),
|
||||
None,
|
||||
"unsubscribe_lo".to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(sub.next().await.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sub_with_reconnect() {
|
||||
let (handle, addr) = run_server().await.unwrap();
|
||||
let client = RpcClient::builder().build(addr.clone()).await.unwrap();
|
||||
|
||||
let sub = client
|
||||
.subscribe(
|
||||
"subscribe_lo".to_string(),
|
||||
None,
|
||||
"unsubscribe_lo".to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Tell server to shut down.
|
||||
let _ = handle.send(());
|
||||
|
||||
// Drain any values from the subscription. We should end with a DisconnectedWillReconnect error,
|
||||
// so that subscriptions have the opportunity to react to the fact that we were disconnected.
|
||||
let sub_ended_with_disconnect_err = sub.fold(false, async |_, next| matches!(next, Err(DisconnectedWillReconnect(_))));
|
||||
let sub_ended_with_disconnect_err = tokio::time::timeout(tokio::time::Duration::from_secs(5), sub_ended_with_disconnect_err)
|
||||
.await
|
||||
.expect("timeout should not be hit");
|
||||
|
||||
assert!(sub_ended_with_disconnect_err, "DisconnectedWillReconnect err was last message in sub");
|
||||
|
||||
// Start a new server at the same address as the old one. (This will wait a bit for the addr to be free)
|
||||
let (_handle, _) = run_server_with_settings(Some(&addr), false).await.unwrap();
|
||||
|
||||
// Hack to wait for the server to restart.
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// We can subscribe again on the same client and it should work.
|
||||
let mut sub = client
|
||||
.subscribe(
|
||||
"subscribe_lo".to_string(),
|
||||
None,
|
||||
"unsubscribe_lo".to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(sub.next().await, Some(Ok(_))));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn call_with_reconnect() {
|
||||
let (handle, addr) = run_server_with_settings(None, true).await.unwrap();
|
||||
|
||||
let client = Arc::new(RpcClient::builder().build(addr.clone()).await.unwrap());
|
||||
|
||||
let req_fut = client.request("say_hello".to_string(), None).boxed();
|
||||
let timeout_fut = tokio::time::sleep(Duration::from_secs(5));
|
||||
|
||||
// If the call isn't replied in 5 secs then it's regarded as it's still pending.
|
||||
let req_fut = match futures::future::select(Box::pin(timeout_fut), req_fut).await {
|
||||
Either::Left((_, f)) => f,
|
||||
Either::Right(_) => panic!("RPC call finished"),
|
||||
};
|
||||
|
||||
// Close the connection with a pending call.
|
||||
let _ = handle.send(());
|
||||
|
||||
// Restart the server
|
||||
let (_handle, _) = run_server_with_settings(Some(&addr), false).await.unwrap();
|
||||
|
||||
// Hack to wait for the server to restart.
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// This call should fail because reconnect.
|
||||
assert!(req_fut.await.is_err());
|
||||
// Future call should work after reconnect.
|
||||
assert!(client.request("say_hello".to_string(), None).await.is_ok());
|
||||
}
|
||||
|
||||
async fn run_server() -> Result<(tokio::sync::broadcast::Sender<()>, String), BoxError> {
|
||||
run_server_with_settings(None, false).await
|
||||
}
|
||||
|
||||
async fn run_server_with_settings(
|
||||
url: Option<&str>,
|
||||
dont_respond_to_method_calls: bool,
|
||||
) -> Result<(tokio::sync::broadcast::Sender<()>, String), BoxError> {
|
||||
use jsonrpsee::server::HttpRequest;
|
||||
|
||||
let sockaddr = match url {
|
||||
Some(url) => url.strip_prefix("ws://").unwrap(),
|
||||
None => "127.0.0.1:0",
|
||||
};
|
||||
|
||||
let mut i = 0;
|
||||
|
||||
let listener = loop {
|
||||
if let Ok(l) = tokio::net::TcpListener::bind(sockaddr).await {
|
||||
break l;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
if i >= 100 {
|
||||
panic!("Addr already in use");
|
||||
}
|
||||
|
||||
i += 1;
|
||||
};
|
||||
|
||||
let mut module = RpcModule::new(());
|
||||
|
||||
if dont_respond_to_method_calls {
|
||||
module.register_async_method("say_hello", |_, _, _| async {
|
||||
futures::future::pending::<()>().await;
|
||||
"timeout"
|
||||
})?;
|
||||
} else {
|
||||
module.register_async_method("say_hello", |_, _, _| async { "lo" })?;
|
||||
}
|
||||
|
||||
module.register_subscription(
|
||||
"subscribe_lo",
|
||||
"subscribe_lo",
|
||||
"unsubscribe_lo",
|
||||
|_params, pending, _ctx, _| async move {
|
||||
let sink = pending.accept().await.unwrap();
|
||||
let i = 0;
|
||||
|
||||
loop {
|
||||
if sink
|
||||
.send(SubscriptionMessage::from_json(&i).unwrap())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_secs(6)).await;
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
||||
let (tx, mut rx) = tokio::sync::broadcast::channel(4);
|
||||
let tx2 = tx.clone();
|
||||
let (stop_handle, server_handle) = stop_channel();
|
||||
let addr = listener.local_addr().expect("Could not find local addr");
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let sock = tokio::select! {
|
||||
res = listener.accept() => {
|
||||
match res {
|
||||
Ok((stream, _remote_addr)) => stream,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to accept connection: {:?}", e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = rx.recv() => {
|
||||
break
|
||||
}
|
||||
};
|
||||
|
||||
let module = module.clone();
|
||||
let rx2 = tx2.subscribe();
|
||||
let tx2 = tx2.clone();
|
||||
let stop_handle2 = stop_handle.clone();
|
||||
|
||||
let svc = tower::service_fn(move |req: HttpRequest<hyper::body::Incoming>| {
|
||||
let module = module.clone();
|
||||
let tx = tx2.clone();
|
||||
let stop_handle = stop_handle2.clone();
|
||||
|
||||
let conn_permit = ConnectionGuard::new(1).try_acquire().unwrap();
|
||||
|
||||
if ws::is_upgrade_request(&req) {
|
||||
let rpc_service = RpcServiceBuilder::new();
|
||||
let conn = ConnectionState::new(stop_handle, 1, conn_permit);
|
||||
|
||||
async move {
|
||||
let mut rx = tx.subscribe();
|
||||
|
||||
let (rp, conn_fut) =
|
||||
ws::connect(req, ServerConfig::default(), module, conn, rpc_service)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tokio::spawn(async move {
|
||||
tokio::select! {
|
||||
_ = conn_fut => (),
|
||||
_ = rx.recv() => {},
|
||||
}
|
||||
});
|
||||
|
||||
Ok::<_, BoxError>(rp)
|
||||
}
|
||||
.boxed()
|
||||
} else {
|
||||
async { Ok(http::response::denied()) }.boxed()
|
||||
}
|
||||
});
|
||||
|
||||
tokio::spawn(serve_with_graceful_shutdown(sock, svc, rx2));
|
||||
}
|
||||
|
||||
drop(server_handle);
|
||||
});
|
||||
|
||||
Ok((tx, format!("ws://{addr}")))
|
||||
}
|
||||
|
||||
async fn serve_with_graceful_shutdown<S, B, I>(
|
||||
io: I,
|
||||
service: S,
|
||||
mut rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) where
|
||||
S: tower::Service<HttpRequest<hyper::body::Incoming>, Response = HttpResponse<B>>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ 'static,
|
||||
S::Future: Send,
|
||||
S::Response: Send,
|
||||
S::Error: Into<BoxError>,
|
||||
B: http_body::Body<Data = hyper::body::Bytes> + Send + 'static,
|
||||
B::Error: Into<BoxError>,
|
||||
I: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin + 'static,
|
||||
{
|
||||
if let Err(e) =
|
||||
jsonrpsee::server::serve_with_graceful_shutdown(io, service, rx.recv().map(|_| ())).await
|
||||
{
|
||||
tracing::error!("Error while serving: {:?}", e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// 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 super::RpcError;
|
||||
|
||||
pub fn display_close_reason(err: &RpcError) -> String {
|
||||
match err {
|
||||
RpcError::RestartNeeded(e) => e.to_string(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// 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 [`RoundRobinRpcClient`], which is useful for load balancing
|
||||
//! requests across multiple RPC clients.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! use pezkuwi_subxt_rpcs::client::{RpcClient, RoundRobinRpcClient, jsonrpsee_client};
|
||||
//!
|
||||
//! // Construct some RpcClients (we'll make some jsonrpsee clients here, but
|
||||
//! // you could use anything which implements `RpcClientT`).
|
||||
//! let client1 = jsonrpsee_client("http://localhost:8080").await.unwrap();
|
||||
//! let client2 = jsonrpsee_client("http://localhost:8081").await.unwrap();
|
||||
//! let client3 = jsonrpsee_client("http://localhost:8082").await.unwrap();
|
||||
//!
|
||||
//! let round_robin_client = RoundRobinRpcClient::new(vec![client1, client2, client3]);
|
||||
//!
|
||||
//! // 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(round_robin_client);
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use super::{RawRpcFuture, RawRpcSubscription, RpcClientT};
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
|
||||
/// A simple RPC client which is provided a set of clients on initialization and
|
||||
/// will round-robin through them for each request.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RoundRobinRpcClient<Client> {
|
||||
inner: Arc<RoundRobinRpcClientInner<Client>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RoundRobinRpcClientInner<Client> {
|
||||
clients: Vec<Client>,
|
||||
next_index: AtomicUsize,
|
||||
}
|
||||
|
||||
impl<Client: RpcClientT> RoundRobinRpcClient<Client> {
|
||||
/// Create a new `RoundRobinRpcClient` with the given clients.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the `clients` vector is empty.
|
||||
pub fn new(clients: Vec<Client>) -> Self {
|
||||
assert!(!clients.is_empty(), "At least one client must be provided");
|
||||
Self {
|
||||
inner: Arc::new(RoundRobinRpcClientInner { clients, next_index: AtomicUsize::new(0) }),
|
||||
}
|
||||
}
|
||||
|
||||
fn next_client(&self) -> &Client {
|
||||
let idx = self.next_index();
|
||||
&self.inner.clients[idx]
|
||||
}
|
||||
|
||||
fn next_index(&self) -> usize {
|
||||
// Note: fetch_add wraps on overflow so no need to handle this.
|
||||
self.inner.next_index.fetch_add(1, Ordering::Relaxed) % self.inner.clients.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Client: RpcClientT> RpcClientT for RoundRobinRpcClient<Client> {
|
||||
fn request_raw<'a>(
|
||||
&'a self,
|
||||
method: &'a str,
|
||||
params: Option<Box<serde_json::value::RawValue>>,
|
||||
) -> RawRpcFuture<'a, Box<serde_json::value::RawValue>> {
|
||||
let client = self.next_client();
|
||||
client.request_raw(method, params)
|
||||
}
|
||||
|
||||
fn subscribe_raw<'a>(
|
||||
&'a self,
|
||||
sub: &'a str,
|
||||
params: Option<Box<serde_json::value::RawValue>>,
|
||||
unsub: &'a str,
|
||||
) -> RawRpcFuture<'a, RawRpcSubscription> {
|
||||
let client = self.next_client();
|
||||
client.subscribe_raw(sub, params, unsub)
|
||||
}
|
||||
}
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
// 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;
|
||||
use futures::{Stream, StreamExt};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
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
|
||||
/// and is cheaply cloneable.
|
||||
#[derive(Clone)]
|
||||
pub struct RpcClient {
|
||||
client: Arc<dyn RpcClientT>,
|
||||
}
|
||||
|
||||
impl RpcClient {
|
||||
#[cfg(feature = "jsonrpsee")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "jsonrpsee")))]
|
||||
/// Create a default RPC client pointed at some URL, currently based on [`jsonrpsee`].
|
||||
///
|
||||
/// Errors if an insecure URL is provided. In this case, use [`RpcClient::from_insecure_url`]
|
||||
/// instead.
|
||||
pub async fn from_url<U: AsRef<str>>(url: U) -> Result<Self, Error> {
|
||||
crate::utils::validate_url_is_secure(url.as_ref())?;
|
||||
RpcClient::from_insecure_url(url).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "jsonrpsee")]
|
||||
/// Create a default RPC client pointed at some URL, currently based on [`jsonrpsee`].
|
||||
///
|
||||
/// Allows insecure URLs without SSL encryption, e.g. (http:// and ws:// URLs).
|
||||
pub async fn from_insecure_url<U: AsRef<str>>(url: U) -> Result<Self, Error> {
|
||||
let client = super::jsonrpsee_client(url.as_ref())
|
||||
.await
|
||||
.map_err(|e| Error::Client(Box::new(e)))?;
|
||||
Ok(Self::new(client))
|
||||
}
|
||||
|
||||
/// Create a new [`RpcClient`] from an arbitrary [`RpcClientT`] implementation.
|
||||
pub fn new<R: RpcClientT>(client: R) -> Self {
|
||||
RpcClient { client: Arc::new(client) }
|
||||
}
|
||||
|
||||
/// Make an RPC request, given a method name and some parameters.
|
||||
///
|
||||
/// See [`RpcParams`] and the [`rpc_params!`] macro for an example of how to
|
||||
/// construct the parameters.
|
||||
pub async fn request<Res: DeserializeOwned>(
|
||||
&self,
|
||||
method: &str,
|
||||
params: RpcParams,
|
||||
) -> Result<Res, Error> {
|
||||
let res = self.client.request_raw(method, params.build()).await?;
|
||||
let val = serde_json::from_str(res.get()).map_err(Error::Deserialization)?;
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
/// Subscribe to an RPC endpoint, providing the parameters and the method to call to
|
||||
/// unsubscribe from it again.
|
||||
///
|
||||
/// See [`RpcParams`] and the [`rpc_params!`] macro for an example of how to
|
||||
/// construct the parameters.
|
||||
pub async fn subscribe<Res: DeserializeOwned>(
|
||||
&self,
|
||||
sub: &str,
|
||||
params: RpcParams,
|
||||
unsub: &str,
|
||||
) -> Result<RpcSubscription<Res>, Error> {
|
||||
let sub = self.client.subscribe_raw(sub, params.build(), unsub).await?;
|
||||
Ok(RpcSubscription::new(sub))
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: RpcClientT> From<C> for RpcClient {
|
||||
fn from(client: C) -> Self {
|
||||
RpcClient::new(client)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RpcClient {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("RpcClient").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for RpcClient {
|
||||
type Target = dyn RpcClientT;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.client
|
||||
}
|
||||
}
|
||||
|
||||
/// Create some [`RpcParams`] to pass to our [`RpcClient`]. [`RpcParams`]
|
||||
/// simply enforces that parameters handed to our [`RpcClient`] methods
|
||||
/// are the correct shape.
|
||||
///
|
||||
/// As with the [`serde_json::json!`] macro, this will panic if you provide
|
||||
/// parameters which cannot successfully be serialized to JSON.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,standalone_crate
|
||||
/// use pezkuwi_subxt_rpcs::client::{ rpc_params, RpcParams };
|
||||
///
|
||||
/// // If you provide no params you get `None` back
|
||||
/// let params: RpcParams = rpc_params![];
|
||||
/// assert!(params.build().is_none());
|
||||
///
|
||||
/// // If you provide params you get `Some<Box<RawValue>>` back.
|
||||
/// let params: RpcParams = rpc_params![1, true, "foo"];
|
||||
/// assert_eq!(params.build().unwrap().get(), "[1,true,\"foo\"]");
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! rpc_params {
|
||||
($($p:expr), *) => {{
|
||||
// May be unused if empty; no params.
|
||||
#[allow(unused_mut)]
|
||||
let mut params = $crate::client::RpcParams::new();
|
||||
$(
|
||||
params.push($p).expect("values passed to rpc_params! must be serializable to JSON");
|
||||
)*
|
||||
params
|
||||
}}
|
||||
}
|
||||
pub use rpc_params;
|
||||
|
||||
/// This represents the parameters passed to an [`RpcClient`], and exists to
|
||||
/// enforce that parameters are provided in the correct format.
|
||||
///
|
||||
/// Prefer to use the [`rpc_params!`] macro for simpler creation of these.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,standalone_crate
|
||||
/// use pezkuwi_subxt_rpcs::client::RpcParams;
|
||||
///
|
||||
/// let mut params = RpcParams::new();
|
||||
/// params.push(1).unwrap();
|
||||
/// params.push(true).unwrap();
|
||||
/// params.push("foo").unwrap();
|
||||
///
|
||||
/// assert_eq!(params.build().unwrap().get(), "[1,true,\"foo\"]");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RpcParams(Vec<u8>);
|
||||
|
||||
impl RpcParams {
|
||||
/// Create a new empty set of [`RpcParams`].
|
||||
pub fn new() -> Self {
|
||||
Self(Vec::new())
|
||||
}
|
||||
/// Push a parameter into our [`RpcParams`]. This serializes it to JSON
|
||||
/// in the process, and so will return an error if this is not possible.
|
||||
pub fn push<P: Serialize>(&mut self, param: P) -> Result<(), Error> {
|
||||
if self.0.is_empty() {
|
||||
self.0.push(b'[');
|
||||
} else {
|
||||
self.0.push(b',')
|
||||
}
|
||||
serde_json::to_writer(&mut self.0, ¶m).map_err(Error::Deserialization)?;
|
||||
Ok(())
|
||||
}
|
||||
/// Build a [`RawValue`] from our params, returning `None` if no parameters
|
||||
/// were provided.
|
||||
pub fn build(mut self) -> Option<Box<RawValue>> {
|
||||
if self.0.is_empty() {
|
||||
None
|
||||
} else {
|
||||
self.0.push(b']');
|
||||
let s = unsafe { String::from_utf8_unchecked(self.0) };
|
||||
Some(RawValue::from_string(s).expect("Should be valid JSON"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A generic RPC Subscription. This implements [`Stream`], and so most of
|
||||
/// the functionality you'll need to interact with it comes from the
|
||||
/// [`StreamExt`] extension trait.
|
||||
pub struct RpcSubscription<Res> {
|
||||
inner: RawRpcSubscription,
|
||||
_marker: std::marker::PhantomData<Res>,
|
||||
}
|
||||
|
||||
impl<Res> std::fmt::Debug for RpcSubscription<Res> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("RpcSubscription")
|
||||
.field("inner", &"RawRpcSubscription")
|
||||
.field("_marker", &self._marker)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Res> RpcSubscription<Res> {
|
||||
/// Creates a new [`RpcSubscription`].
|
||||
pub fn new(inner: RawRpcSubscription) -> Self {
|
||||
Self { inner, _marker: std::marker::PhantomData }
|
||||
}
|
||||
|
||||
/// Obtain the ID associated with this subscription.
|
||||
pub fn subscription_id(&self) -> Option<&str> {
|
||||
self.inner.id.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Res: DeserializeOwned> RpcSubscription<Res> {
|
||||
/// Returns the next item in the stream. This is just a wrapper around
|
||||
/// [`StreamExt::next()`] so that you can avoid the extra import.
|
||||
pub async fn next(&mut self) -> Option<Result<Res, Error>> {
|
||||
StreamExt::next(self).await
|
||||
}
|
||||
}
|
||||
|
||||
impl<Res> std::marker::Unpin for RpcSubscription<Res> {}
|
||||
|
||||
impl<Res: DeserializeOwned> Stream for RpcSubscription<Res> {
|
||||
type Item = Result<Res, Error>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
let res = futures::ready!(self.inner.stream.poll_next_unpin(cx));
|
||||
|
||||
// Decode the inner RawValue to the type we're expecting and map
|
||||
// any errors to the right shape:
|
||||
let res = res.map(|r| {
|
||||
r.and_then(|raw_val| {
|
||||
serde_json::from_str(raw_val.get()).map_err(Error::Deserialization)
|
||||
})
|
||||
});
|
||||
|
||||
Poll::Ready(res)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// 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;
|
||||
use futures::Stream;
|
||||
use std::{future::Future, pin::Pin};
|
||||
|
||||
// Re-exporting for simplicity since it's used a bunch in the trait definition.
|
||||
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`], 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
|
||||
/// the caller. This is the case because we want the methods to be object-safe (which prohibits
|
||||
/// generics), and want to avoid any unnecessary allocations in serializing/deserializing
|
||||
/// parameters.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Implementations are free to panic if the `RawValue`'s passed to `request_raw` or
|
||||
/// `subscribe_raw` are not JSON arrays. Internally, we ensure that this is always the case.
|
||||
pub trait RpcClientT: Send + Sync + 'static {
|
||||
/// Make a raw request for which we expect a single response back from. Implementations
|
||||
/// should expect that the params will either be `None`, or be an already-serialized
|
||||
/// JSON array of parameters.
|
||||
///
|
||||
/// See [`super::RpcParams`] and the [`super::rpc_params!`] macro for an example of how to
|
||||
/// construct the parameters.
|
||||
///
|
||||
/// Prefer to use the interface provided on [`super::RpcClient`] where possible.
|
||||
fn request_raw<'a>(
|
||||
&'a self,
|
||||
method: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
) -> RawRpcFuture<'a, Box<RawValue>>;
|
||||
|
||||
/// Subscribe to some method. Implementations should expect that the params will
|
||||
/// either be `None`, or be an already-serialized JSON array of parameters.
|
||||
///
|
||||
/// See [`super::RpcParams`] and the [`super::rpc_params!`] macro for an example of how to
|
||||
/// construct the parameters.
|
||||
///
|
||||
/// Prefer to use the interface provided on [`super::RpcClient`] where possible.
|
||||
fn subscribe_raw<'a>(
|
||||
&'a self,
|
||||
sub: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
unsub: &'a str,
|
||||
) -> RawRpcFuture<'a, RawRpcSubscription>;
|
||||
}
|
||||
|
||||
/// A boxed future that is returned from the [`RpcClientT`] methods.
|
||||
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>, Error>> + Send + 'static>>,
|
||||
/// The ID associated with the subscription.
|
||||
pub id: Option<String>,
|
||||
}
|
||||
|
||||
impl<T: RpcClientT> RpcClientT for std::sync::Arc<T> {
|
||||
fn request_raw<'a>(
|
||||
&'a self,
|
||||
method: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
) -> RawRpcFuture<'a, Box<RawValue>> {
|
||||
(**self).request_raw(method, params)
|
||||
}
|
||||
|
||||
fn subscribe_raw<'a>(
|
||||
&'a self,
|
||||
sub: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
unsub: &'a str,
|
||||
) -> RawRpcFuture<'a, RawRpcSubscription> {
|
||||
(**self).subscribe_raw(sub, params, unsub)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: RpcClientT> RpcClientT for Box<T> {
|
||||
fn request_raw<'a>(
|
||||
&'a self,
|
||||
method: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
) -> RawRpcFuture<'a, Box<RawValue>> {
|
||||
(**self).request_raw(method, params)
|
||||
}
|
||||
|
||||
fn subscribe_raw<'a>(
|
||||
&'a self,
|
||||
sub: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
unsub: &'a str,
|
||||
) -> RawRpcFuture<'a, RawRpcSubscription> {
|
||||
(**self).subscribe_raw(sub, params, unsub)
|
||||
}
|
||||
}
|
||||
Vendored
+148
@@ -0,0 +1,148 @@
|
||||
// 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 Bizinikiwi 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 Bizinikiwi 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_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
#[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: Hash;
|
||||
/// 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 Hash: serde::de::DeserializeOwned + serde::Serialize {}
|
||||
impl<T> Hash 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 `pezkuwi_subxt::Config`
|
||||
// is also a valid `RpcConfig`.
|
||||
#[cfg(feature = "subxt")]
|
||||
mod impl_config {
|
||||
use super::*;
|
||||
use pezkuwi_subxt_core::config::HashFor;
|
||||
|
||||
impl<T> RpcConfig for T
|
||||
where
|
||||
T: pezkuwi_subxt_core::Config,
|
||||
{
|
||||
type Header = T::Header;
|
||||
type Hash = HashFor<T>;
|
||||
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)
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
// 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;
|
||||
pub(crate) use cfg_jsonrpsee;
|
||||
pub(crate) use cfg_mock_rpc_client;
|
||||
pub(crate) use cfg_reconnecting_rpc_client;
|
||||
pub(crate) use cfg_unstable_light_client;
|
||||
+1380
File diff suppressed because it is too large
Load Diff
+682
@@ -0,0 +1,682 @@
|
||||
// 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::{
|
||||
Error, RpcConfig,
|
||||
client::{RpcClient, RpcSubscription, rpc_params},
|
||||
};
|
||||
use codec::Decode;
|
||||
use derive_where::derive_where;
|
||||
use frame_metadata::RuntimeMetadataPrefixed;
|
||||
use primitive_types::U256;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// An interface to call the legacy RPC methods. This interface is instantiated with
|
||||
/// some `T: Config` trait which determines some of the types that the RPC methods will
|
||||
/// take or hand back.
|
||||
#[derive_where(Clone, Debug)]
|
||||
pub struct LegacyRpcMethods<T> {
|
||||
client: RpcClient,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: RpcConfig> LegacyRpcMethods<T> {
|
||||
/// Instantiate the legacy RPC method interface.
|
||||
pub fn new(client: RpcClient) -> Self {
|
||||
LegacyRpcMethods { client, _marker: std::marker::PhantomData }
|
||||
}
|
||||
|
||||
/// Fetch the raw bytes for a given storage key
|
||||
pub async fn state_get_storage(
|
||||
&self,
|
||||
key: &[u8],
|
||||
hash: Option<T::Hash>,
|
||||
) -> Result<Option<StorageData>, Error> {
|
||||
let params = rpc_params![to_hex(key), hash];
|
||||
let data: Option<Bytes> = self.client.request("state_getStorage", params).await?;
|
||||
Ok(data.map(|b| b.0))
|
||||
}
|
||||
|
||||
/// Returns the keys with prefix with pagination support.
|
||||
/// Up to `count` keys will be returned.
|
||||
/// If `start_key` is passed, return next keys in storage in lexicographic order.
|
||||
pub async fn state_get_keys_paged(
|
||||
&self,
|
||||
key: &[u8],
|
||||
count: u32,
|
||||
start_key: Option<&[u8]>,
|
||||
at: Option<T::Hash>,
|
||||
) -> Result<Vec<StorageKey>, Error> {
|
||||
let start_key = start_key.map(to_hex);
|
||||
let params = rpc_params![to_hex(key), count, start_key, at];
|
||||
let data: Vec<Bytes> = self.client.request("state_getKeysPaged", params).await?;
|
||||
Ok(data.into_iter().map(|b| b.0).collect())
|
||||
}
|
||||
|
||||
/// Query historical storage entries in the range from the start block to the end block,
|
||||
/// defaulting the end block to the current best block if it's not given. The first
|
||||
/// [`StorageChangeSet`] returned has all of the values for each key, and subsequent ones
|
||||
/// only contain values for any keys which have changed since the last.
|
||||
pub async fn state_query_storage(
|
||||
&self,
|
||||
keys: impl IntoIterator<Item = &[u8]>,
|
||||
from: T::Hash,
|
||||
to: Option<T::Hash>,
|
||||
) -> Result<Vec<StorageChangeSet<T::Hash>>, Error> {
|
||||
let keys: Vec<String> = keys.into_iter().map(to_hex).collect();
|
||||
let params = rpc_params![keys, from, to];
|
||||
self.client.request("state_queryStorage", params).await
|
||||
}
|
||||
|
||||
/// Query storage entries at some block, using the best block if none is given.
|
||||
/// This essentially provides a way to ask for a batch of values given a batch of keys,
|
||||
/// despite the name of the [`StorageChangeSet`] type.
|
||||
pub async fn state_query_storage_at(
|
||||
&self,
|
||||
keys: impl IntoIterator<Item = &[u8]>,
|
||||
at: Option<T::Hash>,
|
||||
) -> Result<Vec<StorageChangeSet<T::Hash>>, Error> {
|
||||
let keys: Vec<String> = keys.into_iter().map(to_hex).collect();
|
||||
let params = rpc_params![keys, at];
|
||||
self.client.request("state_queryStorageAt", params).await
|
||||
}
|
||||
|
||||
/// Fetch the genesis hash
|
||||
pub async fn genesis_hash(&self) -> Result<T::Hash, Error> {
|
||||
let block_zero = 0u32;
|
||||
let params = rpc_params![block_zero];
|
||||
let genesis_hash: Option<T::Hash> =
|
||||
self.client.request("chain_getBlockHash", params).await?;
|
||||
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<StateGetMetadataResponse, Error> {
|
||||
let bytes: Bytes = self.client.request("state_getMetadata", rpc_params![at]).await?;
|
||||
Ok(StateGetMetadataResponse(bytes.0))
|
||||
}
|
||||
|
||||
/// Fetch system health
|
||||
pub async fn system_health(&self) -> Result<SystemHealth, Error> {
|
||||
self.client.request("system_health", rpc_params![]).await
|
||||
}
|
||||
|
||||
/// Fetch system chain
|
||||
pub async fn system_chain(&self) -> Result<String, Error> {
|
||||
self.client.request("system_chain", rpc_params![]).await
|
||||
}
|
||||
|
||||
/// Fetch system name
|
||||
pub async fn system_name(&self) -> Result<String, Error> {
|
||||
self.client.request("system_name", rpc_params![]).await
|
||||
}
|
||||
|
||||
/// Fetch system version
|
||||
pub async fn system_version(&self) -> Result<String, Error> {
|
||||
self.client.request("system_version", rpc_params![]).await
|
||||
}
|
||||
|
||||
/// Fetch system chain type
|
||||
pub async fn system_chain_type(&self) -> Result<String, Error> {
|
||||
self.client.request("system_chainType", rpc_params![]).await
|
||||
}
|
||||
|
||||
/// Fetch system properties
|
||||
pub async fn system_properties(&self) -> Result<SystemProperties, Error> {
|
||||
self.client.request("system_properties", rpc_params![]).await
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
self.client.request("system_accountNextIndex", rpc_params![&account_id]).await
|
||||
}
|
||||
|
||||
/// Get a header
|
||||
pub async fn chain_get_header(
|
||||
&self,
|
||||
hash: Option<T::Hash>,
|
||||
) -> Result<Option<T::Header>, Error> {
|
||||
let params = rpc_params![hash];
|
||||
let header = self.client.request("chain_getHeader", params).await?;
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
/// Get a block hash, returns hash of latest _best_ block by default.
|
||||
pub async fn chain_get_block_hash(
|
||||
&self,
|
||||
block_number: Option<BlockNumber>,
|
||||
) -> Result<Option<T::Hash>, Error> {
|
||||
let params = rpc_params![block_number];
|
||||
let block_hash = self.client.request("chain_getBlockHash", params).await?;
|
||||
Ok(block_hash)
|
||||
}
|
||||
|
||||
/// Get a block hash of the latest finalized block
|
||||
pub async fn chain_get_finalized_head(&self) -> Result<T::Hash, Error> {
|
||||
let hash = self.client.request("chain_getFinalizedHead", rpc_params![]).await?;
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
/// Get a Block
|
||||
pub async fn chain_get_block(
|
||||
&self,
|
||||
hash: Option<T::Hash>,
|
||||
) -> Result<Option<BlockDetails<T>>, Error> {
|
||||
let params = rpc_params![hash];
|
||||
let block = self.client.request("chain_getBlock", params).await?;
|
||||
Ok(block)
|
||||
}
|
||||
|
||||
/// Reexecute the specified `block_hash` and gather statistics while doing so.
|
||||
///
|
||||
/// This function requires the specified block and its parent to be available
|
||||
/// at the queried node. If either the specified block or the parent is pruned,
|
||||
/// this function will return `None`.
|
||||
pub async fn dev_get_block_stats(
|
||||
&self,
|
||||
block_hash: T::Hash,
|
||||
) -> Result<Option<BlockStats>, Error> {
|
||||
let params = rpc_params![block_hash];
|
||||
let stats = self.client.request("dev_getBlockStats", params).await?;
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
/// Get proof of storage entries at a specific block's state.
|
||||
pub async fn state_get_read_proof(
|
||||
&self,
|
||||
keys: impl IntoIterator<Item = &[u8]>,
|
||||
hash: Option<T::Hash>,
|
||||
) -> Result<ReadProof<T::Hash>, Error> {
|
||||
let keys: Vec<String> = keys.into_iter().map(to_hex).collect();
|
||||
let params = rpc_params![keys, hash];
|
||||
let proof = self.client.request("state_getReadProof", params).await?;
|
||||
Ok(proof)
|
||||
}
|
||||
|
||||
/// Fetch the runtime version
|
||||
pub async fn state_get_runtime_version(
|
||||
&self,
|
||||
at: Option<T::Hash>,
|
||||
) -> Result<RuntimeVersion, Error> {
|
||||
let params = rpc_params![at];
|
||||
let version = self.client.request("state_getRuntimeVersion", params).await?;
|
||||
Ok(version)
|
||||
}
|
||||
|
||||
/// Subscribe to all new best block headers.
|
||||
pub async fn chain_subscribe_new_heads(&self) -> Result<RpcSubscription<T::Header>, Error> {
|
||||
let subscription = self
|
||||
.client
|
||||
.subscribe(
|
||||
// Despite the name, this returns a stream of all new blocks
|
||||
// imported by the node that happen to be added to the current best chain
|
||||
// (ie all best blocks).
|
||||
"chain_subscribeNewHeads",
|
||||
rpc_params![],
|
||||
"chain_unsubscribeNewHeads",
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(subscription)
|
||||
}
|
||||
|
||||
/// Subscribe to all new block headers.
|
||||
pub async fn chain_subscribe_all_heads(&self) -> Result<RpcSubscription<T::Header>, Error> {
|
||||
let subscription = self
|
||||
.client
|
||||
.subscribe(
|
||||
// Despite the name, this returns a stream of all new blocks
|
||||
// imported by the node that happen to be added to the current best chain
|
||||
// (ie all best blocks).
|
||||
"chain_subscribeAllHeads",
|
||||
rpc_params![],
|
||||
"chain_unsubscribeAllHeads",
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(subscription)
|
||||
}
|
||||
|
||||
/// Subscribe to finalized block headers.
|
||||
///
|
||||
/// Note: this may not produce _every_ block in the finalized chain;
|
||||
/// sometimes multiple blocks are finalized at once, and in this case only the
|
||||
/// latest one is returned. the higher level APIs that use this "fill in" the
|
||||
/// gaps for us.
|
||||
pub async fn chain_subscribe_finalized_heads(
|
||||
&self,
|
||||
) -> Result<RpcSubscription<T::Header>, Error> {
|
||||
let subscription = self
|
||||
.client
|
||||
.subscribe(
|
||||
"chain_subscribeFinalizedHeads",
|
||||
rpc_params![],
|
||||
"chain_unsubscribeFinalizedHeads",
|
||||
)
|
||||
.await?;
|
||||
Ok(subscription)
|
||||
}
|
||||
|
||||
/// Subscribe to runtime version updates that produce changes in the metadata.
|
||||
/// The first item emitted by the stream is the current runtime version.
|
||||
pub async fn state_subscribe_runtime_version(
|
||||
&self,
|
||||
) -> Result<RpcSubscription<RuntimeVersion>, Error> {
|
||||
let subscription = self
|
||||
.client
|
||||
.subscribe(
|
||||
"state_subscribeRuntimeVersion",
|
||||
rpc_params![],
|
||||
"state_unsubscribeRuntimeVersion",
|
||||
)
|
||||
.await?;
|
||||
Ok(subscription)
|
||||
}
|
||||
|
||||
/// Create and submit an extrinsic and return corresponding Hash if successful
|
||||
pub async fn author_submit_extrinsic(&self, extrinsic: &[u8]) -> Result<T::Hash, Error> {
|
||||
let params = rpc_params![to_hex(extrinsic)];
|
||||
let xt_hash = self.client.request("author_submitExtrinsic", params).await?;
|
||||
Ok(xt_hash)
|
||||
}
|
||||
|
||||
/// Create and submit an extrinsic and return a subscription to the events triggered.
|
||||
pub async fn author_submit_and_watch_extrinsic(
|
||||
&self,
|
||||
extrinsic: &[u8],
|
||||
) -> Result<RpcSubscription<TransactionStatus<T::Hash>>, Error> {
|
||||
let params = rpc_params![to_hex(extrinsic)];
|
||||
let subscription = self
|
||||
.client
|
||||
.subscribe("author_submitAndWatchExtrinsic", params, "author_unwatchExtrinsic")
|
||||
.await?;
|
||||
Ok(subscription)
|
||||
}
|
||||
|
||||
/// Insert a key into the keystore.
|
||||
pub async fn author_insert_key(
|
||||
&self,
|
||||
key_type: String,
|
||||
suri: String,
|
||||
public: Vec<u8>,
|
||||
) -> Result<(), Error> {
|
||||
let params = rpc_params![key_type, suri, Bytes(public)];
|
||||
self.client.request("author_insertKey", params).await
|
||||
}
|
||||
|
||||
/// Generate new session keys and returns the corresponding public keys.
|
||||
pub async fn author_rotate_keys(&self) -> Result<Vec<u8>, Error> {
|
||||
let bytes: Bytes = self.client.request("author_rotateKeys", rpc_params![]).await?;
|
||||
Ok(bytes.0)
|
||||
}
|
||||
|
||||
/// Checks if the keystore has private keys for the given session public keys.
|
||||
///
|
||||
/// `session_keys` is the SCALE encoded session keys object from the runtime.
|
||||
///
|
||||
/// Returns `true` if all private keys could be found.
|
||||
pub async fn author_has_session_keys(&self, session_keys: Vec<u8>) -> Result<bool, Error> {
|
||||
let params = rpc_params![Bytes(session_keys)];
|
||||
self.client.request("author_hasSessionKeys", params).await
|
||||
}
|
||||
|
||||
/// Checks if the keystore has private keys for the given public key and key type.
|
||||
///
|
||||
/// Returns `true` if a private key could be found.
|
||||
pub async fn author_has_key(
|
||||
&self,
|
||||
public_key: Vec<u8>,
|
||||
key_type: String,
|
||||
) -> Result<bool, Error> {
|
||||
let params = rpc_params![Bytes(public_key), key_type];
|
||||
self.client.request("author_hasKey", params).await
|
||||
}
|
||||
|
||||
/// Execute a runtime API call via `state_call` RPC method.
|
||||
pub async fn state_call(
|
||||
&self,
|
||||
function: &str,
|
||||
call_parameters: Option<&[u8]>,
|
||||
at: Option<T::Hash>,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let call_parameters = call_parameters.unwrap_or_default();
|
||||
let bytes: Bytes = self
|
||||
.client
|
||||
.request("state_call", rpc_params![function, to_hex(call_parameters), at])
|
||||
.await?;
|
||||
Ok(bytes.0)
|
||||
}
|
||||
|
||||
/// Submits the extrinsic to the dry_run RPC, to test if it would succeed.
|
||||
///
|
||||
/// Returns a [`DryRunResult`], which is the result of performing the dry run.
|
||||
pub async fn dry_run(
|
||||
&self,
|
||||
encoded_signed: &[u8],
|
||||
at: Option<T::Hash>,
|
||||
) -> Result<DryRunResultBytes, Error> {
|
||||
let params = rpc_params![to_hex(encoded_signed), at];
|
||||
let result_bytes: Bytes = self.client.request("system_dryRun", params).await?;
|
||||
Ok(DryRunResultBytes(result_bytes.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>;
|
||||
|
||||
/// Storage data.
|
||||
pub type StorageData = Vec<u8>;
|
||||
|
||||
/// Health struct returned by the RPC
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SystemHealth {
|
||||
/// Number of connected peers
|
||||
pub peers: usize,
|
||||
/// Is the node syncing
|
||||
pub is_syncing: bool,
|
||||
/// Should this node have any peers
|
||||
///
|
||||
/// Might be false for local chains or when running without discovery.
|
||||
pub should_have_peers: bool,
|
||||
}
|
||||
|
||||
/// System properties; an arbitrary JSON object.
|
||||
pub type SystemProperties = serde_json::Map<String, serde_json::Value>;
|
||||
|
||||
/// A block number
|
||||
pub type BlockNumber = NumberOrHex;
|
||||
|
||||
/// The response from `chain_getBlock`
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(bound = "T: RpcConfig")]
|
||||
pub struct BlockDetails<T: RpcConfig> {
|
||||
/// The block itself.
|
||||
pub block: Block<T>,
|
||||
/// Block justification.
|
||||
pub justifications: Option<Vec<BlockJustification>>,
|
||||
}
|
||||
|
||||
/// Block details in the [`BlockDetails`].
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Block<T: RpcConfig> {
|
||||
/// The block header.
|
||||
pub header: T::Header,
|
||||
/// The accompanying extrinsics.
|
||||
pub extrinsics: Vec<Bytes>,
|
||||
}
|
||||
|
||||
/// An abstraction over justification for a block's validity under a consensus algorithm.
|
||||
pub type BlockJustification = (ConsensusEngineId, EncodedJustification);
|
||||
/// Consensus engine unique ID.
|
||||
pub type ConsensusEngineId = [u8; 4];
|
||||
/// The encoded justification specific to a consensus engine.
|
||||
pub type EncodedJustification = Vec<u8>;
|
||||
|
||||
/// This contains the runtime version information necessary to make transactions, as obtained from
|
||||
/// the RPC call `state_getRuntimeVersion`,
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeVersion {
|
||||
/// Version of the runtime specification. A full-node will not attempt to use its native
|
||||
/// runtime in substitute for the on-chain Wasm runtime unless all of `spec_name`,
|
||||
/// `spec_version` and `authoring_version` are the same between Wasm and native.
|
||||
pub spec_version: u32,
|
||||
|
||||
/// All existing dispatches are fully compatible when this number doesn't change. If this
|
||||
/// number changes, then `spec_version` must change, also.
|
||||
///
|
||||
/// This number must change when an existing dispatchable (module ID, dispatch ID) is changed,
|
||||
/// either through an alteration in its user-level semantics, a parameter
|
||||
/// added/removed/changed, a dispatchable being removed, a module being removed, or a
|
||||
/// dispatchable/module changing its index.
|
||||
///
|
||||
/// It need *not* change when a new module is added or when a dispatchable is added.
|
||||
pub transaction_version: u32,
|
||||
|
||||
/// Fields unnecessary to Subxt are written out to this map.
|
||||
#[serde(flatten)]
|
||||
pub other: std::collections::HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Possible transaction status events.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is copied from `sp-transaction-pool` to avoid a dependency on that crate. Therefore it
|
||||
/// must be kept compatible with that type from the target bizinikiwi version.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum TransactionStatus<Hash> {
|
||||
/// Transaction is part of the future queue.
|
||||
Future,
|
||||
/// Transaction is part of the ready queue.
|
||||
Ready,
|
||||
/// The transaction has been broadcast to the given peers.
|
||||
Broadcast(Vec<String>),
|
||||
/// Transaction has been included in block with given hash.
|
||||
InBlock(Hash),
|
||||
/// The block this transaction was included in has been retracted.
|
||||
Retracted(Hash),
|
||||
/// Maximum number of finality watchers has been reached,
|
||||
/// old watchers are being removed.
|
||||
FinalityTimeout(Hash),
|
||||
/// Transaction has been finalized by a finality-gadget, e.g GRANDPA
|
||||
Finalized(Hash),
|
||||
/// Transaction has been replaced in the pool, by another transaction
|
||||
/// that provides the same tags. (e.g. same (sender, nonce)).
|
||||
Usurped(Hash),
|
||||
/// Transaction has been dropped from the pool because of the limit.
|
||||
Dropped,
|
||||
/// Transaction is no longer valid in the current state.
|
||||
Invalid,
|
||||
}
|
||||
|
||||
/// The decoded result returned from calling `system_dryRun` on some extrinsic.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
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.
|
||||
/// If Subxt is available, the bytes here can be further decoded by calling:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// pezkuwi_subxt::error::DispatchError::decode_from(bytes, metadata)?;
|
||||
/// ```
|
||||
///
|
||||
/// Where metadata is an instance of `pezkuwi_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,
|
||||
}
|
||||
|
||||
/// The bytes representing an error dry running an extrinsic. call
|
||||
/// [`DryRunResultBytes::into_dry_run_result`] to attempt to decode this into something more
|
||||
/// meaningful.
|
||||
pub struct DryRunResultBytes(pub Vec<u8>);
|
||||
|
||||
impl DryRunResultBytes {
|
||||
/// 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;
|
||||
|
||||
// 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(DryRunDecodeError::WrongNumberOfBytes);
|
||||
}
|
||||
|
||||
if bytes[0] == 0 && bytes[1] == 0 {
|
||||
// Ok(Ok(())); transaction is valid and executed ok
|
||||
Ok(DryRunResult::Success)
|
||||
} else if bytes[0] == 0 && bytes[1] == 1 {
|
||||
// Ok(Err(dispatch_error)); transaction is valid but execution failed
|
||||
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(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")]
|
||||
pub struct StorageChangeSet<Hash> {
|
||||
/// Block hash
|
||||
pub block: Hash,
|
||||
/// A list of changes; tuples of storage key and optional storage data.
|
||||
pub changes: Vec<(Bytes, Option<Bytes>)>,
|
||||
}
|
||||
|
||||
/// Statistics of a block returned by the `dev_getBlockStats` RPC.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BlockStats {
|
||||
/// The length in bytes of the storage proof produced by executing the block.
|
||||
pub witness_len: u64,
|
||||
/// The length in bytes of the storage proof after compaction.
|
||||
pub witness_compact_len: u64,
|
||||
/// Length of the block in bytes.
|
||||
///
|
||||
/// This information can also be acquired by downloading the whole block. This merely
|
||||
/// saves some complexity on the client side.
|
||||
pub block_len: u64,
|
||||
/// Number of extrinsics in the block.
|
||||
///
|
||||
/// This information can also be acquired by downloading the whole block. This merely
|
||||
/// saves some complexity on the client side.
|
||||
pub num_extrinsics: u64,
|
||||
}
|
||||
|
||||
/// ReadProof struct returned by the RPC
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is copied from `sc-rpc-api` to avoid a dependency on that crate. Therefore it
|
||||
/// must be kept compatible with that type from the target bizinikiwi version.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReadProof<Hash> {
|
||||
/// Block hash used to generate the proof
|
||||
pub at: Hash,
|
||||
/// A proof used to prove that storage entries are included in the storage trie
|
||||
pub proof: Vec<Bytes>,
|
||||
}
|
||||
|
||||
/// A number type that can be serialized both as a number or a string that encodes a number in a
|
||||
/// string.
|
||||
///
|
||||
/// We allow two representations of the block number as input. Either we deserialize to the type
|
||||
/// that is specified in the block type or we attempt to parse given hex value.
|
||||
///
|
||||
/// The primary motivation for having this type is to avoid overflows when using big integers in
|
||||
/// JavaScript (which we consider as an important RPC API consumer).
|
||||
#[derive(Copy, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
#[serde(untagged)]
|
||||
pub enum NumberOrHex {
|
||||
/// The number represented directly.
|
||||
Number(u64),
|
||||
/// Hex representation of the number.
|
||||
Hex(U256),
|
||||
}
|
||||
|
||||
impl NumberOrHex {
|
||||
/// Converts this number into an U256.
|
||||
pub fn into_u256(self) -> U256 {
|
||||
match self {
|
||||
NumberOrHex::Number(n) => n.into(),
|
||||
NumberOrHex::Hex(h) => h,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NumberOrHex> for U256 {
|
||||
fn from(num_or_hex: NumberOrHex) -> U256 {
|
||||
num_or_hex.into_u256()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! into_number_or_hex {
|
||||
($($t: ty)+) => {
|
||||
$(
|
||||
impl From<$t> for NumberOrHex {
|
||||
fn from(x: $t) -> Self {
|
||||
NumberOrHex::Number(x.into())
|
||||
}
|
||||
}
|
||||
)+
|
||||
}
|
||||
}
|
||||
into_number_or_hex!(u8 u16 u32 u64);
|
||||
|
||||
impl From<u128> for NumberOrHex {
|
||||
fn from(n: u128) -> Self {
|
||||
NumberOrHex::Hex(n.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<U256> for NumberOrHex {
|
||||
fn from(n: U256) -> Self {
|
||||
NumberOrHex::Hex(n)
|
||||
}
|
||||
}
|
||||
|
||||
/// A quick helper to encode some bytes to hex.
|
||||
fn to_hex(bytes: impl AsRef<[u8]>) -> String {
|
||||
format!("0x{}", hex::encode(bytes.as_ref()))
|
||||
}
|
||||
|
||||
/// Hex-serialized shim for `Vec<u8>`.
|
||||
#[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Hash, PartialOrd, Ord, Debug)]
|
||||
pub struct Bytes(#[serde(with = "impl_serde::serialize")] pub Vec<u8>);
|
||||
impl std::ops::Deref for Bytes {
|
||||
type Target = [u8];
|
||||
fn deref(&self) -> &[u8] {
|
||||
&self.0[..]
|
||||
}
|
||||
}
|
||||
impl From<Vec<u8>> for Bytes {
|
||||
fn from(s: Vec<u8>) -> Self {
|
||||
Bytes(s)
|
||||
}
|
||||
}
|
||||
+20
@@ -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://pezkuwichain.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 Bizinikiwi versions.
|
||||
|
||||
pub mod chain_head;
|
||||
pub mod legacy;
|
||||
|
||||
pub use chain_head::ChainHeadRpcMethods;
|
||||
pub use legacy::LegacyRpcMethods;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// 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(()) }
|
||||
}
|
||||
Reference in New Issue
Block a user