Rework light client (#1475)

* WIP second pass over light client code for simpler API

* First pass new light client

* pub(crate) LightClientRpc::new_raw(), and fmt

* Update examples and add back a way to configure boot nodes and fetch chainspec from a URL

* Fix light client examples

* remove unused deps and tidy lightclient feature flags

* fix wasm error

* LightClientRpc can be cloned

* update light client tests

* Other small fixes

* exclude mod unless jsonrpsee

* Fix wasm-lightclient-tests

* add back docsrs bit and web+native feature flag compile error

* update book and light client example names

* fix docs
This commit is contained in:
James Wilson
2024-03-15 15:21:06 +00:00
committed by GitHub
parent 4831f816f2
commit b069c4425a
32 changed files with 1236 additions and 1590 deletions
+2 -2
View File
@@ -53,10 +53,10 @@ impl<T: Config> LegacyBackendBuilder<T> {
/// Given an [`RpcClient`] to use to make requests, this returns a [`LegacyBackend`],
/// which implements the [`Backend`] trait.
pub fn build(self, client: RpcClient) -> LegacyBackend<T> {
pub fn build(self, client: impl Into<RpcClient>) -> LegacyBackend<T> {
LegacyBackend {
storage_page_size: self.storage_page_size,
methods: LegacyRpcMethods::new(client),
methods: LegacyRpcMethods::new(client.into()),
}
}
}
+53
View File
@@ -0,0 +1,53 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use super::{RawRpcFuture, RawRpcSubscription, RpcClientT};
use crate::error::RpcError;
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
.map_err(lc_err_to_rpc_err)?;
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
.map_err(lc_err_to_rpc_err)?;
let id = Some(sub.id().to_owned());
let stream = sub
.map_err(|e| RpcError::ClientError(Box::new(e)))
.boxed();
Ok(RawRpcSubscription { id, stream })
})
}
}
fn lc_err_to_rpc_err(err: LightClientRpcError) -> RpcError {
match err {
LightClientRpcError::JsonRpcError(e) => RpcError::ClientError(Box::new(e)),
LightClientRpcError::SmoldotError(e) => RpcError::RequestRejected(e),
LightClientRpcError::BackgroundTaskDropped => RpcError::SubscriptionDropped,
}
}
+4
View File
@@ -60,6 +60,10 @@ crate::macros::cfg_jsonrpsee! {
mod jsonrpsee_impl;
}
crate::macros::cfg_unstable_light_client! {
mod lightclient_impl;
}
crate::macros::cfg_reconnecting_rpc_client! {
mod reconnecting_jsonrpsee_impl;
pub use reconnecting_jsonrpsee_ws_client as reconnecting_rpc_client;
+6
View File
@@ -79,6 +79,12 @@ impl RpcClient {
}
}
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()
+5 -2
View File
@@ -75,9 +75,12 @@ impl<T: Config> UnstableBackendBuilder<T> {
/// Given an [`RpcClient`] to use to make requests, this returns a tuple of an [`UnstableBackend`],
/// which implements the [`Backend`] trait, and an [`UnstableBackendDriver`] which must be polled in
/// order for the backend to make progress.
pub fn build(self, client: RpcClient) -> (UnstableBackend<T>, UnstableBackendDriver<T>) {
pub fn build(
self,
client: impl Into<RpcClient>,
) -> (UnstableBackend<T>, UnstableBackendDriver<T>) {
// Construct the underlying follow_stream layers:
let rpc_methods = UnstableRpcMethods::new(client);
let rpc_methods = UnstableRpcMethods::new(client.into());
let follow_stream =
follow_stream::FollowStream::<T::Hash>::from_methods(rpc_methods.clone());
let follow_stream_unpin = follow_stream_unpin::FollowStreamUnpin::<T::Hash>::from_methods(
+16 -21
View File
@@ -8,25 +8,33 @@
//! node. This means that you don't have to trust a specific node when interacting with some chain.
//!
//! This feature is currently unstable. Use the `unstable-light-client` feature flag to enable it.
//! To use this in WASM environments, also enable the `web` feature flag.
//! To use this in WASM environments, enable the `web` feature flag and disable the "native" one.
//!
//! To connect to a blockchain network, the Light Client requires a trusted sync state of the network,
//! known as a _chain spec_. One way to obtain this is by making a `sync_state_genSyncSpec` RPC call to a
//! trusted node belonging to the chain that you wish to interact with.
//!
//! The following is an example of fetching the chain spec from a local running node on port 9933:
//! Subxt exposes a utility method to obtain the chain spec: [`crate::utils::fetch_chainspec_from_rpc_node()`].
//! Alternately, you can manually make an RPC call to `sync_state_genSyncSpec` like do (assuming a node running
//! locally on port 9933):
//!
//! ```bash
//! curl -H "Content-Type: application/json" -d '{"id":1, "jsonrpc":"2.0", "method": "sync_state_genSyncSpec", "params":[true]}' http://localhost:9933/ | jq .result > chain_spec.json
//! ```
//!
//! Alternately, you can have the `LightClient` download the chain spec from a trusted node when it
//! initializes, which is not recommended in production but is useful for examples and testing, as below.
//!
//! ## Examples
//!
//! ### Basic Example
//!
//! This basic example uses some already-known chain specs to connect to a relay chain and parachain
//! and stream information about their finalized blocks:
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/light_client_basic.rs")]
//! ```
//!
//! ### Connecting to a local node
//!
//! This example connects to a local chain and submits a transaction. To run this, you first need
//! to have a local polkadot node running using the following command:
//!
@@ -34,23 +42,10 @@
//! polkadot --dev --node-key 0000000000000000000000000000000000000000000000000000000000000001
//! ```
//!
//! Leave that running for a minute, and then you can run the example using the following command
//! in the `subxt` crate:
//!
//! ```bash
//! cargo run --example light_client_tx_basic --features=unstable-light-client
//! ```
//!
//! This is the code that will be executed:
//! Then, the following code will download a chain spec from this local node, alter the bootnodes
//! to point only to the local node, and then submit a transaction through it.
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/light_client_tx_basic.rs")]
#![doc = include_str!("../../../examples/light_client_local_node.rs")]
//! ```
//!
//! ### Connecting to a parachain
//!
//! This example connects to a parachain using the light client. Currently, it's quite verbose to do this.
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/light_client_parachains.rs")]
//! ```
-336
View File
@@ -1,336 +0,0 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use super::{rpc::LightClientRpc, LightClient, LightClientError};
use crate::backend::rpc::RpcClient;
use crate::client::RawLightClient;
use crate::macros::{cfg_jsonrpsee_native, cfg_jsonrpsee_web};
use crate::{config::Config, error::Error, OnlineClient};
use std::num::NonZeroU32;
use subxt_lightclient::{smoldot, AddedChain};
#[cfg(feature = "jsonrpsee")]
use crate::utils::validate_url_is_secure;
/// Builder for [`LightClient`].
#[derive(Clone, Debug)]
pub struct LightClientBuilder<T: Config> {
max_pending_requests: NonZeroU32,
max_subscriptions: u32,
bootnodes: Option<Vec<serde_json::Value>>,
potential_relay_chains: Option<Vec<smoldot::ChainId>>,
_marker: std::marker::PhantomData<T>,
}
impl<T: Config> Default for LightClientBuilder<T> {
fn default() -> Self {
Self {
max_pending_requests: NonZeroU32::new(128)
.expect("Valid number is greater than zero; qed"),
max_subscriptions: 1024,
bootnodes: None,
potential_relay_chains: None,
_marker: std::marker::PhantomData,
}
}
}
impl<T: Config> LightClientBuilder<T> {
/// Create a new [`LightClientBuilder`].
pub fn new() -> LightClientBuilder<T> {
LightClientBuilder::default()
}
/// Overwrite the bootnodes of the chain specification.
///
/// Can be used to provide trusted entities to the chain spec, or for
/// testing environments.
pub fn bootnodes<'a>(mut self, bootnodes: impl IntoIterator<Item = &'a str>) -> Self {
self.bootnodes = Some(bootnodes.into_iter().map(Into::into).collect());
self
}
/// Maximum number of JSON-RPC in the queue of requests waiting to be processed.
/// This parameter is necessary for situations where the JSON-RPC clients aren't
/// trusted. If you control all the requests that are sent out and don't want them
/// to fail, feel free to pass `u32::max_value()`.
///
/// Default is 128.
pub fn max_pending_requests(mut self, max_pending_requests: NonZeroU32) -> Self {
self.max_pending_requests = max_pending_requests;
self
}
/// Maximum number of active subscriptions before new ones are automatically
/// rejected. Any JSON-RPC request that causes the server to generate notifications
/// counts as a subscription.
///
/// Default is 1024.
pub fn max_subscriptions(mut self, max_subscriptions: u32) -> Self {
self.max_subscriptions = max_subscriptions;
self
}
/// If the chain spec defines a parachain, contains the list of relay chains to choose
/// from. Ignored if not a parachain.
///
/// This field is necessary because multiple different chain can have the same identity.
///
/// For example: if user A adds a chain named "Kusama", then user B adds a different chain
/// also named "Kusama", then user B adds a parachain whose relay chain is "Kusama", it would
/// be wrong to connect to the "Kusama" created by user A.
pub fn potential_relay_chains(
mut self,
potential_relay_chains: impl IntoIterator<Item = smoldot::ChainId>,
) -> Self {
self.potential_relay_chains = Some(potential_relay_chains.into_iter().collect());
self
}
/// Build the light client with specified URL to connect to.
/// You must provide the port number in the URL.
///
/// ## Panics
///
/// The panic behaviour depends on the feature flag being used:
///
/// ### Native
///
/// Panics when called outside of a `tokio` runtime context.
///
/// ### Web
///
/// If smoldot panics, then the promise created will be leaked. For more details, see
/// https://docs.rs/wasm-bindgen-futures/latest/wasm_bindgen_futures/fn.future_to_promise.html.
#[cfg(feature = "jsonrpsee")]
#[cfg_attr(docsrs, doc(cfg(feature = "jsonrpsee")))]
pub async fn build_from_url<Url: AsRef<str>>(self, url: Url) -> Result<LightClient<T>, Error> {
validate_url_is_secure(url.as_ref())?;
self.build_from_insecure_url(url).await
}
/// Build the light client with specified URL to connect to. Allows insecure URLs (no SSL, ws:// or http://).
///
/// For secure connections only, please use [`crate::LightClientBuilder::build_from_url`].
#[cfg(feature = "jsonrpsee")]
pub async fn build_from_insecure_url<Url: AsRef<str>>(
self,
url: Url,
) -> Result<LightClient<T>, Error> {
let chain_spec = fetch_url(url.as_ref()).await?;
self.build_client(chain_spec).await
}
/// Build the light client from chain spec.
///
/// The most important field of the configuration is the chain specification.
/// This is a JSON document containing all the information necessary for the client to
/// connect to said chain.
///
/// The chain spec must be obtained from a trusted entity.
///
/// It can be fetched from a trusted node with the following command:
/// ```bash
/// curl -H "Content-Type: application/json" -d '{"id":1, "jsonrpc":"2.0", "method": "sync_state_genSyncSpec", "params":[true]}' http://localhost:9944/ | jq .result > res.spec
/// ```
///
/// # Note
///
/// For testing environments, please populate the "bootNodes" if the not already provided.
/// See [`Self::bootnodes`] for more details.
///
/// ## Panics
///
/// The panic behaviour depends on the feature flag being used:
///
/// ### Native
///
/// Panics when called outside of a `tokio` runtime context.
///
/// ### Web
///
/// If smoldot panics, then the promise created will be leaked. For more details, see
/// https://docs.rs/wasm-bindgen-futures/latest/wasm_bindgen_futures/fn.future_to_promise.html.
pub async fn build(self, chain_spec: &str) -> Result<LightClient<T>, Error> {
let chain_spec = serde_json::from_str(chain_spec)
.map_err(|_| Error::LightClient(LightClientError::InvalidChainSpec))?;
self.build_client(chain_spec).await
}
/// Build the light client.
async fn build_client(
self,
mut chain_spec: serde_json::Value,
) -> Result<LightClient<T>, Error> {
// Set custom bootnodes if provided.
if let Some(bootnodes) = self.bootnodes {
if let serde_json::Value::Object(map) = &mut chain_spec {
map.insert("bootNodes".to_string(), serde_json::Value::Array(bootnodes));
}
}
let config = smoldot::AddChainConfig {
specification: &chain_spec.to_string(),
json_rpc: smoldot::AddChainConfigJsonRpc::Enabled {
max_pending_requests: self.max_pending_requests,
max_subscriptions: self.max_subscriptions,
},
potential_relay_chains: self.potential_relay_chains.unwrap_or_default().into_iter(),
database_content: "",
user_data: (),
};
let raw_rpc = LightClientRpc::new(config)?;
build_client_from_rpc(raw_rpc).await
}
}
/// Raw builder for [`RawLightClient`].
#[derive(Default)]
pub struct RawLightClientBuilder {
chains: Vec<AddedChain>,
}
impl RawLightClientBuilder {
/// Create a new [`RawLightClientBuilder`].
pub fn new() -> RawLightClientBuilder {
RawLightClientBuilder::default()
}
/// Adds a new chain to the list of chains synchronized by the light client.
pub fn add_chain(
mut self,
chain_id: smoldot::ChainId,
rpc_responses: smoldot::JsonRpcResponses,
) -> Self {
self.chains.push(AddedChain {
chain_id,
rpc_responses,
});
self
}
/// Construct a [`RawLightClient`] from a raw smoldot client.
///
/// The provided `chain_id` is the chain with which the current instance of light client will interact.
/// To target a different chain call the [`LightClient::target_chain`] method.
pub async fn build<TPlatform: smoldot::PlatformRef>(
self,
client: smoldot::Client<TPlatform>,
) -> Result<RawLightClient, Error> {
// The raw subxt light client that spawns the smoldot background task.
let raw_rpc: subxt_lightclient::RawLightClientRpc =
subxt_lightclient::LightClientRpc::new_from_client(client, self.chains.into_iter());
// The crate implementation of `RpcClientT` over the raw subxt light client.
let raw_rpc = crate::client::light_client::rpc::RawLightClientRpc::from_inner(raw_rpc);
Ok(RawLightClient { raw_rpc })
}
}
/// Build the light client from a raw rpc client.
async fn build_client_from_rpc<T: Config>(
raw_rpc: LightClientRpc,
) -> Result<LightClient<T>, Error> {
let chain_id = raw_rpc.chain_id();
let rpc_client = RpcClient::new(raw_rpc);
let client = OnlineClient::<T>::from_rpc_client(rpc_client).await?;
Ok(LightClient { client, chain_id })
}
/// Fetch the chain spec from the URL.
#[cfg(feature = "jsonrpsee")]
async fn fetch_url(url: impl AsRef<str>) -> Result<serde_json::Value, Error> {
use jsonrpsee::core::client::{ClientT, SubscriptionClientT};
use jsonrpsee::rpc_params;
use serde_json::value::RawValue;
let client = jsonrpsee_helpers::client(url.as_ref()).await?;
let result = client
.request("sync_state_genSyncSpec", jsonrpsee::rpc_params![true])
.await
.map_err(|err| Error::Rpc(crate::error::RpcError::ClientError(Box::new(err))))?;
// Subscribe to the finalized heads of the chain.
let mut subscription = SubscriptionClientT::subscribe::<Box<RawValue>, _>(
&client,
"chain_subscribeFinalizedHeads",
rpc_params![],
"chain_unsubscribeFinalizedHeads",
)
.await
.map_err(|err| Error::Rpc(crate::error::RpcError::ClientError(Box::new(err))))?;
// We must ensure that the finalized block of the chain is not the block included
// in the chainSpec.
// This is a temporary workaround for: https://github.com/smol-dot/smoldot/issues/1562.
// The first finalized block that is received might by the finalized block could be the one
// included in the chainSpec. Decoding the chainSpec for this purpose is too complex.
let _ = subscription.next().await;
let _ = subscription.next().await;
Ok(result)
}
cfg_jsonrpsee_native! {
mod jsonrpsee_helpers {
use crate::error::{Error, LightClientError};
use tokio_util::compat::Compat;
pub use jsonrpsee::{
client_transport::ws::{self, EitherStream, Url, WsTransportClientBuilder},
core::client::Client,
};
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 url = Url::parse(url).map_err(|_| Error::LightClient(LightClientError::InvalidUrl))?;
if url.scheme() != "ws" && url.scheme() != "wss" {
return Err(Error::LightClient(LightClientError::InvalidScheme));
}
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: Url) -> Result<(Sender, Receiver), Error> {
WsTransportClientBuilder::default()
.build(url)
.await
.map_err(|_| Error::LightClient(LightClientError::Handshake))
}
}
}
cfg_jsonrpsee_web! {
mod jsonrpsee_helpers {
use crate::error::{Error, LightClientError};
pub use jsonrpsee::{
client_transport::web,
core::client::{Client, ClientBuilder},
};
/// Build web RPC client from URL
pub async fn client(url: &str) -> Result<Client, Error> {
let (sender, receiver) = web::connect(url)
.await
.map_err(|_| Error::LightClient(LightClientError::Handshake))?;
Ok(ClientBuilder::default()
.max_buffer_capacity_per_subscription(4096)
.build_with_wasm(sender, receiver))
}
}
}
-194
View File
@@ -1,194 +0,0 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! This module provides support for light clients.
mod builder;
mod rpc;
use crate::{
backend::rpc::RpcClient,
blocks::BlocksClient,
client::{OfflineClientT, OnlineClientT},
config::Config,
constants::ConstantsClient,
custom_values::CustomValuesClient,
events::EventsClient,
runtime_api::RuntimeApiClient,
storage::StorageClient,
tx::TxClient,
OnlineClient,
};
pub use builder::{LightClientBuilder, RawLightClientBuilder};
use derivative::Derivative;
use subxt_lightclient::LightClientRpcError;
// Re-export smoldot related objects.
pub use subxt_lightclient::smoldot;
/// Light client error.
#[derive(Debug, thiserror::Error)]
pub enum LightClientError {
/// Error originated from the low-level RPC layer.
#[error("Rpc error: {0}")]
Rpc(LightClientRpcError),
/// The background task is closed.
#[error("Failed to communicate with the background task.")]
BackgroundClosed,
/// Invalid RPC parameters cannot be serialized as JSON string.
#[error("RPC parameters cannot be serialized as JSON string.")]
InvalidParams,
/// The provided URL scheme is invalid.
///
/// Supported versions: WS, WSS.
#[error("The provided URL scheme is invalid.")]
InvalidScheme,
/// The provided URL is invalid.
#[error("The provided URL scheme is invalid.")]
InvalidUrl,
/// The provided chain spec is invalid.
#[error("The provided chain spec is not a valid JSON object.")]
InvalidChainSpec,
/// Handshake error while connecting to a node.
#[error("WS handshake failed.")]
Handshake,
}
/// The raw light-client RPC implementation that is used to connect with the chain.
#[derive(Clone)]
pub struct RawLightClient {
raw_rpc: rpc::RawLightClientRpc,
}
impl RawLightClient {
/// Construct a [`RawLightClient`] using its builder interface.
///
/// The raw builder is utilized for constructing light-clients from a low
/// level smoldot client.
///
/// This is especially useful when you want to gain access to the smoldot client.
/// For example, you may want to connect to multiple chains and/or parachains while reusing the
/// same smoldot client under the hood. Or you may want to configure different values for
/// smoldot internal buffers, number of subscriptions and relay chains.
///
/// # Note
///
/// If you are unsure, please use [`LightClient::builder`] instead.
pub fn builder() -> RawLightClientBuilder {
RawLightClientBuilder::default()
}
/// Target a different chain identified by the provided chain ID for requests.
///
/// The provided chain ID is provided by the `smoldot_light::Client::add_chain` and it must
/// match one of the `smoldot_light::JsonRpcResponses` provided in [`RawLightClientBuilder::add_chain`].
///
/// # Note
///
/// This uses the same underlying instance spawned by the builder.
pub async fn for_chain<TChainConfig: Config>(
&self,
chain_id: smoldot::ChainId,
) -> Result<LightClient<TChainConfig>, crate::Error> {
let raw_rpc = self.raw_rpc.for_chain(chain_id);
let rpc_client = RpcClient::new(raw_rpc.clone());
let client = OnlineClient::<TChainConfig>::from_rpc_client(rpc_client).await?;
Ok(LightClient { client, chain_id })
}
}
/// The light-client RPC implementation that is used to connect with the chain.
#[derive(Derivative)]
#[derivative(Clone(bound = ""))]
pub struct LightClient<T: Config> {
client: OnlineClient<T>,
chain_id: smoldot::ChainId,
}
impl<T: Config> LightClient<T> {
/// Construct a [`LightClient`] using its builder interface.
pub fn builder() -> LightClientBuilder<T> {
LightClientBuilder::new()
}
// We add the below impls so that we don't need to
// think about importing the OnlineClientT/OfflineClientT
// traits to use these things:
/// Return the [`crate::Metadata`] used in this client.
fn metadata(&self) -> crate::Metadata {
self.client.metadata()
}
/// Return the genesis hash.
fn genesis_hash(&self) -> <T as Config>::Hash {
self.client.genesis_hash()
}
/// Return the runtime version.
fn runtime_version(&self) -> crate::backend::RuntimeVersion {
self.client.runtime_version()
}
/// Work with transactions.
pub fn tx(&self) -> TxClient<T, Self> {
<Self as OfflineClientT<T>>::tx(self)
}
/// Work with events.
pub fn events(&self) -> EventsClient<T, Self> {
<Self as OfflineClientT<T>>::events(self)
}
/// Work with storage.
pub fn storage(&self) -> StorageClient<T, Self> {
<Self as OfflineClientT<T>>::storage(self)
}
/// Access constants.
pub fn constants(&self) -> ConstantsClient<T, Self> {
<Self as OfflineClientT<T>>::constants(self)
}
/// Access custom types.
pub fn custom_values(&self) -> CustomValuesClient<T, Self> {
<Self as OfflineClientT<T>>::custom_values(self)
}
/// Work with blocks.
pub fn blocks(&self) -> BlocksClient<T, Self> {
<Self as OfflineClientT<T>>::blocks(self)
}
/// Work with runtime API.
pub fn runtime_api(&self) -> RuntimeApiClient<T, Self> {
<Self as OfflineClientT<T>>::runtime_api(self)
}
/// Returns the chain ID of the current light-client.
pub fn chain_id(&self) -> smoldot::ChainId {
self.chain_id
}
}
impl<T: Config> OnlineClientT<T> for LightClient<T> {
fn backend(&self) -> &dyn crate::backend::Backend<T> {
self.client.backend()
}
}
impl<T: Config> OfflineClientT<T> for LightClient<T> {
fn metadata(&self) -> crate::Metadata {
self.metadata()
}
fn genesis_hash(&self) -> <T as Config>::Hash {
self.genesis_hash()
}
fn runtime_version(&self) -> crate::backend::RuntimeVersion {
self.runtime_version()
}
}
-162
View File
@@ -1,162 +0,0 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use super::{smoldot, LightClientError};
use crate::{
backend::rpc::{RawRpcFuture, RawRpcSubscription, RpcClientT},
error::{Error, RpcError},
};
use futures::StreamExt;
use serde_json::value::RawValue;
use tokio_stream::wrappers::UnboundedReceiverStream;
pub const LOG_TARGET: &str = "subxt-rpc-light-client";
/// The raw light-client RPC implementation that is used to connect with the chain.
#[derive(Clone)]
pub struct RawLightClientRpc(subxt_lightclient::RawLightClientRpc);
impl RawLightClientRpc {
/// Constructs a new [`RawLightClientRpc`] from a low level [`subxt_lightclient::RawLightClientRpc`].
pub fn from_inner(client: subxt_lightclient::RawLightClientRpc) -> RawLightClientRpc {
RawLightClientRpc(client)
}
/// Constructs a new [`LightClientRpc`] that communicates with the provided chain.
pub fn for_chain(&self, chain_id: smoldot::ChainId) -> LightClientRpc {
LightClientRpc(self.0.for_chain(chain_id))
}
}
/// The light-client RPC implementation that is used to connect with the chain.
#[derive(Clone)]
pub struct LightClientRpc(subxt_lightclient::LightClientRpc);
impl LightClientRpc {
/// Constructs a new [`LightClientRpc`], providing the chain specification.
///
/// The chain specification can be downloaded from a trusted network via
/// the `sync_state_genSyncSpec` RPC method. This parameter expects the
/// chain spec in text format (ie not in hex-encoded scale-encoded as RPC methods
/// will provide).
///
/// ## Panics
///
/// The panic behaviour depends on the feature flag being used:
///
/// ### Native
///
/// Panics when called outside of a `tokio` runtime context.
///
/// ### Web
///
/// If smoldot panics, then the promise created will be leaked. For more details, see
/// https://docs.rs/wasm-bindgen-futures/latest/wasm_bindgen_futures/fn.future_to_promise.html.
pub fn new(
config: smoldot::AddChainConfig<'_, (), impl Iterator<Item = smoldot::ChainId>>,
) -> Result<LightClientRpc, Error> {
let rpc = subxt_lightclient::LightClientRpc::new(config).map_err(LightClientError::Rpc)?;
Ok(LightClientRpc(rpc))
}
/// Returns the chain ID of the current light-client.
pub fn chain_id(&self) -> smoldot::ChainId {
self.0.chain_id()
}
}
impl RpcClientT for LightClientRpc {
fn request_raw<'a>(
&'a self,
method: &'a str,
params: Option<Box<RawValue>>,
) -> RawRpcFuture<'a, Box<RawValue>> {
let client = self.clone();
let chain_id = self.chain_id();
Box::pin(async move {
let params = match params {
Some(params) => serde_json::to_string(&params).map_err(|_| {
RpcError::ClientError(Box::new(LightClientError::InvalidParams))
})?,
None => "[]".into(),
};
// Fails if the background is closed.
let rx = client
.0
.method_request(method.to_string(), params)
.map_err(|_| RpcError::ClientError(Box::new(LightClientError::BackgroundClosed)))?;
// Fails if the background is closed.
let response = rx
.await
.map_err(|_| RpcError::ClientError(Box::new(LightClientError::BackgroundClosed)))?;
tracing::trace!(target: LOG_TARGET, "RPC response={:?} chain={:?}", response, chain_id);
response.map_err(|err| RpcError::ClientError(Box::new(err)))
})
}
fn subscribe_raw<'a>(
&'a self,
sub: &'a str,
params: Option<Box<RawValue>>,
unsub: &'a str,
) -> RawRpcFuture<'a, RawRpcSubscription> {
let client = self.clone();
let chain_id = self.chain_id();
Box::pin(async move {
tracing::trace!(
target: LOG_TARGET,
"Subscribe to {:?} with params {:?} chain={:?}",
sub,
params,
chain_id,
);
let params = match params {
Some(params) => serde_json::to_string(&params).map_err(|_| {
RpcError::ClientError(Box::new(LightClientError::InvalidParams))
})?,
None => "[]".into(),
};
// Fails if the background is closed.
let (sub_id, notif) = client
.0
.subscription_request(sub.to_string(), params, unsub.to_string())
.map_err(|_| RpcError::ClientError(Box::new(LightClientError::BackgroundClosed)))?;
// Fails if the background is closed.
let result = sub_id
.await
.map_err(|_| RpcError::ClientError(Box::new(LightClientError::BackgroundClosed)))?
.map_err(|err| {
RpcError::ClientError(Box::new(LightClientError::Rpc(
subxt_lightclient::LightClientRpcError::Request(err.to_string()),
)))
})?;
let sub_id = result
.get()
.trim_start_matches('"')
.trim_end_matches('"')
.to_string();
tracing::trace!(target: LOG_TARGET, "Received subscription={} chain={:?}", sub_id, chain_id);
let stream = UnboundedReceiverStream::new(notif);
let rpc_subscription = RawRpcSubscription {
stream: Box::pin(stream.map(Ok)),
id: Some(sub_id),
};
Ok(rpc_subscription)
})
}
}
-8
View File
@@ -11,14 +11,6 @@
mod offline_client;
mod online_client;
crate::macros::cfg_unstable_light_client! {
mod light_client;
pub use light_client::{
LightClient, LightClientBuilder, LightClientError, RawLightClient, RawLightClientBuilder,
};
}
pub use offline_client::{OfflineClient, OfflineClientT};
pub use online_client::{
ClientRuntimeUpdater, OnlineClient, OnlineClientT, RuntimeUpdaterStream, Update, UpgradeError,
+6 -2
View File
@@ -84,7 +84,10 @@ impl<T: Config> OnlineClient<T> {
impl<T: Config> OnlineClient<T> {
/// Construct a new [`OnlineClient`] by providing an [`RpcClient`] to drive the connection.
/// This will use the current default [`Backend`], which may change in future releases.
pub async fn from_rpc_client(rpc_client: RpcClient) -> Result<OnlineClient<T>, Error> {
pub async fn from_rpc_client(
rpc_client: impl Into<RpcClient>,
) -> Result<OnlineClient<T>, Error> {
let rpc_client = rpc_client.into();
let backend = Arc::new(LegacyBackend::builder().build(rpc_client));
OnlineClient::from_backend(backend).await
}
@@ -106,8 +109,9 @@ impl<T: Config> OnlineClient<T> {
genesis_hash: T::Hash,
runtime_version: RuntimeVersion,
metadata: impl Into<Metadata>,
rpc_client: RpcClient,
rpc_client: impl Into<RpcClient>,
) -> Result<OnlineClient<T>, Error> {
let rpc_client = rpc_client.into();
let backend = Arc::new(LegacyBackend::builder().build(rpc_client));
OnlineClient::from_backend_with(genesis_hash, runtime_version, metadata, backend)
}
+1 -1
View File
@@ -7,7 +7,7 @@
mod dispatch_error;
crate::macros::cfg_unstable_light_client! {
pub use crate::client::LightClientError;
pub use subxt_lightclient::LightClientError;
}
// Re-export dispatch error types:
+5
View File
@@ -61,6 +61,11 @@ pub mod utils;
#[macro_use]
mod macros;
// Expose light client bits
cfg_unstable_light_client! {
pub use subxt_lightclient as lightclient;
}
// Expose a few of the most common types at root,
// but leave most types behind their respective modules.
pub use crate::{
+113
View File
@@ -0,0 +1,113 @@
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use crate::macros::{cfg_jsonrpsee_native, cfg_jsonrpsee_web};
use serde_json::value::RawValue;
/// Possible errors encountered trying to fetch a chain spec from an RPC node.
#[derive(thiserror::Error, Debug)]
#[allow(missing_docs)]
pub enum FetchChainspecError {
#[error("Cannot fetch chain spec: RPC error: {0}.")]
RpcError(String),
#[error("Cannot fetch chain spec: Invalid URL.")]
InvalidUrl,
#[error("Cannot fetch chain spec: Invalid URL scheme.")]
InvalidScheme,
#[error("Cannot fetch chain spec: Handshake error establishing WS connection.")]
HandshakeError,
}
/// Fetch a chain spec from an RPC node at the given URL.
pub async fn fetch_chainspec_from_rpc_node(
url: impl AsRef<str>,
) -> Result<Box<RawValue>, FetchChainspecError> {
use jsonrpsee::core::client::{ClientT, SubscriptionClientT};
use jsonrpsee::rpc_params;
let client = jsonrpsee_helpers::client(url.as_ref()).await?;
let result = client
.request("sync_state_genSyncSpec", jsonrpsee::rpc_params![true])
.await
.map_err(|err| FetchChainspecError::RpcError(err.to_string()))?;
// Subscribe to the finalized heads of the chain.
let mut subscription = SubscriptionClientT::subscribe::<Box<RawValue>, _>(
&client,
"chain_subscribeFinalizedHeads",
rpc_params![],
"chain_unsubscribeFinalizedHeads",
)
.await
.map_err(|err| FetchChainspecError::RpcError(err.to_string()))?;
// We must ensure that the finalized block of the chain is not the block included
// in the chainSpec.
// This is a temporary workaround for: https://github.com/smol-dot/smoldot/issues/1562.
// The first finalized block that is received might by the finalized block could be the one
// included in the chainSpec. Decoding the chainSpec for this purpose is too complex.
let _ = subscription.next().await;
let _ = subscription.next().await;
Ok(result)
}
cfg_jsonrpsee_native! {
mod jsonrpsee_helpers {
use super::FetchChainspecError;
use tokio_util::compat::Compat;
pub use jsonrpsee::{
client_transport::ws::{self, EitherStream, Url, WsTransportClientBuilder},
core::client::Client,
};
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, FetchChainspecError> {
let url = Url::parse(url).map_err(|_| FetchChainspecError::InvalidUrl)?;
if url.scheme() != "ws" && url.scheme() != "wss" {
return Err(FetchChainspecError::InvalidScheme);
}
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: Url) -> Result<(Sender, Receiver), FetchChainspecError> {
WsTransportClientBuilder::default()
.build(url)
.await
.map_err(|_| FetchChainspecError::HandshakeError)
}
}
}
cfg_jsonrpsee_web! {
mod jsonrpsee_helpers {
use super::FetchChainspecError;
pub use jsonrpsee::{
client_transport::web,
core::client::{Client, ClientBuilder},
};
/// Build web RPC client from URL
pub async fn client(url: &str) -> Result<Client, FetchChainspecError> {
let (sender, receiver) = web::connect(url)
.await
.map_err(|_| FetchChainspecError::HandshakeError)?;
Ok(ClientBuilder::default()
.max_buffer_capacity_per_subscription(4096)
.build_with_wasm(sender, receiver))
}
}
}
+6
View File
@@ -14,6 +14,7 @@ mod unchecked_extrinsic;
mod wrapper_opaque;
use crate::error::RpcError;
use crate::macros::cfg_jsonrpsee;
use crate::Error;
use codec::{Compact, Decode, Encode};
use derivative::Derivative;
@@ -27,6 +28,11 @@ pub use static_type::Static;
pub use unchecked_extrinsic::UncheckedExtrinsic;
pub use wrapper_opaque::WrapperKeepOpaque;
cfg_jsonrpsee! {
mod fetch_chain_spec;
pub use fetch_chain_spec::{fetch_chainspec_from_rpc_node, FetchChainspecError};
}
// Used in codegen
#[doc(hidden)]
pub use primitive_types::{H160, H256, H512};