diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index fb945920e4..73d477fc18 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -109,10 +109,10 @@ jobs: - name: Run clippy run: | - cargo clippy --all-targets --features unstable-light-client -- -D warnings + cargo clippy --all-targets --features light-client -- -D warnings cargo clippy -p subxt-lightclient --no-default-features --features web -- -D warnings cargo clippy -p subxt --no-default-features --features web -- -D warnings - cargo clippy -p subxt --no-default-features --features web,unstable-light-client -- -D warnings + cargo clippy -p subxt --no-default-features --features web,light-client -- -D warnings - if: "failure()" uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5 @@ -144,7 +144,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy - args: -p subxt --no-default-features --features web,unstable-light-client,jsonrpsee --target wasm32-unknown-unknown -- -D warnings + args: -p subxt --no-default-features --features web,light-client,jsonrpsee --target wasm32-unknown-unknown -- -D warnings - if: "failure()" uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5 @@ -433,7 +433,7 @@ jobs: uses: actions-rs/cargo@v1.0.3 with: command: test - args: --release --package integration-tests --features unstable-light-client + args: --release --package integration-tests --features light-client - if: "failure()" uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5 diff --git a/Cargo.lock b/Cargo.lock index 29edbbb323..2551d07eb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5648,6 +5648,7 @@ dependencies = [ "subxt-utils-accountid32", "thiserror 2.0.12", "tokio", + "tokio-util", "tower", "tracing", "tracing-subscriber", diff --git a/rpcs/Cargo.toml b/rpcs/Cargo.toml index 9a7cb75c45..3cfc929529 100644 --- a/rpcs/Cargo.toml +++ b/rpcs/Cargo.toml @@ -18,7 +18,7 @@ keywords = ["parity", "subxt", "rpcs"] default = ["jsonrpsee", "native"] jsonrpsee = ["dep:jsonrpsee", "dep:tokio-util"] -unstable-light-client = [ +light-client = [ "dep:subxt-lightclient" ] diff --git a/rpcs/src/client/mod.rs b/rpcs/src/client/mod.rs index ce9c7fe875..6964833a25 100644 --- a/rpcs/src/client/mod.rs +++ b/rpcs/src/client/mod.rs @@ -18,7 +18,7 @@ //! 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 +//! - **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. diff --git a/rpcs/src/macros.rs b/rpcs/src/macros.rs index dc6b8fa195..e42ca57ceb 100644 --- a/rpcs/src/macros.rs +++ b/rpcs/src/macros.rs @@ -14,7 +14,7 @@ macro_rules! cfg_feature { macro_rules! cfg_unstable_light_client { ($($item:item)*) => { - crate::macros::cfg_feature!("unstable-light-client", $($item)*); + crate::macros::cfg_feature!("light-client", $($item)*); }; } diff --git a/subxt/Cargo.toml b/subxt/Cargo.toml index 6e338bfa7d..fffdd0ed1f 100644 --- a/subxt/Cargo.toml +++ b/subxt/Cargo.toml @@ -25,7 +25,7 @@ default = ["jsonrpsee", "native"] # Features that we expect to be enabled for documentation. docs = [ "default", - "unstable-light-client", + "light-client", "runtime", "reconnecting-rpc-client", ] @@ -35,6 +35,7 @@ docs = [ native = [ "subxt-lightclient?/native", "subxt-rpcs/native", + "tokio-util", "tokio?/sync", "sp-crypto-hashing/std", ] @@ -75,7 +76,7 @@ unstable-metadata = [] # Activate this to expose the Light Client functionality. # Note that this feature is experimental and things may break or not work as expected. -unstable-light-client = ["subxt-lightclient", "subxt-rpcs/unstable-light-client"] +light-client = ["subxt-lightclient", "subxt-rpcs/light-client"] # Activate this to expose the ability to generate metadata from Wasm runtime files. runtime-wasm-path = ["subxt-macro/runtime-wasm-path"] @@ -118,6 +119,9 @@ subxt-lightclient = { workspace = true, optional = true, default-features = fals subxt-rpcs = { workspace = true } subxt-utils-accountid32 = { workspace = true } +# Included if "native" feature is enabled +tokio-util = { workspace = true, features = ["compat"], optional = true } + # Included if the reconnecting rpc client feature is enabled # Only the `tokio/sync` is used in the reconnecting rpc client # and that compiles both for native and web. @@ -146,23 +150,18 @@ tower = { workspace = true } hyper = { workspace = true } http-body = { workspace = true } -# [[example]] -# name = "light_client_basic" -# path = "examples/light_client_basic.rs" -# required-features = ["unstable-light-client", "jsonrpsee"] -# -# [[example]] -# name = "light_client_local_node" -# path = "examples/light_client_local_node.rs" -# required-features = ["unstable-light-client", "jsonrpsee", "native"] -# -# [[example]] -# name = "setup_reconnecting_rpc_client" -# path = "examples/setup_reconnecting_rpc_client.rs" -# required-features = ["reconnecting-rpc-client"] +[[example]] +name = "light_client" +path = "examples/light_client.rs" +required-features = ["light-client", "jsonrpsee"] + +[[example]] +name = "rpc_client" +path = "examples/rpc_client.rs" +required-features = ["reconnecting-rpc-client"] [package.metadata.docs.rs] features = ["docs"] [package.metadata.playground] -features = ["default", "unstable-light-client"] +features = ["default", "light-client"] diff --git a/subxt/examples/light_client.rs b/subxt/examples/light_client.rs new file mode 100644 index 0000000000..4f48d8aabb --- /dev/null +++ b/subxt/examples/light_client.rs @@ -0,0 +1,65 @@ +//! We can configure Subxt to use a Smoldot based lightclient to connect to a chain. +use futures::StreamExt; +use subxt::{OnlineClient, PolkadotConfig, lightclient::LightClient}; + +// Generate an interface that we can use from the node's metadata. +#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")] +pub mod polkadot {} + +const POLKADOT_SPEC: &str = include_str!("../../artifacts/demo_chain_specs/polkadot.json"); +const ASSET_HUB_SPEC: &str = + include_str!("../../artifacts/demo_chain_specs/polkadot_asset_hub.json"); + +#[tokio::main] +async fn main() -> Result<(), Box> { + // The lightclient logs are informative: + tracing_subscriber::fmt::init(); + + // (Optional) for dev purposes, we can use a Subxt utility function to fetch a chainspec from + // a locally running node if we like, but in this example we use some pre-baked chainspecs: + let _chain_spec = subxt::utils::fetch_chainspec_from_rpc_node("ws://127.0.0.1:9944").await; + + // Instantiate a light client with the Polkadot relay chain, + // and connect it to Asset Hub, too. + let (lightclient, polkadot_rpc) = LightClient::relay_chain(POLKADOT_SPEC)?; + let asset_hub_rpc = lightclient.parachain(ASSET_HUB_SPEC)?; + + // Create Subxt clients from these Smoldot backed RPC clients. + let config = PolkadotConfig::new(); + let polkadot_api = + OnlineClient::::from_rpc_client(config.clone(), polkadot_rpc).await?; + let asset_hub_api = + OnlineClient::::from_rpc_client(config, asset_hub_rpc).await?; + + // Now we can use them as with any other Subxt instance. Here we fetch finalized blocks + // from both chains and print some detail about the contained extrinsics. + let polkadot_sub = polkadot_api + .stream_blocks() + .await? + .map(|block| ("Polkadot", block)); + let parachain_sub = asset_hub_api + .stream_blocks() + .await? + .map(|block| ("AssetHub", block)); + + let mut stream_combinator = futures::stream::select(polkadot_sub, parachain_sub); + + while let Some((chain, block)) = stream_combinator.next().await { + let block = block?; + + // Print some details about the blocks we fetch via the light client. + println!("Chain {:?} hash={:?}", chain, block.hash()); + let at_block = block.at().await?; + let extrinsics = at_block.extrinsics().fetch().await?; + for ext in extrinsics.iter() { + let ext = ext?; + + let idx = ext.index(); + let pallet_name = ext.pallet_name(); + let call_name = ext.call_name(); + println!(" #{idx}: {pallet_name}.{call_name}"); + } + } + + Ok(()) +} diff --git a/subxt/examples/rpc_client.rs b/subxt/examples/rpc_client.rs new file mode 100644 index 0000000000..b7ae55aff9 --- /dev/null +++ b/subxt/examples/rpc_client.rs @@ -0,0 +1,38 @@ +//! We can provide a custom RPC client to Subxt to use, instead of the default. +use subxt::config::RpcConfigFor; +use subxt::{Error, OnlineClient, PolkadotConfig}; +use subxt_rpcs::client::{ReconnectingRpcClient, RpcClient}; + +#[tokio::main] +async fn main() -> Result<(), Error> { + let config = PolkadotConfig::new(); + + // Configure an RPC client. Here we use the reconnecting one, but several impls are + // available, or you can implement the subxt_rpcs::client::RpcClientT trait yourself + // to bring your own RPC client. + let inner_rpc_client = ReconnectingRpcClient::builder() + .build("wss://rpc.ibp.network/polkadot") + .await + .map_err(Error::other)?; + + let rpc_client = RpcClient::new(inner_rpc_client); + + // Pass it to Subxt to use. + let client = OnlineClient::from_rpc_client(config, rpc_client.clone()).await?; + + // We can use the Subxt client: + let at_block = client.at_current_block().await?; + let header = at_block.block_header().await?; + println!("Current block header via Subxt: {header:?}"); + + // Since we cloned the RPC client above, we can use it ourselves too: + let legacy_rpcs = + subxt_rpcs::methods::LegacyRpcMethods::>::new(rpc_client); + let header = legacy_rpcs + .chain_get_header(Some(at_block.block_hash())) + .await? + .unwrap(); + println!("Current block header via RPC call: {header:?}"); + + Ok(()) +} diff --git a/subxt/src/error.rs b/subxt/src/error.rs index 1548a61f27..079b3a82ec 100644 --- a/subxt/src/error.rs +++ b/subxt/src/error.rs @@ -10,7 +10,7 @@ mod hex; use std::borrow::Cow; use thiserror::Error as DeriveError; -#[cfg(feature = "unstable-light-client")] +#[cfg(feature = "light-client")] pub use subxt_lightclient::LightClientError; // Re-export dispatch error types: @@ -86,10 +86,10 @@ pub enum Error { OtherRpcClientError(#[from] subxt_rpcs::Error), #[error("Other codec error: {0}")] OtherCodecError(#[from] codec::Error), - #[cfg(feature = "unstable-light-client")] + #[cfg(feature = "light-client")] #[error("Other light client error: {0}")] OtherLightClientError(#[from] subxt_lightclient::LightClientError), - #[cfg(feature = "unstable-light-client")] + #[cfg(feature = "light-client")] #[error("Other light client RPC error: {0}")] OtherLightClientRpcError(#[from] subxt_lightclient::LightClientRpcError), // Dev note: Nothing in subxt should ever emit this error. It can instead be used @@ -167,6 +167,10 @@ impl Error { Error::ModuleErrorDetailsError(e) => e.backend_error(), Error::ModuleErrorDecodeError(e) => e.backend_error(), Error::DispatchErrorDecodeError(e) => e.backend_error(), + #[cfg(feature = "light-client")] + Error::OtherLightClientError(_) => None, + #[cfg(feature = "light-client")] + Error::OtherLightClientRpcError(_) => None, // BackendError is always a BackendError: Error::BackendError(e) => Some(e), // Other errors come from different crates so can never contain a BackendError: diff --git a/subxt/src/lib.rs b/subxt/src/lib.rs index 3782b6d649..0f0b9ba85a 100644 --- a/subxt/src/lib.rs +++ b/subxt/src/lib.rs @@ -79,7 +79,7 @@ pub use crate::{ }; // Expose light client bits -#[cfg(feature = "unstable-light-client")] +#[cfg(feature = "light-client")] pub use subxt_lightclient as lightclient; /// Re-export external crates that are made use of in the subxt API. diff --git a/subxt/src/utils.rs b/subxt/src/utils.rs index 660ec94b99..6194e7df4d 100644 --- a/subxt/src/utils.rs +++ b/subxt/src/utils.rs @@ -6,6 +6,8 @@ mod account_id20; mod era; +#[cfg(feature = "jsonrpsee")] +mod fetch_chain_spec; mod multi_address; mod multi_signature; mod range_map; @@ -32,6 +34,10 @@ pub use yesnomaybe::{Maybe, No, NoMaybe, Yes, YesMaybe, YesNo}; pub use subxt_utils_accountid32::AccountId32; +// Lightclient helper to fetch chain spec from a running node. +#[cfg(feature = "jsonrpsee")] +pub use fetch_chain_spec::{FetchChainspecError, fetch_chainspec_from_rpc_node}; + /// Wraps an already encoded byte vector, prevents being encoded as a raw byte vector as part of /// the transaction payload #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] diff --git a/subxt/src/utils/fetch_chain_spec.rs b/subxt/src/utils/fetch_chain_spec.rs new file mode 100644 index 0000000000..488e324b38 --- /dev/null +++ b/subxt/src/utils/fetch_chain_spec.rs @@ -0,0 +1,110 @@ +// 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 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, +) -> Result, 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::, _>( + &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(all(feature = "jsonrpsee", feature = "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>; + pub type Receiver = ws::Receiver>; + + /// Build WS RPC client from URL + pub async fn client(url: &str) -> Result { + 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(all(feature = "jsonrpsee", feature = "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 { + 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)) + } +} diff --git a/testing/integration-tests/Cargo.toml b/testing/integration-tests/Cargo.toml index 780f9bd2d8..8dc8c040d1 100644 --- a/testing/integration-tests/Cargo.toml +++ b/testing/integration-tests/Cargo.toml @@ -16,10 +16,10 @@ description = "Subxt integration tests that rely on the Substrate binary" default = [] # Enable to run the tests with Light Client support. -unstable-light-client = ["subxt/unstable-light-client"] +light-client = ["subxt/light-client"] # Enable to run the full-client tests with Light Client support. -unstable-light-client-long-running = ["subxt/unstable-light-client"] +light-client-long-running = ["subxt/light-client"] # Enable this to use the chainhead backend in tests _instead of_ # the default one which relies on the "old" RPC methods. diff --git a/testing/integration-tests/build.rs b/testing/integration-tests/build.rs index bce3d4dcb2..559863b123 100644 --- a/testing/integration-tests/build.rs +++ b/testing/integration-tests/build.rs @@ -3,8 +3,8 @@ use cfg_aliases::cfg_aliases; fn main() { // Setup cfg aliases cfg_aliases! { - lightclient: { any(feature = "unstable-light-client", feature = "unstable-light-client-long-running") }, - fullclient: { all(not(feature = "unstable-light-client"), not(feature = "unstable-light-client-long-running")) }, + lightclient: { any(feature = "light-client", feature = "light-client-long-running") }, + fullclient: { all(not(feature = "light-client"), not(feature = "light-client-long-running")) }, legacy_backend: { not(feature = "chainhead-backend") }, chainhead_backend: { feature = "chainhead-backend" }, } diff --git a/testing/integration-tests/src/lib.rs b/testing/integration-tests/src/lib.rs index a27fb8f44b..c58513ecf7 100644 --- a/testing/integration-tests/src/lib.rs +++ b/testing/integration-tests/src/lib.rs @@ -2,10 +2,8 @@ // This file is dual-licensed as Apache-2.0 or GPL-3.0. // see LICENSE for license details. -#[cfg(all(feature = "unstable-light-client", feature = "chainhead-backend"))] -compile_error!( - "The features 'unstable-light-client' and 'chainhead-backend' cannot be used together" -); +#[cfg(all(feature = "light-client", feature = "chainhead-backend"))] +compile_error!("The features 'light-client' and 'chainhead-backend' cannot be used together"); #[cfg(test)] pub mod utils; @@ -15,12 +13,12 @@ pub mod utils; use utils::*; #[cfg(any( - all(test, not(feature = "unstable-light-client")), - all(test, feature = "unstable-light-client-long-running") + all(test, not(feature = "light-client")), + all(test, feature = "light-client-long-running") ))] mod full_client; -#[cfg(all(test, feature = "unstable-light-client"))] +#[cfg(all(test, feature = "light-client"))] mod light_client; #[cfg(test)] diff --git a/testing/wasm-lightclient-tests/Cargo.toml b/testing/wasm-lightclient-tests/Cargo.toml index e79ec54ba0..538563a0c3 100644 --- a/testing/wasm-lightclient-tests/Cargo.toml +++ b/testing/wasm-lightclient-tests/Cargo.toml @@ -13,6 +13,6 @@ serde_json = "1" futures-util = "0.3.30" # This crate is not a part of the workspace, because it -# requires the "jsonrpsee web unstable-light-client" features to be enabled, which we don't +# requires the "jsonrpsee web light-client" features to be enabled, which we don't # want enabled for workspace builds in general. -subxt = { path = "../../subxt", default-features = false, features = ["web", "jsonrpsee", "unstable-light-client"] } +subxt = { path = "../../subxt", default-features = false, features = ["web", "jsonrpsee", "light-client"] }