Add subxt-historic crate for accesing historic (non head-of-chain) blocks (#2040)

* WIP subxt-historic

* WIP subxt-historic

* WIP subxt-historic; flesh out basic foundations

* WIP filling in extrinsic decoding functionality

* iter and decode transaction extensions

* Fill in the Online/OfflineClient APIs and move more things to be part of the chain Config

* WIP storage

* clippy, fmt, finish extrinsics example

* prep for 0.0.1 release to claim crate name

* fix README link

* fmt

* WIP thinking about storage APIs

* WIP working out storage APIs

* Storage plain value fetching first pass

* WIP storage: first pass iterating over values done

* First apss finishing storage APIs

* fmt and clippy

* Create a storage example showing fetch and iteration

* Bump to frame-decode 0.9.0

* Bump subxt-historic to 0.0.3 for preview release

* Remove unused deps

* fix import

* clippy

* doc fixes

* tweak CI and fix some cargo hack findings

* Update README: subxt-historic is prerelease
This commit is contained in:
James Wilson
2025-08-26 17:56:21 +01:00
committed by GitHub
parent 23f3ebe6b5
commit 3aabd6dc09
33 changed files with 3220 additions and 55 deletions
+53
View File
@@ -14,6 +14,11 @@ use jsonrpsee::{
};
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 {
@@ -82,3 +87,51 @@ impl From<JsonrpseeError> for Error {
}
}
}
// 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))
}
}
+5
View File
@@ -28,11 +28,13 @@
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 subxt_lightclient::LightClientRpc as LightClientRpcClient;
pub use subxt_lightclient::LightClient;
}
crate::macros::cfg_reconnecting_rpc_client! {
@@ -45,6 +47,9 @@ crate::macros::cfg_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;
+94
View File
@@ -0,0 +1,94 @@
// 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 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)
}
}
+1 -49
View File
@@ -32,7 +32,7 @@ impl RpcClient {
///
/// 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())
let client = super::jsonrpsee_client(url.as_ref())
.await
.map_err(|e| Error::Client(Box::new(e)))?;
Ok(Self::new(client))
@@ -242,51 +242,3 @@ impl<Res: DeserializeOwned> Stream for RpcSubscription<Res> {
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))
}
}