Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow generalising over RPC implementation #634

Merged
merged 31 commits into from
Aug 31, 2022
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
68abe93
WIP generalising RPC client
jsdw Aug 22, 2022
3440b79
WIP: non-object-safe RpcClientT.. aah generics everywhere
jsdw Aug 23, 2022
eab9fc0
WIP object-safe RpcClientT trait and no more extra generics
jsdw Aug 23, 2022
642facd
Get core things compiling again with object-safe RpcClientT trait
jsdw Aug 24, 2022
0a29484
Make jsonrpsee optional and get test-runtime working again
jsdw Aug 24, 2022
19dd1b0
cargo fmt
jsdw Aug 24, 2022
6deb5f8
add RpcParams object to enforce correct formatting of rps params
jsdw Aug 24, 2022
1583351
Wee tweaks
jsdw Aug 24, 2022
6b6de23
clippy fixes
jsdw Aug 24, 2022
69a0696
cargo fmt
jsdw Aug 24, 2022
5c4a846
TWeak a few types
jsdw Aug 24, 2022
9e30f78
make sure we get jsonrpsee-types, too
jsdw Aug 24, 2022
c2bd26c
Add examples for rpc_params/RpcParams
jsdw Aug 24, 2022
c70c599
more doc tweaks
jsdw Aug 24, 2022
e61b342
remove a now unneeded dev note
jsdw Aug 24, 2022
f60931d
Option<Box<RawValue>> instead to avoid allocations in some cases
jsdw Aug 25, 2022
8b86509
update docs
jsdw Aug 25, 2022
5a5924a
tweak RpcClientT trait docs
jsdw Aug 25, 2022
a9ad2d8
Tweak docs around RpcClient and RpcClientT. Don't expose RpcClientT d…
jsdw Aug 25, 2022
0ebfe08
more doc tweaking about RpcParams and undo decision not to expose Rpc…
jsdw Aug 25, 2022
beb959d
Doc tweak
jsdw Aug 25, 2022
439c43f
more doc tweaks
jsdw Aug 25, 2022
4dd18c2
Fix doc thing
jsdw Aug 25, 2022
1f393ba
Merge branch 'master' into jsdw-generic-rpc
jsdw Aug 25, 2022
ea6d6a3
Add an example of injecting a custom RPC client
jsdw Aug 25, 2022
f59c0a1
Fix a typo
jsdw Aug 25, 2022
ec08976
Address clippy things in example
jsdw Aug 26, 2022
0fdc5e0
Fix a silly typo
jsdw Aug 26, 2022
8a342b9
another clippy fix
jsdw Aug 26, 2022
e028c94
rpc_params to panic instead of returning a result, like serde_json::j…
jsdw Aug 30, 2022
682be15
fix docs
jsdw Aug 30, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion subxt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@ description = "Submit extrinsics (transactions) to a substrate node via RPC"
keywords = ["parity", "substrate", "blockchain"]

[features]
default = ["jsonrpsee"]

# Activate this to expose functionality only used for integration testing.
# The exposed functionality is subject to breaking changes at any point,
# and should not be relied upon.
integration-tests = []

# Jsonrpsee if the default RPC provider used in Subxt. However, it can be
# swapped out for an alternative implementation, and so is optional.
jsonrpsee = ["dep:jsonrpsee"]
niklasad1 marked this conversation as resolved.
Show resolved Hide resolved

[dependencies]
bitvec = { version = "1.0.0", default-features = false, features = ["alloc"] }
codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive", "full", "bit-vec"] }
Expand All @@ -26,7 +32,7 @@ scale-value = "0.5.0"
scale-decode = "0.3.0"
futures = "0.3.13"
hex = "0.4.3"
jsonrpsee = { version = "0.15.1", features = ["async-client", "client-ws-transport"] }
jsonrpsee = { version = "0.15.1", features = ["async-client", "client-ws-transport", "jsonrpsee-types"], optional = true }
serde = { version = "1.0.124", features = ["derive"] }
serde_json = "1.0.64"
thiserror = "1.0.24"
Expand Down
61 changes: 53 additions & 8 deletions subxt/src/client/online_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::{
events::EventsClient,
rpc::{
Rpc,
RpcClient,
RpcClientT,
RuntimeVersion,
},
storage::StorageClient,
Expand Down Expand Up @@ -52,12 +52,14 @@ struct Inner<T: Config> {
impl<T: Config> std::fmt::Debug for OnlineClient<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Client")
.field("rpc", &"<Rpc>")
.field("rpc", &"RpcClient")
.field("inner", &self.inner)
.finish()
}
}

// The default constructors assume Jsonrpsee.
#[cfg(feature = "jsonrpsee")]
impl<T: Config> OnlineClient<T> {
/// Construct a new [`OnlineClient`] using default settings which
/// point to a locally running node on `ws://127.0.0.1:9944`.
Expand All @@ -68,16 +70,20 @@ impl<T: Config> OnlineClient<T> {

/// Construct a new [`OnlineClient`], providing a URL to connect to.
pub async fn from_url(url: impl AsRef<str>) -> Result<OnlineClient<T>, Error> {
let client = crate::rpc::ws_client(url.as_ref()).await?;
let client = jsonrpsee_helpers::ws_client(url.as_ref())
.await
.map_err(|e| crate::error::RpcError(e.to_string()))?;
OnlineClient::from_rpc_client(client).await
}
}

/// Construct a new [`OnlineClient`] by providing the underlying [`RpcClient`]
/// to use to drive the connection.
pub async fn from_rpc_client(
rpc_client: impl Into<RpcClient>,
impl<T: Config> OnlineClient<T> {
/// Construct a new [`OnlineClient`] by providing an underlying [`RpcClientT`]
/// implementation to drive the connection.
pub async fn from_rpc_client<R: RpcClientT>(
rpc_client: R,
) -> Result<OnlineClient<T>, Error> {
let rpc = Rpc::new(rpc_client.into());
let rpc = Rpc::new(rpc_client);

let (genesis_hash, runtime_version, metadata) = future::join3(
rpc.genesis_hash(),
Expand Down Expand Up @@ -232,3 +238,42 @@ impl<T: Config> ClientRuntimeUpdater<T> {
Ok(())
}
}

// helpers for a jsonrpsee specific OnlineClient.
#[cfg(feature = "jsonrpsee")]
mod jsonrpsee_helpers {
pub use jsonrpsee::{
client_transport::ws::{
InvalidUri,
Receiver,
Sender,
Uri,
WsTransportClientBuilder,
},
core::{
client::{
Client,
ClientBuilder,
},
Error,
},
};

/// Build WS RPC client from URL
pub async fn ws_client(url: &str) -> Result<Client, Error> {
let (sender, receiver) = ws_transport(url).await?;
Ok(ClientBuilder::default()
.max_notifs_per_subscription(4096)
.build_with_tokio(sender, receiver))
}

async fn ws_transport(url: &str) -> Result<(Sender, Receiver), Error> {
let url: Uri = url
.parse()
.map_err(|e: InvalidUri| Error::Transport(e.into()))?;
WsTransportClientBuilder::default()
.build(url)
.await
.map_err(|e| Error::Transport(e.into()))
}
}
9 changes: 7 additions & 2 deletions subxt/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ pub use crate::metadata::{
InvalidMetadataError,
MetadataError,
};
pub use jsonrpsee::core::error::Error as RequestError;
pub use scale_value::scale::{
DecodeError,
EncodeError,
Expand All @@ -36,7 +35,7 @@ pub enum Error {
Codec(#[from] codec::Error),
/// Rpc error.
#[error("Rpc error: {0}")]
Rpc(#[from] RequestError),
Rpc(#[from] RpcError),
/// Serde serialization error
#[error("Serde json error: {0}")]
Serialization(#[from] serde_json::error::Error),
Expand Down Expand Up @@ -102,6 +101,12 @@ impl From<DispatchError> for Error {
}
}

/// An RPC error. Since we are generic over the RPC client that is used,
/// the error is any custom string.
#[derive(Debug, thiserror::Error)]
#[error("RPC error: {0}")]
pub struct RpcError(pub String);

/// This is our attempt to decode a runtime DispatchError. We either
/// successfully decode it into a [`ModuleError`], or we fail and keep
/// hold of the bytes, which we can attempt to decode if we have an
Expand Down
6 changes: 3 additions & 3 deletions subxt/src/events/event_subscription.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::{
client::OnlineClientT,
error::Error,
events::EventsClient,
rpc::Subscription,
Config,
};
use derivative::Derivative;
Expand All @@ -18,7 +19,6 @@ use futures::{
Stream,
StreamExt,
};
use jsonrpsee::core::client::Subscription;
use sp_runtime::traits::Header;
use std::{
marker::Unpin,
Expand All @@ -32,12 +32,12 @@ pub use super::{
FilterEvents,
};

/// A `jsonrpsee` Subscription. This forms a part of the `EventSubscription` type handed back
/// A Subscription. This forms a part of the `EventSubscription` type handed back
/// in codegen from `subscribe_finalized`, and is exposed to be used in codegen.
#[doc(hidden)]
pub type FinalizedEventSub<Header> = BoxStream<'static, Result<Header, Error>>;

/// A `jsonrpsee` Subscription. This forms a part of the `EventSubscription` type handed back
/// A Subscription. This forms a part of the `EventSubscription` type handed back
/// in codegen from `subscribe`, and is exposed to be used in codegen.
#[doc(hidden)]
pub type EventSub<Item> = Subscription<Item>;
Expand Down
86 changes: 86 additions & 0 deletions subxt/src/rpc/jsonrpsee_impl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright 2019-2022 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.

use super::{
RpcClientT,
RpcFuture,
RpcSubscription,
};
use crate::error::RpcError;
use futures::stream::{
StreamExt,
TryStreamExt,
};
use jsonrpsee::{
core::client::{
Client,
ClientT,
SubscriptionClientT,
},
types::ParamsSer,
};
use serde_json::value::{
RawValue,
Value,
};

impl RpcClientT for Client {
fn request_raw<'a>(
&'a self,
method: &'a str,
params: Option<Box<RawValue>>,
) -> RpcFuture<'a, Box<RawValue>> {
Box::pin(async move {
let params = prep_params_for_jsonrpsee(params)?;
let res = ClientT::request(self, method, Some(params))
.await
.map_err(|e| RpcError(e.to_string()))?;
Ok(res)
})
}

fn subscribe_raw<'a>(
&'a self,
sub: &'a str,
params: Option<Box<RawValue>>,
unsub: &'a str,
) -> RpcFuture<'a, RpcSubscription> {
Box::pin(async move {
let params = prep_params_for_jsonrpsee(params)?;
let sub = SubscriptionClientT::subscribe::<Box<RawValue>>(
self,
sub,
Some(params),
unsub,
)
.await
.map_err(|e| RpcError(e.to_string()))?
.map_err(|e| RpcError(e.to_string()))
.boxed();
Ok(sub)
})
}
}

// This is ugly; we have to encode to Value's to be compat with the jsonrpc interface.
// Remove and simplify this once something like https://github.com/paritytech/jsonrpsee/issues/862 is in:
fn prep_params_for_jsonrpsee(
params: Option<Box<RawValue>>,
) -> Result<ParamsSer<'static>, RpcError> {
let params = match params {
Some(params) => params,
// No params? avoid any work and bail early.
None => return Ok(ParamsSer::Array(Vec::new())),
};
let val = serde_json::to_value(&params).expect("RawValue guarantees valid JSON");
let arr = match val {
Value::Array(arr) => Ok(arr),
_ => {
Err(RpcError(format!(
"RPC Params are expected to be an array but got {params}"
)))
}
}?;
Ok(ParamsSer::Array(arr))
}
76 changes: 76 additions & 0 deletions subxt/src/rpc/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright 2019-2022 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.

//! RPC types and client for interacting with a substrate node.
//!
//! These is used behind the scenes by various `subxt` APIs, but can
//! also be used directly.
//!
//! - [`Rpc`] is the highest level wrapper, and the one you will run into
//! first. It contains the higher level methods for interacting with a node.
//! - [`RpcClient`] is what [`Rpc`] uses to actually talk to a node, offering
//! a [`RpcClient::request`] and [`RpcClient::subscribe`] method to do so.
//! - [`RpcClientT`] is the underlying dynamic RPC implementation. This provides
//! the low level [`RpcClientT::request_raw`] and [`RpcClientT::subscribe_raw`]
//! methods. This can be swapped out for a custom implementation, but by default
//! we'll rely on `jsonrpsee` for this.
//!
//! # Example
//!
//! Fetching storage keys
//!
//! ```no_run
//! use subxt::{ PolkadotConfig, OnlineClient, storage::StorageKey };
//!
//! #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
//! pub mod polkadot {}
//!
//! # #[tokio::main]
//! # async fn main() {
//! let api = OnlineClient::<PolkadotConfig>::new().await.unwrap();
//!
//! let key = polkadot::storage()
//! .xcm_pallet()
//! .version_notifiers_root()
//! .to_bytes();
//!
//! // Fetch up to 10 keys.
//! let keys = api
//! .rpc()
//! .storage_keys_paged(&key, 10, None, None)
//! .await
//! .unwrap();
//!
//! for key in keys.iter() {
//! println!("Key: 0x{}", hex::encode(&key));
//! }
//! # }
//! ```

// Allow an `rpc.rs` file in the `rpc` folder to align better
// with other file names for their types.
#![allow(clippy::module_inception)]

#[cfg(feature = "jsonrpsee")]
mod jsonrpsee_impl;

mod rpc;
mod rpc_client;
mod rpc_client_t;

// Expose the `Rpc` struct and any associated types.
pub use rpc::*;

pub use rpc_client_t::{
RpcClientT,
RpcFuture,
RpcSubscription,
};

pub use rpc_client::{
rpc_params,
RpcClient,
RpcParams,
Subscription,
};
Loading