mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-23 14:21:11 +00:00
Split RPCs into a separate crate (#1910)
* WIP extract RPCs into separate crate * fmt * Fix test * Remove unused deps * fix import * WIP: Fix up errors and most tests. Start extracintg some tests/code to rpc crate * MockRpcClient sync or async * MockRpcClient only async but better type inference * WIP MockRpcClient FnMuts and some test updates to use it * Get all but one test working with new MockRpcClient * WIP trying to debug failure * WIP, Tests mostly fixed, need to add back oen more * Get mock RPC tests working * fmt * fmt * Clippy and comment tweak * update CI to explicitly check subxt-rpc features * clippy * small tweaks after pass over * feature flag rename * update some docs * Fix some examples * fmt * Fix features flags to work with web/wasm32 * Fix unused dep warning * explicit targets in wasm CI * Add better crate level docs * fmt * Address review comments * Comment out flaky test for now and make more obvious how similar POlkadot and Substrate configs are * Not a doc comment * Remove unused imports
This commit is contained in:
@@ -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::{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;
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 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,633 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! This module exposes a [`MockRpcClient`], which is useful for testing.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use subxt_rpcs::client::{ RpcClient, MockRpcClient };
|
||||
//! use subxt_rpcs::client::mock_rpc_client::Json;
|
||||
//!
|
||||
//! let mut state = vec![
|
||||
//! Json(1u8),
|
||||
//! Json(2u8),
|
||||
//! Json(3u8),
|
||||
//! ];
|
||||
//!
|
||||
//! // Define a mock client by providing some functions which intercept
|
||||
//! // method and subscription calls and return some response.
|
||||
//! let mock_client = MockRpcClient::builder()
|
||||
//! .method_handler_once("foo", move |params| {
|
||||
//! // Return each item from our state, and then null afterwards.
|
||||
//! let val = state.pop();
|
||||
//! async move { val }
|
||||
//! })
|
||||
//! .subscription_handler("bar", |params, unsub| async move {
|
||||
//! // Arrays, vecs or an RpcSubscription can be returned here to
|
||||
//! // signal the set of values to be handed back on a subscription.
|
||||
//! vec![Json(1), Json(2), Json(3)]
|
||||
//! })
|
||||
//! .build();
|
||||
//!
|
||||
//! // Build an RPC Client that can be used in Subxt or in conjunction with
|
||||
//! // the RPC methods provided in this crate.
|
||||
//! let rpc_client = RpcClient::new(mock_client);
|
||||
//! ```
|
||||
|
||||
use super::{RpcClientT, RawRpcFuture, RawRpcSubscription};
|
||||
use crate::{Error, UserError};
|
||||
use core::future::Future;
|
||||
use futures::StreamExt;
|
||||
use serde_json::value::RawValue;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
type MethodHandlerFnOnce = Box<dyn FnOnce(&str, Option<Box<serde_json::value::RawValue>>) -> RawRpcFuture<'static, Box<RawValue>> + Send + Sync + 'static>;
|
||||
type SubscriptionHandlerFnOnce = Box<dyn FnOnce(&str, Option<Box<serde_json::value::RawValue>>, &str) -> RawRpcFuture<'static, RawRpcSubscription> + Send + Sync + 'static>;
|
||||
|
||||
type MethodHandlerFn = Box<dyn FnMut(&str, Option<Box<serde_json::value::RawValue>>) -> RawRpcFuture<'static, Box<RawValue>> + Send + Sync + 'static>;
|
||||
type SubscriptionHandlerFn = Box<dyn FnMut(&str, Option<Box<serde_json::value::RawValue>>, &str) -> RawRpcFuture<'static, RawRpcSubscription> + Send + Sync + 'static>;
|
||||
|
||||
/// A builder to configure and build a new [`MockRpcClient`].
|
||||
#[derive(Default)]
|
||||
pub struct MockRpcClientBuilder {
|
||||
method_handlers_once: HashMap<String, VecDeque<MethodHandlerFnOnce>>,
|
||||
method_handlers: HashMap<String, MethodHandlerFn>,
|
||||
method_fallback: Option<MethodHandlerFn>,
|
||||
subscription_handlers_once: HashMap<String, VecDeque<SubscriptionHandlerFnOnce>>,
|
||||
subscription_handlers: HashMap<String, SubscriptionHandlerFn>,
|
||||
subscription_fallback: Option<SubscriptionHandlerFn>
|
||||
}
|
||||
|
||||
impl MockRpcClientBuilder {
|
||||
/// Add a handler for a specific RPC method. This is called exactly once, and multiple such calls for the same method can be
|
||||
/// added. Only when any calls registered with this have been used up is the method set by [`Self::method_handler`] called.
|
||||
pub fn method_handler_once<MethodHandler, MFut, MRes>(mut self, name: impl Into<String>, f: MethodHandler) -> Self
|
||||
where
|
||||
MethodHandler: FnOnce(Option<Box<serde_json::value::RawValue>>) -> MFut + Send + Sync + 'static,
|
||||
MFut: Future<Output = MRes> + Send + 'static,
|
||||
MRes: IntoHandlerResponse,
|
||||
{
|
||||
let handler: MethodHandlerFnOnce = Box::new(move |_method: &str, params: Option<Box<serde_json::value::RawValue>>| {
|
||||
let fut = f(params);
|
||||
Box::pin(async move { fut.await.into_handler_response() })
|
||||
});
|
||||
self.method_handlers_once.entry(name.into()).or_default().push_back(handler);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a handler for a specific RPC method.
|
||||
pub fn method_handler<MethodHandler, MFut, MRes>(mut self, name: impl Into<String>, mut f: MethodHandler) -> Self
|
||||
where
|
||||
MethodHandler: FnMut(Option<Box<serde_json::value::RawValue>>) -> MFut + Send + Sync + 'static,
|
||||
MFut: Future<Output = MRes> + Send + 'static,
|
||||
MRes: IntoHandlerResponse,
|
||||
{
|
||||
let handler: MethodHandlerFn = Box::new(move |_method: &str, params: Option<Box<serde_json::value::RawValue>>| {
|
||||
let fut = f(params);
|
||||
Box::pin(async move { fut.await.into_handler_response() })
|
||||
});
|
||||
self.method_handlers.insert(name.into(), handler);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a fallback handler to handle any methods not handled by a specific handler.
|
||||
pub fn method_fallback<MethodHandler, MFut, MRes>(mut self, mut f: MethodHandler) -> Self
|
||||
where
|
||||
MethodHandler: FnMut(String, Option<Box<serde_json::value::RawValue>>) -> MFut + Send + Sync + 'static,
|
||||
MFut: Future<Output = MRes> + Send + 'static,
|
||||
MRes: IntoHandlerResponse,
|
||||
{
|
||||
let handler: MethodHandlerFn = Box::new(move |method: &str, params: Option<Box<serde_json::value::RawValue>>| {
|
||||
let fut = f(method.to_owned(), params);
|
||||
Box::pin(async move { fut.await.into_handler_response() })
|
||||
});
|
||||
self.method_fallback = Some(handler);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a handler for a specific RPC subscription.
|
||||
pub fn subscription_handler_once<SubscriptionHandler, SFut, SRes>(mut self, name: impl Into<String>, f: SubscriptionHandler) -> Self
|
||||
where
|
||||
SubscriptionHandler: FnOnce(Option<Box<serde_json::value::RawValue>>, String) -> SFut + Send + Sync + 'static,
|
||||
SFut: Future<Output = SRes> + Send + 'static,
|
||||
SRes: IntoSubscriptionResponse,
|
||||
{
|
||||
let handler: SubscriptionHandlerFnOnce = Box::new(move |_sub: &str, params: Option<Box<serde_json::value::RawValue>>, unsub: &str| {
|
||||
let fut = f(params, unsub.to_owned());
|
||||
Box::pin(async move { fut.await.into_subscription_response() })
|
||||
});
|
||||
self.subscription_handlers_once.entry(name.into()).or_default().push_back(handler);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a handler for a specific RPC subscription.
|
||||
pub fn subscription_handler<SubscriptionHandler, SFut, SRes>(mut self, name: impl Into<String>, mut f: SubscriptionHandler) -> Self
|
||||
where
|
||||
SubscriptionHandler: FnMut(Option<Box<serde_json::value::RawValue>>, String) -> SFut + Send + Sync + 'static,
|
||||
SFut: Future<Output = SRes> + Send + 'static,
|
||||
SRes: IntoSubscriptionResponse,
|
||||
{
|
||||
let handler: SubscriptionHandlerFn = Box::new(move |_sub: &str, params: Option<Box<serde_json::value::RawValue>>, unsub: &str| {
|
||||
let fut = f(params, unsub.to_owned());
|
||||
Box::pin(async move { fut.await.into_subscription_response() })
|
||||
});
|
||||
self.subscription_handlers.insert(name.into(), handler);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a fallback handler to handle any subscriptions not handled by a specific handler.
|
||||
pub fn subscription_fallback<SubscriptionHandler, SFut, SRes>(mut self, mut f: SubscriptionHandler) -> Self
|
||||
where
|
||||
SubscriptionHandler: FnMut(String, Option<Box<serde_json::value::RawValue>>, String) -> SFut + Send + Sync + 'static,
|
||||
SFut: Future<Output = SRes> + Send + 'static,
|
||||
SRes: IntoSubscriptionResponse,
|
||||
{
|
||||
let handler: SubscriptionHandlerFn = Box::new(move |sub: &str, params: Option<Box<serde_json::value::RawValue>>, unsub: &str| {
|
||||
let fut = f(sub.to_owned(), params, unsub.to_owned());
|
||||
Box::pin(async move { fut.await.into_subscription_response() })
|
||||
});
|
||||
self.subscription_fallback = Some(handler);
|
||||
self
|
||||
}
|
||||
|
||||
/// Construct a [`MockRpcClient`] given some state which will be mutably available to each of the handlers.
|
||||
pub fn build(self) -> MockRpcClient {
|
||||
MockRpcClient {
|
||||
method_handlers_once: Arc::new(Mutex::new(self.method_handlers_once)),
|
||||
method_handlers: Arc::new(Mutex::new(self.method_handlers)),
|
||||
method_fallback: self.method_fallback.map(|f| Arc::new(Mutex::new(f))),
|
||||
subscription_handlers_once: Arc::new(Mutex::new(self.subscription_handlers_once)),
|
||||
subscription_handlers: Arc::new(Mutex::new(self.subscription_handlers)),
|
||||
subscription_fallback: self.subscription_fallback.map(|f| Arc::new(Mutex::new(f))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A mock RPC client that responds programmatically to requests.
|
||||
/// Useful for testing.
|
||||
#[derive(Clone)]
|
||||
pub struct MockRpcClient {
|
||||
// These are all accessed for just long enough to call the method. The method
|
||||
// returns a future, but the method call itself isn't held for long.
|
||||
method_handlers_once: Arc<Mutex<HashMap<String, VecDeque<MethodHandlerFnOnce>>>>,
|
||||
method_handlers: Arc<Mutex<HashMap<String, MethodHandlerFn>>>,
|
||||
method_fallback: Option<Arc<Mutex<MethodHandlerFn>>>,
|
||||
subscription_handlers_once: Arc<Mutex<HashMap<String, VecDeque<SubscriptionHandlerFnOnce>>>>,
|
||||
subscription_handlers: Arc<Mutex<HashMap<String, SubscriptionHandlerFn>>>,
|
||||
subscription_fallback: Option<Arc<Mutex<SubscriptionHandlerFn>>>,
|
||||
}
|
||||
|
||||
impl MockRpcClient {
|
||||
/// Construct a new [`MockRpcClient`]
|
||||
pub fn builder() -> MockRpcClientBuilder {
|
||||
MockRpcClientBuilder::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl RpcClientT for MockRpcClient {
|
||||
fn request_raw<'a>(
|
||||
&'a self,
|
||||
method: &'a str,
|
||||
params: Option<Box<serde_json::value::RawValue>>,
|
||||
) -> RawRpcFuture<'a, Box<serde_json::value::RawValue>> {
|
||||
// Remove and call a one-time handler if any exist.
|
||||
let mut handlers_once = self.method_handlers_once.lock().unwrap();
|
||||
if let Some(handlers) = handlers_once.get_mut(method) {
|
||||
if let Some(handler) = handlers.pop_front() {
|
||||
return handler(method, params)
|
||||
}
|
||||
}
|
||||
drop(handlers_once);
|
||||
|
||||
// Call a specific handler for the method if one is found.
|
||||
let mut handlers = self.method_handlers.lock().unwrap();
|
||||
if let Some(handler) = handlers.get_mut(method) {
|
||||
return handler(method, params)
|
||||
}
|
||||
drop(handlers);
|
||||
|
||||
// Call a fallback handler if one exists
|
||||
if let Some(handler) = &self.method_fallback {
|
||||
let mut handler = handler.lock().unwrap();
|
||||
return handler(method, params)
|
||||
}
|
||||
|
||||
// Else, method not found.
|
||||
Box::pin(async move { Err(UserError::method_not_found().into()) })
|
||||
}
|
||||
fn subscribe_raw<'a>(
|
||||
&'a self,
|
||||
sub: &'a str,
|
||||
params: Option<Box<serde_json::value::RawValue>>,
|
||||
unsub: &'a str,
|
||||
) -> RawRpcFuture<'a, RawRpcSubscription> {
|
||||
// Remove and call a one-time handler if any exist.
|
||||
let mut handlers_once = self.subscription_handlers_once.lock().unwrap();
|
||||
if let Some(handlers) = handlers_once.get_mut(sub) {
|
||||
if let Some(handler) = handlers.pop_front() {
|
||||
return handler(sub, params, unsub)
|
||||
}
|
||||
}
|
||||
drop(handlers_once);
|
||||
|
||||
// Call a specific handler for the subscrpition if one is found.
|
||||
let mut handlers = self.subscription_handlers.lock().unwrap();
|
||||
if let Some(handler) = handlers.get_mut(sub) {
|
||||
return handler(sub, params, unsub)
|
||||
}
|
||||
drop(handlers);
|
||||
|
||||
// Call a fallback handler if one exists
|
||||
if let Some(handler) = &self.subscription_fallback {
|
||||
let mut handler = handler.lock().unwrap();
|
||||
return handler(sub, params, unsub)
|
||||
}
|
||||
|
||||
// Else, method not found.
|
||||
Box::pin(async move { Err(UserError::method_not_found().into()) })
|
||||
}
|
||||
}
|
||||
|
||||
/// Return responses wrapped in this to have them serialized to JSON.
|
||||
pub struct Json<T>(pub T);
|
||||
|
||||
impl Json<serde_json::Value> {
|
||||
/// Create a [`Json<serde_json::Value>`] from some serializable value.
|
||||
/// Useful when value types are heterogenous.
|
||||
pub fn value_of<T: serde::Serialize>(item: T) -> Self {
|
||||
Json(serde_json::to_value(item).expect("item cannot be converted to a serde_json::Value"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Anything that can be converted into a valid handler response implements this.
|
||||
pub trait IntoHandlerResponse {
|
||||
/// Convert self into a handler response.
|
||||
fn into_handler_response(self) -> Result<Box<RawValue>, Error>;
|
||||
}
|
||||
|
||||
impl <T: IntoHandlerResponse> IntoHandlerResponse for Result<T, Error> {
|
||||
fn into_handler_response(self) -> Result<Box<RawValue>, Error> {
|
||||
self.and_then(|val| val.into_handler_response())
|
||||
}
|
||||
}
|
||||
|
||||
impl <T: IntoHandlerResponse> IntoHandlerResponse for Option<T> {
|
||||
fn into_handler_response(self) -> Result<Box<RawValue>, Error> {
|
||||
self.ok_or_else(|| UserError::method_not_found().into())
|
||||
.and_then(|val| val.into_handler_response())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHandlerResponse for Box<RawValue> {
|
||||
fn into_handler_response(self) -> Result<Box<RawValue>, Error> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHandlerResponse for serde_json::Value {
|
||||
fn into_handler_response(self) -> Result<Box<RawValue>, Error> {
|
||||
serialize_to_raw_value(&self)
|
||||
}
|
||||
}
|
||||
|
||||
impl <T: serde::Serialize> IntoHandlerResponse for Json<T> {
|
||||
fn into_handler_response(self) -> Result<Box<RawValue>, Error> {
|
||||
serialize_to_raw_value(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHandlerResponse for core::convert::Infallible {
|
||||
fn into_handler_response(self) -> Result<Box<RawValue>, Error> {
|
||||
match self {}
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_to_raw_value<T: serde::Serialize>(val: &T) -> Result<Box<RawValue>, Error> {
|
||||
let res = serde_json::to_string(val).map_err(Error::Deserialization)?;
|
||||
let raw_value = RawValue::from_string(res).map_err(Error::Deserialization)?;
|
||||
Ok(raw_value)
|
||||
}
|
||||
|
||||
/// Anything that can be a response to a subscription handler implements this.
|
||||
pub trait IntoSubscriptionResponse {
|
||||
/// Convert self into a handler response.
|
||||
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error>;
|
||||
}
|
||||
|
||||
// A tuple of a subscription plus some string is treated as a subscription with that string ID.
|
||||
impl <T: IntoSubscriptionResponse, S: Into<String>> IntoSubscriptionResponse for (T, S) {
|
||||
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
|
||||
self.0
|
||||
.into_subscription_response()
|
||||
.map(|mut r| {
|
||||
r.id = Some(self.1.into());
|
||||
r
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl <T: IntoHandlerResponse + Send + 'static> IntoSubscriptionResponse for tokio::sync::mpsc::Receiver<T> {
|
||||
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
|
||||
struct IntoStream<T>(tokio::sync::mpsc::Receiver<T>);
|
||||
impl <T> futures::Stream for IntoStream<T> {
|
||||
type Item = T;
|
||||
fn poll_next(mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll<Option<Self::Item>> {
|
||||
self.0.poll_recv(cx)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(RawRpcSubscription {
|
||||
stream: Box::pin(IntoStream(self).map(|item| item.into_handler_response())),
|
||||
id: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl <T: IntoHandlerResponse + Send + 'static> IntoSubscriptionResponse for tokio::sync::mpsc::UnboundedReceiver<T> {
|
||||
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
|
||||
struct IntoStream<T>(tokio::sync::mpsc::UnboundedReceiver<T>);
|
||||
impl <T> futures::Stream for IntoStream<T> {
|
||||
type Item = T;
|
||||
fn poll_next(mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll<Option<Self::Item>> {
|
||||
self.0.poll_recv(cx)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(RawRpcSubscription {
|
||||
stream: Box::pin(IntoStream(self).map(|item| item.into_handler_response())),
|
||||
id: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoSubscriptionResponse for RawRpcSubscription {
|
||||
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl <T: IntoSubscriptionResponse> IntoSubscriptionResponse for Result<T, Error> {
|
||||
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
|
||||
self.and_then(|res| res.into_subscription_response())
|
||||
}
|
||||
}
|
||||
|
||||
impl <T: IntoHandlerResponse + Send + 'static> IntoSubscriptionResponse for Vec<T> {
|
||||
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
|
||||
let iter = self.into_iter().map(|item| item.into_handler_response());
|
||||
Ok(RawRpcSubscription {
|
||||
stream: Box::pin(futures::stream::iter(iter)),
|
||||
id: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl <T: IntoSubscriptionResponse + Send + 'static> IntoSubscriptionResponse for Option<T> {
|
||||
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
|
||||
match self {
|
||||
Some(sub) => {
|
||||
sub.into_subscription_response()
|
||||
},
|
||||
None => {
|
||||
Ok(RawRpcSubscription {
|
||||
stream: Box::pin(futures::stream::empty()),
|
||||
id: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl <T: IntoHandlerResponse + Send + 'static, const N: usize> IntoSubscriptionResponse for [T; N] {
|
||||
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
|
||||
let iter = self.into_iter().map(|item| item.into_handler_response());
|
||||
Ok(RawRpcSubscription {
|
||||
stream: Box::pin(futures::stream::iter(iter)),
|
||||
id: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoSubscriptionResponse for core::convert::Infallible {
|
||||
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
|
||||
match self {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send the first items and then the second items back on a subscription;
|
||||
/// If any one of the responses is an error, we'll return the error.
|
||||
/// If one response has an ID and the other doesn't, we'll use that ID.
|
||||
pub struct AndThen<A, B>(pub A, pub B);
|
||||
|
||||
impl <A: IntoSubscriptionResponse, B: IntoSubscriptionResponse> IntoSubscriptionResponse for AndThen<A, B> {
|
||||
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
|
||||
let a_responses = self.0.into_subscription_response();
|
||||
let b_responses = self.1.into_subscription_response();
|
||||
|
||||
match (a_responses, b_responses) {
|
||||
(Err(a), _) => {
|
||||
Err(a)
|
||||
},
|
||||
(_, Err(b)) => {
|
||||
Err(b)
|
||||
},
|
||||
(Ok(mut a), Ok(b)) => {
|
||||
a.stream = Box::pin(a.stream.chain(b.stream));
|
||||
a.id = a.id.or(b.id);
|
||||
Ok(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send back either one response or the other.
|
||||
pub enum Either<A, B> {
|
||||
/// The first possibility.
|
||||
A(A),
|
||||
/// The second possibility.
|
||||
B(B)
|
||||
}
|
||||
|
||||
impl <A: IntoHandlerResponse, B: IntoHandlerResponse> IntoHandlerResponse for Either<A, B> {
|
||||
fn into_handler_response(self) -> Result<Box<RawValue>, Error> {
|
||||
match self {
|
||||
Either::A(a) => a.into_handler_response(),
|
||||
Either::B(b) => b.into_handler_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl <A: IntoSubscriptionResponse, B: IntoSubscriptionResponse> IntoSubscriptionResponse for Either<A, B> {
|
||||
fn into_subscription_response(self) -> Result<RawRpcSubscription, Error> {
|
||||
match self {
|
||||
Either::A(a) => a.into_subscription_response(),
|
||||
Either::B(b) => b.into_subscription_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{RpcClient, rpc_params};
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_method_params() {
|
||||
let rpc_client = MockRpcClient::builder()
|
||||
.method_handler("foo", |params| async {
|
||||
Json(params)
|
||||
})
|
||||
.build();
|
||||
|
||||
let rpc_client = RpcClient::new(rpc_client);
|
||||
|
||||
// We get back whatever params we give
|
||||
let res: (i32,i32,i32) = rpc_client.request("foo", rpc_params![1, 2, 3]).await.unwrap();
|
||||
assert_eq!(res, (1,2,3));
|
||||
|
||||
let res: (String,) = rpc_client.request("foo", rpc_params!["hello"]).await.unwrap();
|
||||
assert_eq!(res, ("hello".to_owned(),));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_method_handler_then_fallback() {
|
||||
let rpc_client = MockRpcClient::builder()
|
||||
.method_handler("foo", |_params| async {
|
||||
Json(1)
|
||||
})
|
||||
.method_fallback(|name, _params| async {
|
||||
Json(name)
|
||||
})
|
||||
.build();
|
||||
|
||||
let rpc_client = RpcClient::new(rpc_client);
|
||||
|
||||
// Whenever we call "foo", we get 1 back.
|
||||
for i in [1,1,1,1] {
|
||||
let res: i32 = rpc_client.request("foo", rpc_params![]).await.unwrap();
|
||||
assert_eq!(res, i);
|
||||
}
|
||||
|
||||
// Whenever we call anything else, we get the name of the method back
|
||||
for name in ["bar", "wibble", "steve"] {
|
||||
let res: String = rpc_client.request(name, rpc_params![]).await.unwrap();
|
||||
assert_eq!(res, name);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_method_once_then_handler() {
|
||||
let rpc_client = MockRpcClient::builder()
|
||||
.method_handler_once("foo", |_params| async {
|
||||
Json(1)
|
||||
})
|
||||
.method_handler("foo", |_params| async {
|
||||
Json(2)
|
||||
})
|
||||
.build();
|
||||
|
||||
let rpc_client = RpcClient::new(rpc_client);
|
||||
|
||||
// Check that we call the "once" one time and then the second after that.
|
||||
for i in [1,2,2,2,2] {
|
||||
let res: i32 = rpc_client.request("foo", rpc_params![]).await.unwrap();
|
||||
assert_eq!(res, i);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_method_once() {
|
||||
let rpc_client = MockRpcClient::builder()
|
||||
.method_handler_once("foo", |_params| async {
|
||||
Json(1)
|
||||
})
|
||||
.method_handler_once("foo", |_params| async {
|
||||
Json(2)
|
||||
})
|
||||
.method_handler_once("foo", |_params| async {
|
||||
Json(3)
|
||||
})
|
||||
.build();
|
||||
|
||||
let rpc_client = RpcClient::new(rpc_client);
|
||||
|
||||
// Check that each method is only called once, in the right order.
|
||||
for i in [1,2,3] {
|
||||
let res: i32 = rpc_client.request("foo", rpc_params![]).await.unwrap();
|
||||
assert_eq!(res, i);
|
||||
}
|
||||
|
||||
// Check that we get a "method not found" error afterwards.
|
||||
let err = rpc_client.request::<i32>("foo", rpc_params![]).await.unwrap_err();
|
||||
let not_found_code = UserError::method_not_found().code;
|
||||
assert!(matches!(err, Error::User(u) if u.code == not_found_code));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_subscription_once_then_handler_then_fallback() {
|
||||
let rpc_client = MockRpcClient::builder()
|
||||
.subscription_handler_once("foo", |_params, _unsub| async {
|
||||
vec![Json(0), Json(0)]
|
||||
})
|
||||
.subscription_handler("foo", |_params, _unsub| async {
|
||||
vec![Json(1), Json(2), Json(3)]
|
||||
})
|
||||
.subscription_fallback(|_name, _params, _unsub| async {
|
||||
vec![Json(4)]
|
||||
})
|
||||
.build();
|
||||
|
||||
let rpc_client = RpcClient::new(rpc_client);
|
||||
|
||||
// "foo" returns 0,0 the first time it's subscribed to
|
||||
let sub = rpc_client.subscribe::<i32>("foo", rpc_params![], "unsub").await.unwrap();
|
||||
let res: Vec<i32> = sub.map(|i| i.unwrap()).collect().await;
|
||||
assert_eq!(res, vec![0,0]);
|
||||
|
||||
// then, "foo" returns 1,2,3 in subscription every other time
|
||||
for _ in 1..5 {
|
||||
let sub = rpc_client.subscribe::<i32>("foo", rpc_params![], "unsub").await.unwrap();
|
||||
let res: Vec<i32> = sub.map(|i| i.unwrap()).collect().await;
|
||||
assert_eq!(res, vec![1,2,3]);
|
||||
}
|
||||
|
||||
// anything else returns 4
|
||||
let sub = rpc_client.subscribe::<i32>("bar", rpc_params![], "unsub").await.unwrap();
|
||||
let res: Vec<i32> = sub.map(|i| i.unwrap()).collect().await;
|
||||
assert_eq!(res, vec![4]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_subscription_and_then_with_channel() {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(10);
|
||||
|
||||
let rpc_client = MockRpcClient::builder()
|
||||
.subscription_handler_once("foo", move |_params, _unsub| async move {
|
||||
AndThen(
|
||||
// These should be sent first..
|
||||
vec![Json(1), Json(2), Json(3)],
|
||||
// .. and then anything the channel is handing back.
|
||||
rx
|
||||
)
|
||||
})
|
||||
.build();
|
||||
|
||||
let rpc_client = RpcClient::new(rpc_client);
|
||||
|
||||
// Send a few values down the channel to be handed back in "foo" subscription:
|
||||
tokio::spawn(async move {
|
||||
for i in 4..=6 {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
tx.send(Json(i)).await.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
// Expect all values back:
|
||||
let sub = rpc_client.subscribe::<i32>("foo", rpc_params![], "unsub").await.unwrap();
|
||||
let res: Vec<i32> = sub.map(|i| i.unwrap()).collect().await;
|
||||
assert_eq!(res, vec![1,2,3,4,5,6]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! RPC types and client for interacting with a substrate node.
|
||||
//!
|
||||
//! An RPC client is instantiated and then used to create some methods, for instance
|
||||
//! [`crate::methods::ChainHeadRpcMethods`], which defines the calls that can be made with it.
|
||||
//! The core RPC client bits are:
|
||||
//!
|
||||
//! - [`RpcClientT`] is the underlying dynamic RPC implementation. This provides
|
||||
//! the low level [`RpcClientT::request_raw`] and [`RpcClientT::subscribe_raw`]
|
||||
//! methods.
|
||||
//! - [`RpcClient`] is the higher level wrapper around this, offering
|
||||
//! the [`RpcClient::request`] and [`RpcClient::subscribe`] methods.
|
||||
//!
|
||||
//! We then expose implementations here (depending on which features are enabled)
|
||||
//! which implement [`RpcClientT`] and can therefore be used to construct [`RpcClient`]s.
|
||||
//!
|
||||
//! - **jsonrpsee**: Enable an RPC client based on `jsonrpsee`.
|
||||
//! - **unstable-light-client**: Enable an RPC client which uses the Smoldot light client under
|
||||
//! the hood to communicate with the network of choice.
|
||||
//! - **reconnecting-rpc-client**: Enable an RPC client based on `jsonrpsee` which handles
|
||||
//! reconnecting automatically in the event of network issues.
|
||||
//! - **mock-rpc-client**: Enable a mock RPC client that can be used in tests.
|
||||
//!
|
||||
|
||||
crate::macros::cfg_jsonrpsee! {
|
||||
mod jsonrpsee_impl;
|
||||
pub use jsonrpsee::core::client::Client as JsonrpseeRpcClient;
|
||||
}
|
||||
|
||||
crate::macros::cfg_unstable_light_client! {
|
||||
mod lightclient_impl;
|
||||
pub use subxt_lightclient::LightClientRpc as LightClientRpcClient;
|
||||
}
|
||||
|
||||
crate::macros::cfg_reconnecting_rpc_client! {
|
||||
pub mod reconnecting_rpc_client;
|
||||
pub use reconnecting_rpc_client::RpcClient as ReconnectingRpcClient;
|
||||
}
|
||||
|
||||
crate::macros::cfg_mock_rpc_client! {
|
||||
pub mod mock_rpc_client;
|
||||
pub use mock_rpc_client::MockRpcClient;
|
||||
}
|
||||
|
||||
mod rpc_client;
|
||||
mod rpc_client_t;
|
||||
|
||||
pub use rpc_client::{rpc_params, RpcClient, RpcParams, RpcSubscription};
|
||||
pub use rpc_client_t::{RawRpcFuture, RawRpcSubscription, RawValue, RpcClientT};
|
||||
@@ -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: 10 * 1024 * 1024,
|
||||
max_response_size: 10 * 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 min response size a for websocket message.
|
||||
///
|
||||
/// Default: 10MB
|
||||
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: 10MB
|
||||
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,270 @@
|
||||
// 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 mut sub = client
|
||||
.subscribe(
|
||||
"subscribe_lo".to_string(),
|
||||
None,
|
||||
"unsubscribe_lo".to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let _ = handle.send(());
|
||||
|
||||
// Hack to wait for the server to restart.
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
assert!(matches!(sub.next().await, Some(Ok(_))));
|
||||
assert!(matches!(
|
||||
sub.next().await,
|
||||
Some(Err(DisconnectedWillReconnect(_)))
|
||||
));
|
||||
|
||||
// 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;
|
||||
|
||||
// Subscription should work after reconnect.
|
||||
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 >= 10 {
|
||||
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,292 @@
|
||||
// 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::{de::DeserializeOwned, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use std::{pin::Pin, sync::Arc, task::Poll};
|
||||
|
||||
/// A concrete wrapper around an [`RpcClientT`] which provides some higher level helper methods
|
||||
/// 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 = jsonrpsee_helpers::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
|
||||
/// use 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
|
||||
/// use 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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,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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user