From b069c4425a38cdfe6c84997834f5cd2f5cdba7f9 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Fri, 15 Mar 2024 15:21:06 +0000 Subject: [PATCH] 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 --- Cargo.lock | 2 - lightclient/Cargo.toml | 28 +- lightclient/src/background.rs | 653 +++++++++--------- lightclient/src/chain_config.rs | 71 ++ lightclient/src/client.rs | 212 ------ lightclient/src/lib.rs | 271 +++++++- lightclient/src/platform/mod.rs | 10 +- lightclient/src/rpc.rs | 132 ++++ lightclient/src/shared_client.rs | 47 ++ subxt/Cargo.toml | 33 +- subxt/examples/light_client_basic.rs | 47 ++ ...tx_basic.rs => light_client_local_node.rs} | 29 +- subxt/examples/light_client_parachains.rs | 102 --- subxt/src/backend/legacy/mod.rs | 4 +- subxt/src/backend/rpc/lightclient_impl.rs | 53 ++ subxt/src/backend/rpc/mod.rs | 4 + subxt/src/backend/rpc/rpc_client.rs | 6 + subxt/src/backend/unstable/mod.rs | 7 +- subxt/src/book/usage/light_client.rs | 37 +- subxt/src/client/light_client/builder.rs | 336 --------- subxt/src/client/light_client/mod.rs | 194 ------ subxt/src/client/light_client/rpc.rs | 162 ----- subxt/src/client/mod.rs | 8 - subxt/src/client/online_client.rs | 8 +- subxt/src/error/mod.rs | 2 +- subxt/src/lib.rs | 5 + subxt/src/utils/fetch_chain_spec.rs | 113 +++ subxt/src/utils/mod.rs | 6 + .../integration-tests/src/light_client/mod.rs | 15 +- .../integration-tests/src/utils/node_proc.rs | 46 +- testing/wasm-lightclient-tests/Cargo.lock | 124 ++-- testing/wasm-lightclient-tests/tests/wasm.rs | 19 +- 32 files changed, 1216 insertions(+), 1570 deletions(-) create mode 100644 lightclient/src/chain_config.rs delete mode 100644 lightclient/src/client.rs create mode 100644 lightclient/src/rpc.rs create mode 100644 lightclient/src/shared_client.rs create mode 100644 subxt/examples/light_client_basic.rs rename subxt/examples/{light_client_tx_basic.rs => light_client_local_node.rs} (60%) delete mode 100644 subxt/examples/light_client_parachains.rs create mode 100644 subxt/src/backend/rpc/lightclient_impl.rs delete mode 100644 subxt/src/client/light_client/builder.rs delete mode 100644 subxt/src/client/light_client/mod.rs delete mode 100644 subxt/src/client/light_client/rpc.rs create mode 100644 subxt/src/utils/fetch_chain_spec.rs diff --git a/Cargo.lock b/Cargo.lock index 891d3e986b..6704da008d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4557,7 +4557,6 @@ dependencies = [ "subxt-signer", "thiserror", "tokio", - "tokio-stream", "tokio-util", "tracing", "tracing-subscriber 0.3.18", @@ -4617,7 +4616,6 @@ dependencies = [ name = "subxt-lightclient" version = "0.34.0" dependencies = [ - "either", "futures", "futures-timer", "futures-util", diff --git a/lightclient/Cargo.toml b/lightclient/Cargo.toml index 22475b75b9..d7abdf87f6 100644 --- a/lightclient/Cargo.toml +++ b/lightclient/Cargo.toml @@ -24,23 +24,14 @@ default = ["native"] # Exactly 1 of "web" and "native" is expected. native = [ "smoldot-light/std", - "tokio-stream", - "tokio/sync", "tokio/rt", - "futures-util", ] # Enable this for web/wasm builds. # Exactly 1 of "web" and "native" is expected. web = [ "getrandom/js", - - "smoldot", "smoldot/std", - "smoldot-light", - "tokio-stream", - "tokio/sync", - "futures-util", # For the light-client platform. "wasm-bindgen-futures", @@ -56,29 +47,26 @@ web = [ ] [dependencies] -futures = { workspace = true } +futures = { workspace = true, features = ["async-await"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, features = ["default", "raw_value"] } thiserror = { workspace = true } tracing = { workspace = true } +smoldot-light = { workspace = true } +tokio-stream = { workspace = true } +tokio = { workspace = true, features = ["sync"] } +futures-util = { workspace = true } -# Light client support: -smoldot = { workspace = true, optional = true } -smoldot-light = { workspace = true, optional = true } -either = { workspace = true, optional = true } -tokio = { workspace = true, optional = true } -tokio-stream = { workspace = true, optional = true } -futures-util = { workspace = true, optional = true } +# Only needed for web js-sys = { workspace = true, optional = true } send_wrapper = { workspace = true, optional = true } web-sys = { workspace = true, optional = true } wasm-bindgen = { workspace = true, optional = true } wasm-bindgen-futures = { workspace = true, optional = true } +smoldot = { workspace = true, optional = true } +pin-project = { workspace = true, optional = true } futures-timer = { workspace = true, optional = true } instant = { workspace = true, optional = true } -pin-project = { workspace = true, optional = true } - -# Included if "web" feature is enabled, to enable its js feature. getrandom = { workspace = true, optional = true } [package.metadata.docs.rs] diff --git a/lightclient/src/background.rs b/lightclient/src/background.rs index b755b383b7..e9bd9424e3 100644 --- a/lightclient/src/background.rs +++ b/lightclient/src/background.rs @@ -1,43 +1,47 @@ -// Copyright 2019-2023 Parity Technologies (UK) Ltd. +// 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 futures::stream::StreamExt; -use futures_util::future::{self, Either}; -use serde::Deserialize; +use crate::rpc::RpcResponse; +use crate::shared_client::SharedClient; +use crate::{JsonRpcError, LightClientRpcError}; +use futures::{stream::StreamExt, FutureExt}; use serde_json::value::RawValue; use smoldot_light::platform::PlatformRef; use std::{collections::HashMap, str::FromStr}; use tokio::sync::{mpsc, oneshot}; +use tokio_stream::wrappers::UnboundedReceiverStream; -use crate::client::AddedChain; +const LOG_TARGET: &str = "subxt-light-client-background-task"; -use super::LightClientRpcError; -use smoldot_light::ChainId; +/// Response from [`BackgroundTaskHandle::request()`]. +pub type MethodResponse = Result, LightClientRpcError>; -const LOG_TARGET: &str = "subxt-light-client-background"; +/// Response from [`BackgroundTaskHandle::subscribe()`]. +pub type SubscriptionResponse = Result< + ( + SubscriptionId, + mpsc::UnboundedReceiver, JsonRpcError>>, + ), + LightClientRpcError, +>; -/// The response of an RPC method. -pub type MethodResponse = Result, LightClientRpcError>; +/// Type of subscription IDs we can get back. +pub type SubscriptionId = String; /// Message protocol between the front-end client that submits the RPC requests -/// and the backend handler that produces responses from the chain. -/// -/// The light client uses a single object [`smoldot_light::JsonRpcResponses`] to -/// handle all requests and subscriptions from a chain. A background task is spawned -/// to multiplex the rpc responses and to provide them back to their rightful submitters. +/// and the background task which fetches responses from Smoldot. Hidden behind +/// the [`BackgroundTaskHandle`]. #[derive(Debug)] -pub enum FromSubxt { +enum Message { /// The RPC method request. Request { /// The method of the request. method: String, /// The parameters of the request. - params: String, - /// Channel used to send back the result. + params: Option>, + /// Channel used to send back the method response. sender: oneshot::Sender, - /// The ID of the chain used to identify the chain. - chain_id: ChainId, }, /// The RPC subscription (pub/sub) request. Subscription { @@ -46,29 +50,160 @@ pub enum FromSubxt { /// The method to unsubscribe. unsubscribe_method: String, /// The parameters of the request. - params: String, - /// Channel used to send back the subscription ID if successful. - sub_id: oneshot::Sender, - /// Channel used to send back the notifications. - sender: mpsc::UnboundedSender>, - /// The ID of the chain used to identify the chain. - chain_id: ChainId, + params: Option>, + /// Channel used to send back the subscription response. + sender: oneshot::Sender, }, } -/// Background task data. +/// A handle to communicate with the background task. +#[derive(Clone, Debug)] +pub struct BackgroundTaskHandle { + to_backend: mpsc::UnboundedSender, +} + +impl BackgroundTaskHandle { + /// Make an RPC request via the background task. + pub async fn request(&self, method: String, params: Option>) -> MethodResponse { + let (tx, rx) = oneshot::channel(); + self.to_backend + .send(Message::Request { + method, + params, + sender: tx, + }) + .map_err(|_e| LightClientRpcError::BackgroundTaskDropped)?; + + match rx.await { + Err(_e) => Err(LightClientRpcError::BackgroundTaskDropped), + Ok(response) => response, + } + } + + /// Subscribe to some RPC method via the background task. + pub async fn subscribe( + &self, + method: String, + params: Option>, + unsubscribe_method: String, + ) -> SubscriptionResponse { + let (tx, rx) = oneshot::channel(); + self.to_backend + .send(Message::Subscription { + method, + params, + unsubscribe_method, + sender: tx, + }) + .map_err(|_e| LightClientRpcError::BackgroundTaskDropped)?; + + match rx.await { + Err(_e) => Err(LightClientRpcError::BackgroundTaskDropped), + Ok(response) => response, + } + } +} + +/// A background task which runs with [`BackgroundTask::run()`] and manages messages +/// coming to/from Smoldot. #[allow(clippy::type_complexity)] pub struct BackgroundTask { - /// Smoldot light client implementation that leverages the exposed platform. - client: smoldot_light::Client, - /// Per-chain data. - chain_data: HashMap, + channels: BackgroundTaskChannels, + data: BackgroundTaskData, +} + +impl BackgroundTask { + /// Constructs a new [`BackgroundTask`]. + pub(crate) fn new( + client: SharedClient, + chain_id: smoldot_light::ChainId, + from_back: smoldot_light::JsonRpcResponses, + ) -> (BackgroundTask, BackgroundTaskHandle) { + let (tx, rx) = mpsc::unbounded_channel(); + + let bg_task = BackgroundTask { + channels: BackgroundTaskChannels { + from_front: UnboundedReceiverStream::new(rx), + from_back, + }, + data: BackgroundTaskData { + client, + chain_id, + last_request_id: 0, + pending_subscriptions: HashMap::new(), + requests: HashMap::new(), + subscriptions: HashMap::new(), + }, + }; + + let bg_handle = BackgroundTaskHandle { to_backend: tx }; + + (bg_task, bg_handle) + } + + /// Run the background task, which: + /// - Forwards messages/subscription requests to Smoldot from the front end. + /// - Forwards responses back from Smoldot to the front end. + pub async fn run(self) { + let chain_id = self.data.chain_id; + let mut channels = self.channels; + let mut data = self.data; + + loop { + tokio::pin! { + let from_front_fut = channels.from_front.next().fuse(); + let from_back_fut = channels.from_back.next().fuse(); + } + + futures::select! { + // Message coming from the front end/client. + front_message = from_front_fut => { + let Some(message) = front_message else { + tracing::trace!(target: LOG_TARGET, "Subxt channel closed"); + break; + }; + tracing::trace!( + target: LOG_TARGET, + "Received register message {:?}", + message + ); + + data.handle_requests(message).await; + }, + // Message coming from Smoldot. + back_message = from_back_fut => { + let Some(back_message) = back_message else { + tracing::trace!(target: LOG_TARGET, "Smoldot RPC responses channel closed"); + break; + }; + tracing::trace!( + target: LOG_TARGET, + "Received smoldot RPC chain {:?} result {:?}", + chain_id, back_message + ); + + data.handle_rpc_response(back_message); + } + } + } + + tracing::trace!(target: LOG_TARGET, "Task closed"); + } +} + +struct BackgroundTaskChannels { + /// Messages sent into this background task from the front end. + from_front: UnboundedReceiverStream, + /// Messages sent into the background task from Smoldot. + from_back: smoldot_light::JsonRpcResponses, } -/// The data that we store for each chain. -#[derive(Default)] -struct ChainData { - /// Generates an unique monotonically increasing ID for each chain. +struct BackgroundTaskData { + /// A smoldot light client that can be shared. + client: SharedClient, + /// Knowing the chain ID helps with debugging, but isn't overwise necessary. + chain_id: smoldot_light::ChainId, + /// Know which Id to use next for new requests/subscriptions. last_request_id: usize, /// Map the request ID of a RPC method to the frontend `Sender`. requests: HashMap>, @@ -78,20 +213,12 @@ struct ChainData { /// The RPC method request is made in the background and the response should /// not be sent back to the user. /// Map the request ID of a RPC method to the frontend `Sender`. - id_to_subscription: HashMap, + pending_subscriptions: HashMap, /// Map the subscription ID to the frontend `Sender`. /// /// The subscription ID is entirely generated by the node (smoldot). Therefore, it is /// possible for two distinct subscriptions of different chains to have the same subscription ID. - subscriptions: HashMap, -} - -impl ChainData { - /// Fetch and increment the request ID. - fn next_id(&mut self) -> usize { - self.last_request_id = self.last_request_id.wrapping_add(1); - self.last_request_id - } + subscriptions: HashMap, } /// The state needed to resolve the subscription ID and send @@ -100,72 +227,52 @@ struct PendingSubscription { /// Send the method response ID back to the user. /// /// It contains the subscription ID if successful, or an JSON RPC error object. - sub_id_sender: oneshot::Sender, - /// The subscription state that is added to the `subscriptions` map only - /// if the subscription ID is successfully sent back to the user. - subscription_state: ActiveSubscription, -} - -impl PendingSubscription { - /// Transforms the pending subscription into an active subscription. - fn into_parts(self) -> (oneshot::Sender, ActiveSubscription) { - (self.sub_id_sender, self.subscription_state) - } + response_sender: oneshot::Sender, + /// The unsubscribe method to call when the user drops the receiver + /// part of the channel. + unsubscribe_method: String, } /// The state of the subscription. struct ActiveSubscription { /// Channel to send the subscription notifications back to frontend. - sender: mpsc::UnboundedSender>, + notification_sender: mpsc::UnboundedSender, JsonRpcError>>, /// The unsubscribe method to call when the user drops the receiver /// part of the channel. unsubscribe_method: String, } -impl BackgroundTask { - /// Constructs a new [`BackgroundTask`]. - pub fn new( - client: smoldot_light::Client, - ) -> BackgroundTask { - BackgroundTask { - client, - chain_data: Default::default(), - } - } - - fn for_chain_id( - &mut self, - chain_id: smoldot_light::ChainId, - ) -> ( - &mut ChainData, - &mut smoldot_light::Client, - ) { - let chain_data = self.chain_data.entry(chain_id).or_default(); - let client = &mut self.client; - (chain_data, client) +impl BackgroundTaskData { + /// Fetch and increment the request ID. + fn next_id(&mut self) -> usize { + self.last_request_id = self.last_request_id.wrapping_add(1); + self.last_request_id } /// Handle the registration messages received from the user. - async fn handle_requests(&mut self, message: FromSubxt) { + async fn handle_requests(&mut self, message: Message) { match message { - FromSubxt::Request { + Message::Request { method, params, sender, - chain_id, } => { - let (chain_data, client) = self.for_chain_id(chain_id); - let id = chain_data.next_id(); + let id = self.next_id(); + let chain_id = self.chain_id; + let params = match ¶ms { + Some(params) => params.get(), + None => "null", + }; let request = format!( r#"{{"jsonrpc":"2.0","id":"{}", "method":"{}","params":{}}}"#, id, method, params ); - chain_data.requests.insert(id, sender); + self.requests.insert(id, sender); tracing::trace!(target: LOG_TARGET, "Tracking request id={id} chain={chain_id:?}"); - let result = client.json_rpc_request(request, chain_id); + let result = self.client.json_rpc_request(request, chain_id); if let Err(err) = result { tracing::warn!( target: LOG_TARGET, @@ -173,14 +280,14 @@ impl BackgroundTask { err.to_string() ); - let sender = chain_data + let sender = self .requests .remove(&id) .expect("Channel is inserted above; qed"); // Send the error back to frontend. if sender - .send(Err(LightClientRpcError::Request(err.to_string()))) + .send(Err(LightClientRpcError::SmoldotError(err.to_string()))) .is_err() { tracing::warn!( @@ -192,52 +299,49 @@ impl BackgroundTask { tracing::trace!(target: LOG_TARGET, "Submitted to smoldot request with id={id}"); } } - FromSubxt::Subscription { + Message::Subscription { method, unsubscribe_method, params, - sub_id, sender, - chain_id, } => { - let (chain_data, client) = self.for_chain_id(chain_id); - let id = chain_data.next_id(); + let id = self.next_id(); + let chain_id = self.chain_id; // For subscriptions we need to make a plain RPC request to the subscription method. // The server will return as a result the subscription ID. + let params = match ¶ms { + Some(params) => params.get(), + None => "null", + }; let request = format!( r#"{{"jsonrpc":"2.0","id":"{}", "method":"{}","params":{}}}"#, id, method, params ); tracing::trace!(target: LOG_TARGET, "Tracking subscription request id={id} chain={chain_id:?}"); - let subscription_id_state = PendingSubscription { - sub_id_sender: sub_id, - subscription_state: ActiveSubscription { - sender, - unsubscribe_method, - }, + let pending_subscription = PendingSubscription { + response_sender: sender, + unsubscribe_method, }; - chain_data - .id_to_subscription - .insert(id, subscription_id_state); + self.pending_subscriptions.insert(id, pending_subscription); - let result = client.json_rpc_request(request, chain_id); + let result = self.client.json_rpc_request(request, chain_id); if let Err(err) = result { tracing::warn!( target: LOG_TARGET, "Cannot send RPC request to lightclient {:?}", err.to_string() ); - let subscription_id_state = chain_data - .id_to_subscription + let subscription_id_state = self + .pending_subscriptions .remove(&id) .expect("Channels are inserted above; qed"); // Send the error back to frontend. if subscription_id_state - .sub_id_sender - .send(Err(LightClientRpcError::Request(err.to_string()))) + .response_sender + .send(Err(LightClientRpcError::SmoldotError(err.to_string()))) .is_err() { tracing::warn!( @@ -253,42 +357,11 @@ impl BackgroundTask { } /// Parse the response received from the light client and sent it to the appropriate user. - fn handle_rpc_response(&mut self, chain_id: smoldot_light::ChainId, response: String) { - tracing::trace!(target: LOG_TARGET, "Received from smoldot response={response} chain={chain_id:?}"); - let (chain_data, _client) = self.for_chain_id(chain_id); + fn handle_rpc_response(&mut self, response: String) { + let chain_id = self.chain_id; + tracing::trace!(target: LOG_TARGET, "Received from smoldot response='{response}' chain={chain_id:?}"); match RpcResponse::from_str(&response) { - Ok(RpcResponse::Error { id, error }) => { - let Ok(id) = id.parse::() else { - tracing::warn!(target: LOG_TARGET, "Cannot send error. Id={id} chain={chain_id:?} is not a valid number"); - return; - }; - - if let Some(sender) = chain_data.requests.remove(&id) { - if sender - .send(Err(LightClientRpcError::Request(error.to_string()))) - .is_err() - { - tracing::warn!( - target: LOG_TARGET, - "Cannot send method response to id={id} chain={chain_id:?}", - ); - } - } else if let Some(subscription_id_state) = - chain_data.id_to_subscription.remove(&id) - { - if subscription_id_state - .sub_id_sender - .send(Err(LightClientRpcError::Request(error.to_string()))) - .is_err() - { - tracing::warn!( - target: LOG_TARGET, - "Cannot send method response to id {id} chain={chain_id:?}", - ); - } - } - } Ok(RpcResponse::Method { id, result }) => { let Ok(id) = id.parse::() else { tracing::warn!(target: LOG_TARGET, "Cannot send response. Id={id} chain={chain_id:?} is not a valid number"); @@ -296,42 +369,47 @@ impl BackgroundTask { }; // Send the response back. - if let Some(sender) = chain_data.requests.remove(&id) { + if let Some(sender) = self.requests.remove(&id) { if sender.send(Ok(result)).is_err() { tracing::warn!( target: LOG_TARGET, "Cannot send method response to id={id} chain={chain_id:?}", ); } - } else if let Some(pending_subscription) = chain_data.id_to_subscription.remove(&id) - { - let Ok(sub_id) = result - .get() - .trim_start_matches('"') - .trim_end_matches('"') - .parse::() - else { + } else if let Some(pending_subscription) = self.pending_subscriptions.remove(&id) { + let Ok(sub_id) = serde_json::from_str::(result.get()) else { tracing::warn!( target: LOG_TARGET, - "Subscription id={result} chain={chain_id:?} is not a valid number", + "Subscription id='{result}' chain={chain_id:?} is not a valid string", ); return; }; tracing::trace!(target: LOG_TARGET, "Received subscription id={sub_id} chain={chain_id:?}"); - let (sub_id_sender, active_subscription) = pending_subscription.into_parts(); - if sub_id_sender.send(Ok(result)).is_err() { + let (sub_tx, sub_rx) = mpsc::unbounded_channel(); + + // Send the method response and a channel to receive notifications back. + if pending_subscription + .response_sender + .send(Ok((sub_id.clone(), sub_rx))) + .is_err() + { tracing::warn!( target: LOG_TARGET, - "Cannot send method response to id={id} chain={chain_id:?}", + "Cannot send subscription ID response to id={id} chain={chain_id:?}", ); - return; } - // Track this subscription ID if send is successful. - chain_data.subscriptions.insert(sub_id, active_subscription); + // Store the other end of the notif channel to send future subscription notifications to. + self.subscriptions.insert( + sub_id, + ActiveSubscription { + notification_sender: sub_tx, + unsubscribe_method: pending_subscription.unsubscribe_method, + }, + ); } else { tracing::warn!( target: LOG_TARGET, @@ -339,43 +417,73 @@ impl BackgroundTask { ); } } - Ok(RpcResponse::Subscription { method, id, result }) => { + Ok(RpcResponse::MethodError { id, error }) => { let Ok(id) = id.parse::() else { - tracing::warn!(target: LOG_TARGET, "Cannot send subscription. Id={id} chain={chain_id:?} is not a valid number"); + tracing::warn!(target: LOG_TARGET, "Cannot send error. Id={id} chain={chain_id:?} is not a valid number"); return; }; - let Some(subscription_state) = chain_data.subscriptions.get_mut(&id) else { + if let Some(sender) = self.requests.remove(&id) { + if sender + .send(Err(LightClientRpcError::JsonRpcError(JsonRpcError(error)))) + .is_err() + { + tracing::warn!( + target: LOG_TARGET, + "Cannot send method response to id={id} chain={chain_id:?}", + ); + } + } else if let Some(subscription_id_state) = self.pending_subscriptions.remove(&id) { + if subscription_id_state + .response_sender + .send(Err(LightClientRpcError::JsonRpcError(JsonRpcError(error)))) + .is_err() + { + tracing::warn!( + target: LOG_TARGET, + "Cannot send method response to id {id} chain={chain_id:?}", + ); + } + } + } + Ok(RpcResponse::Notification { + method, + subscription_id, + result, + }) => { + let Some(active_subscription) = self.subscriptions.get_mut(&subscription_id) else { tracing::warn!( target: LOG_TARGET, - "Subscription response id={id} chain={chain_id:?} method={method} is not tracked", + "Subscription response id={subscription_id} chain={chain_id:?} method={method} is not tracked", ); return; }; - if subscription_state.sender.send(result).is_ok() { - // Nothing else to do, user is informed about the notification. - return; + if active_subscription + .notification_sender + .send(Ok(result)) + .is_err() + { + self.unsubscribe(&subscription_id, chain_id); } - - // User dropped the receiver, unsubscribe from the method and remove internal tracking. - let Some(subscription_state) = chain_data.subscriptions.remove(&id) else { - // State is checked to be some above, so this should never happen. - return; - }; - // Make a call to unsubscribe from this method. - let unsub_id = chain_data.next_id(); - let request = format!( - r#"{{"jsonrpc":"2.0","id":"{}", "method":"{}","params":["{}"]}}"#, - unsub_id, subscription_state.unsubscribe_method, id - ); - - if let Err(err) = self.client.json_rpc_request(request, chain_id) { + } + Ok(RpcResponse::NotificationError { + method, + subscription_id, + error, + }) => { + let Some(active_subscription) = self.subscriptions.get_mut(&subscription_id) else { tracing::warn!( target: LOG_TARGET, - "Failed to unsubscribe id={id:?} chain={chain_id:?} method={:?} err={err:?}", subscription_state.unsubscribe_method + "Subscription error id={subscription_id} chain={chain_id:?} method={method} is not tracked", ); - } else { - tracing::debug!(target: LOG_TARGET,"Unsubscribe id={id:?} chain={chain_id:?} method={:?}", subscription_state.unsubscribe_method); + return; + }; + if active_subscription + .notification_sender + .send(Err(JsonRpcError(error))) + .is_err() + { + self.unsubscribe(&subscription_id, chain_id); } } Err(err) => { @@ -384,169 +492,28 @@ impl BackgroundTask { } } - /// Perform the main background task: - /// - receiving requests from subxt RPC method / subscriptions - /// - provides the results from the light client back to users. - pub async fn start_task( - &mut self, - from_subxt: mpsc::UnboundedReceiver, - from_node: Vec, - ) { - let from_subxt_event = tokio_stream::wrappers::UnboundedReceiverStream::new(from_subxt); - - let from_node = from_node.into_iter().map(|rpc| { - Box::pin(futures::stream::unfold(rpc, |mut rpc| async move { - let response = rpc.rpc_responses.next().await; - Some(((response, rpc.chain_id), rpc)) - })) - }); - let stream_combinator = futures::stream::select_all(from_node); - - tokio::pin!(from_subxt_event, stream_combinator); - - let mut from_subxt_event_fut = from_subxt_event.next(); - let mut from_node_event_fut = stream_combinator.next(); - - loop { - match future::select(from_subxt_event_fut, from_node_event_fut).await { - // Message received from subxt. - Either::Left((subxt_message, previous_fut)) => { - let Some(message) = subxt_message else { - tracing::trace!(target: LOG_TARGET, "Subxt channel closed"); - break; - }; - tracing::trace!( - target: LOG_TARGET, - "Received register message {:?}", - message - ); - - self.handle_requests(message).await; - - from_subxt_event_fut = from_subxt_event.next(); - from_node_event_fut = previous_fut; - } - // Message received from rpc handler: lightclient response. - Either::Right((node_message, previous_fut)) => { - let Some((node_message, chain)) = node_message else { - tracing::trace!(target: LOG_TARGET, "Smoldot closed all RPC channels"); - break; - }; - // Smoldot returns `None` if the chain has been removed (which subxt does not remove). - let Some(response) = node_message else { - tracing::trace!(target: LOG_TARGET, "Smoldot RPC responses channel closed"); - break; - }; - tracing::trace!( - target: LOG_TARGET, - "Received smoldot RPC chain {:?} result {:?}", - chain, response - ); - - self.handle_rpc_response(chain, response); - - // Advance backend, save frontend. - from_subxt_event_fut = previous_fut; - from_node_event_fut = stream_combinator.next(); - } - } - } - - tracing::trace!(target: LOG_TARGET, "Task closed"); - } -} - -/// The RPC response from the light-client. -/// This can either be a response of a method, or a notification from a subscription. -#[derive(Debug, Clone)] -enum RpcResponse { - Method { - /// Response ID. - id: String, - /// The result of the method call. - result: Box, - }, - Subscription { - /// RPC method that generated the notification. - method: String, - /// Subscription ID. - id: String, - /// Result. - result: Box, - }, - Error { - /// Response ID. - id: String, - /// Error. - error: Box, - }, -} - -impl std::str::FromStr for RpcResponse { - type Err = serde_json::Error; - - fn from_str(response: &str) -> Result { - // Helper structures to deserialize from raw RPC strings. - #[derive(Deserialize, Debug)] - struct Response { - /// JSON-RPC version. - #[allow(unused)] - jsonrpc: String, - /// Result. - result: Box, - /// Request ID - id: String, - } - #[derive(Deserialize)] - struct NotificationParams { - /// The ID of the subscription. - subscription: String, - /// Result. - result: Box, - } - #[derive(Deserialize)] - struct ResponseNotification { - /// JSON-RPC version. - #[allow(unused)] - jsonrpc: String, - /// RPC method that generated the notification. - method: String, - /// Result. - params: NotificationParams, - } - #[derive(Deserialize)] - struct ErrorResponse { - /// JSON-RPC version. - #[allow(unused)] - jsonrpc: String, - /// Request ID. - id: String, - /// Error. - error: Box, - } - - // Check if the response can be mapped as an RPC method response. - let result: Result = serde_json::from_str(response); - if let Ok(response) = result { - return Ok(RpcResponse::Method { - id: response.id, - result: response.result, - }); - } + // Unsubscribe from a subscription. + fn unsubscribe(&mut self, subscription_id: &str, chain_id: smoldot_light::ChainId) { + let Some(active_subscription) = self.subscriptions.remove(subscription_id) else { + // Subscription doesn't exist so nothing more to do. + return; + }; - let result: Result = serde_json::from_str(response); - if let Ok(notification) = result { - return Ok(RpcResponse::Subscription { - id: notification.params.subscription, - method: notification.method, - result: notification.params.result, - }); + // Build a call to unsubscribe from this method. + let unsub_id = self.next_id(); + let request = format!( + r#"{{"jsonrpc":"2.0","id":"{}", "method":"{}","params":["{}"]}}"#, + unsub_id, active_subscription.unsubscribe_method, subscription_id + ); + + // Submit it. + if let Err(err) = self.client.json_rpc_request(request, chain_id) { + tracing::warn!( + target: LOG_TARGET, + "Failed to unsubscribe id={subscription_id} chain={chain_id:?} method={:?} err={err:?}", active_subscription.unsubscribe_method + ); + } else { + tracing::debug!(target: LOG_TARGET,"Unsubscribe id={subscription_id} chain={chain_id:?} method={:?}", active_subscription.unsubscribe_method); } - - let error: ErrorResponse = serde_json::from_str(response)?; - Ok(RpcResponse::Error { - id: error.id, - error: error.error, - }) } } diff --git a/lightclient/src/chain_config.rs b/lightclient/src/chain_config.rs new file mode 100644 index 0000000000..66d5294476 --- /dev/null +++ b/lightclient/src/chain_config.rs @@ -0,0 +1,71 @@ +// 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 serde_json::Value; +use std::borrow::Cow; + +/// Something went wrong building chain config. +#[non_exhaustive] +#[derive(thiserror::Error, Debug)] +pub enum ChainConfigError { + /// The provided chain spec is the wrong shape. + #[error("Invalid chain spec format")] + InvalidSpecFormat, +} + +/// Configuration to connect to a chain. +pub struct ChainConfig<'a> { + // The chain spec to use. + chain_spec: Cow<'a, str>, +} + +impl<'a> From<&'a str> for ChainConfig<'a> { + fn from(chain_spec: &'a str) -> Self { + ChainConfig::chain_spec(chain_spec) + } +} + +impl<'a> From for ChainConfig<'a> { + fn from(chain_spec: String) -> Self { + ChainConfig::chain_spec(chain_spec) + } +} + +impl<'a> ChainConfig<'a> { + /// Construct a chain config from a chain spec. + pub fn chain_spec(chain_spec: impl Into>) -> Self { + ChainConfig { + chain_spec: chain_spec.into(), + } + } + + /// Set the bootnodes to the given ones. + pub fn set_bootnodes>( + self, + bootnodes: impl IntoIterator, + ) -> Result { + let mut chain_spec_json: Value = serde_json::from_str(&self.chain_spec) + .map_err(|_e| ChainConfigError::InvalidSpecFormat)?; + + if let Value::Object(map) = &mut chain_spec_json { + let bootnodes = bootnodes + .into_iter() + .map(|s| Value::String(s.as_ref().to_owned())) + .collect(); + + map.insert("bootNodes".to_string(), Value::Array(bootnodes)); + } else { + return Err(ChainConfigError::InvalidSpecFormat); + } + + Ok(ChainConfig { + chain_spec: Cow::Owned(chain_spec_json.to_string()), + }) + } + + // Used internally to fetch the chain spec back out. + pub(crate) fn as_chain_spec(&self) -> &str { + &self.chain_spec + } +} diff --git a/lightclient/src/client.rs b/lightclient/src/client.rs deleted file mode 100644 index f06c968b66..0000000000 --- a/lightclient/src/client.rs +++ /dev/null @@ -1,212 +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 std::iter; - -use super::{ - background::{BackgroundTask, FromSubxt, MethodResponse}, - LightClientRpcError, -}; -use serde_json::value::RawValue; -use tokio::sync::{mpsc, mpsc::error::SendError, oneshot}; - -use super::platform::build_platform; - -pub const LOG_TARGET: &str = "subxt-light-client"; - -/// A raw light-client RPC implementation that can connect to multiple chains. -#[derive(Clone)] -pub struct RawLightClientRpc { - /// Communicate with the backend task that multiplexes the responses - /// back to the frontend. - to_backend: mpsc::UnboundedSender, -} - -impl RawLightClientRpc { - /// Construct a [`LightClientRpc`] that can communicated with the provided chain. - /// - /// # Note - /// - /// This uses the same underlying instance created by [`LightClientRpc::new_from_client`]. - pub fn for_chain(&self, chain_id: smoldot_light::ChainId) -> LightClientRpc { - LightClientRpc { - to_backend: self.to_backend.clone(), - chain_id, - } - } -} - -/// The light-client RPC implementation that is used to connect with the chain. -#[derive(Clone)] -pub struct LightClientRpc { - /// Communicate with the backend task that multiplexes the responses - /// back to the frontend. - to_backend: mpsc::UnboundedSender, - /// The chain ID to target for requests. - chain_id: smoldot_light::ChainId, -} - -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_light::AddChainConfig< - '_, - (), - impl IntoIterator, - >, - ) -> Result { - tracing::trace!(target: LOG_TARGET, "Create light client"); - - let mut client = smoldot_light::Client::new(build_platform()); - - let config = smoldot_light::AddChainConfig { - specification: config.specification, - json_rpc: config.json_rpc, - database_content: config.database_content, - potential_relay_chains: config.potential_relay_chains.into_iter(), - user_data: config.user_data, - }; - - let smoldot_light::AddChainSuccess { - chain_id, - json_rpc_responses, - } = client - .add_chain(config) - .map_err(|err| LightClientRpcError::AddChainError(err.to_string()))?; - - let rpc_responses = json_rpc_responses.expect("Light client RPC configured; qed"); - - let raw_client = Self::new_from_client( - client, - iter::once(AddedChain { - chain_id, - rpc_responses, - }), - ); - Ok(raw_client.for_chain(chain_id)) - } - - /// Constructs a new [`RawLightClientRpc`] from the raw smoldot client. - /// - /// Receives a list of RPC objects as a result of calling `smoldot_light::Client::add_chain`. - /// This [`RawLightClientRpc`] can target different chains using [`RawLightClientRpc::for_chain`] method. - /// - /// ## 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_from_client( - client: smoldot_light::Client, - chains: impl IntoIterator, - ) -> RawLightClientRpc - where - TPlat: smoldot_light::platform::PlatformRef + Clone, - { - let (to_backend, backend) = mpsc::unbounded_channel(); - let chains = chains.into_iter().collect(); - - let future = async move { - let mut task = BackgroundTask::new(client); - task.start_task(backend, chains).await; - }; - - #[cfg(feature = "native")] - tokio::spawn(future); - #[cfg(feature = "web")] - wasm_bindgen_futures::spawn_local(future); - - RawLightClientRpc { to_backend } - } - - /// Returns the chain ID of the current light-client. - pub fn chain_id(&self) -> smoldot_light::ChainId { - self.chain_id - } - - /// Submits an RPC method request to the light-client. - /// - /// This method sends a request to the light-client to execute an RPC method with the provided parameters. - /// The parameters are parsed into a valid JSON object in the background. - pub fn method_request( - &self, - method: String, - params: String, - ) -> Result, SendError> { - let (sender, receiver) = oneshot::channel(); - - self.to_backend.send(FromSubxt::Request { - method, - params, - sender, - chain_id: self.chain_id, - })?; - - Ok(receiver) - } - - /// Makes an RPC subscription call to the light-client. - /// - /// This method sends a request to the light-client to establish an RPC subscription with the provided parameters. - /// The parameters are parsed into a valid JSON object in the background. - #[allow(clippy::type_complexity)] - pub fn subscription_request( - &self, - method: String, - params: String, - unsubscribe_method: String, - ) -> Result< - ( - oneshot::Receiver, - mpsc::UnboundedReceiver>, - ), - SendError, - > { - let (sub_id, sub_id_rx) = oneshot::channel(); - let (sender, receiver) = mpsc::unbounded_channel(); - - self.to_backend.send(FromSubxt::Subscription { - method, - unsubscribe_method, - params, - sub_id, - sender, - chain_id: self.chain_id, - })?; - - Ok((sub_id_rx, receiver)) - } -} - -/// The added chain of the light-client. -pub struct AddedChain { - /// The id of the chain. - pub chain_id: smoldot_light::ChainId, - /// Producer of RPC responses for the chain. - pub rpc_responses: smoldot_light::JsonRpcResponses, -} diff --git a/lightclient/src/lib.rs b/lightclient/src/lib.rs index 34800fe4f0..6df5c2ec47 100644 --- a/lightclient/src/lib.rs +++ b/lightclient/src/lib.rs @@ -2,52 +2,259 @@ // This file is dual-licensed as Apache-2.0 or GPL-3.0. // see LICENSE for license details. -//! Low level light client implementation for RPC method and -//! subscriptions requests. -//! -//! The client implementation supports both native and wasm -//! environments. -//! -//! This leverages the smoldot crate to connect to the chain. +//! A wrapper around [`smoldot_light`] which provides an light client capable of connecting +//! to Substrate based chains. +#![deny(missing_docs)] #![cfg_attr(docsrs, feature(doc_cfg))] #[cfg(any( all(feature = "web", feature = "native"), not(any(feature = "web", feature = "native")) ))] -compile_error!("subxt: exactly one of the 'web' and 'native' features should be used."); +compile_error!("subxt-lightclient: exactly one of the 'web' and 'native' features should be used."); -mod background; -mod client; mod platform; +mod shared_client; +// mod receiver; +mod background; +mod chain_config; +mod rpc; -// Used to enable the js feature for wasm. -#[cfg(feature = "web")] -#[allow(unused_imports)] -pub use getrandom as _; - -pub use client::{AddedChain, LightClientRpc, RawLightClientRpc}; - -/// Re-exports of the smoldot related objects. -pub mod smoldot { - pub use smoldot_light::{ - platform::PlatformRef, AddChainConfig, AddChainConfigJsonRpc, ChainId, Client, - JsonRpcResponses, - }; +use background::{BackgroundTask, BackgroundTaskHandle}; +use futures::Stream; +use platform::DefaultPlatform; +use serde_json::value::RawValue; +use shared_client::SharedClient; +use std::future::Future; +use tokio::sync::mpsc; - #[cfg(feature = "native")] - #[cfg_attr(docsrs, doc(cfg(feature = "native")))] - pub use smoldot_light::platform::default::DefaultPlatform; -} +pub use chain_config::{ChainConfig, ChainConfigError}; -/// Light client error. +/// Things that can go wrong when constructing the [`LightClient`]. #[derive(Debug, thiserror::Error)] -pub enum LightClientRpcError { +pub enum LightClientError { /// Error encountered while adding the chain to the light-client. #[error("Failed to add the chain to the light client: {0}.")] AddChainError(String), - /// Error originated while trying to submit a RPC request. - #[error("RPC request cannot be sent: {0}.")] - Request(String), +} + +/// Things that can go wrong calling methods of [`LightClientRpc`]. +#[derive(Debug, thiserror::Error)] +pub enum LightClientRpcError { + /// Error response from the JSON-RPC server. + #[error("{0}")] + JsonRpcError(JsonRpcError), + /// Smoldot could not handle the RPC call. + #[error("Smoldot could not handle the RPC call: {0}.")] + SmoldotError(String), + /// Background task dropped. + #[error("The background task was dropped.")] + BackgroundTaskDropped, +} + +/// An error response from the JSON-RPC server (ie smoldot) in response to +/// a method call or as a subscription notification. +#[derive(Debug, thiserror::Error)] +#[error("RPC Error: {0}.")] +pub struct JsonRpcError(Box); + +/// This represents a single light client connection to the network. Instantiate +/// it with [`LightClient::relay_chain()`] to communicate with a relay chain, and +/// then call [`LightClient::parachain()`] to establish connections to parachains. +#[derive(Clone)] +pub struct LightClient { + client: SharedClient, + relay_chain_id: smoldot_light::ChainId, +} + +impl LightClient { + /// Given a chain spec, establish a connection to a relay chain. Any subsequent calls to + /// [`LightClient::parachain()`] will set this as the relay chain. + /// + /// # 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 relay_chain<'a>( + chain_config: impl Into>, + ) -> Result<(Self, LightClientRpc), LightClientError> { + let mut client = smoldot_light::Client::new(platform::build_platform()); + let chain_config = chain_config.into(); + let chain_spec = chain_config.as_chain_spec(); + + let config = smoldot_light::AddChainConfig { + specification: chain_spec, + json_rpc: smoldot_light::AddChainConfigJsonRpc::Enabled { + max_pending_requests: u32::MAX.try_into().unwrap(), + max_subscriptions: u32::MAX, + }, + database_content: "", + potential_relay_chains: std::iter::empty(), + user_data: (), + }; + + let added_chain = client + .add_chain(config) + .map_err(|err| LightClientError::AddChainError(err.to_string()))?; + + let relay_chain_id = added_chain.chain_id; + let rpc_responses = added_chain + .json_rpc_responses + .expect("Light client RPC configured; qed"); + let shared_client: SharedClient<_> = client.into(); + + let light_client_rpc = + LightClientRpc::new_raw(shared_client.clone(), relay_chain_id, rpc_responses); + let light_client = Self { + client: shared_client, + relay_chain_id, + }; + + Ok((light_client, light_client_rpc)) + } + + /// Given a chain spec, establish a connection to a parachain. + /// + /// # 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 parachain<'a>( + &self, + chain_config: impl Into>, + ) -> Result { + let chain_config = chain_config.into(); + let chain_spec = chain_config.as_chain_spec(); + + let config = smoldot_light::AddChainConfig { + specification: chain_spec, + json_rpc: smoldot_light::AddChainConfigJsonRpc::Enabled { + max_pending_requests: u32::MAX.try_into().unwrap(), + max_subscriptions: u32::MAX, + }, + database_content: "", + potential_relay_chains: std::iter::once(self.relay_chain_id), + user_data: (), + }; + + let added_chain = self + .client + .add_chain(config) + .map_err(|err| LightClientError::AddChainError(err.to_string()))?; + + let chain_id = added_chain.chain_id; + let rpc_responses = added_chain + .json_rpc_responses + .expect("Light client RPC configured; qed"); + + Ok(LightClientRpc::new_raw( + self.client.clone(), + chain_id, + rpc_responses, + )) + } +} + +/// This represents a single RPC connection to a specific chain, and is constructed by calling +/// one of the methods on [`LightClient`]. Using this, you can make RPC requests to the chain. +#[derive(Clone, Debug)] +pub struct LightClientRpc { + handle: BackgroundTaskHandle, +} + +impl LightClientRpc { + // Dev note: this would provide a "low leveL" interface if one is needed. + // Do we actually need to provide this, or can we entirely hide Smoldot? + pub(crate) fn new_raw( + client: impl Into>, + chain_id: smoldot_light::ChainId, + rpc_responses: smoldot_light::JsonRpcResponses, + ) -> Self + where + TPlat: smoldot_light::platform::PlatformRef + Send + 'static, + TChain: Send + 'static, + { + let (background_task, background_handle) = + BackgroundTask::new(client.into(), chain_id, rpc_responses); + + // For now we spawn the background task internally, but later we can expose + // methods to give this back to the user so that they can exert backpressure. + spawn(async move { background_task.run().await }); + + LightClientRpc { + handle: background_handle, + } + } + + /// Make an RPC request to a chain, getting back a result. + pub async fn request( + &self, + method: String, + params: Option>, + ) -> Result, LightClientRpcError> { + self.handle.request(method, params).await + } + + /// Subscribe to some RPC method, getting back a stream of notifications. + pub async fn subscribe( + &self, + method: String, + params: Option>, + unsub: String, + ) -> Result { + let (id, notifications) = self.handle.subscribe(method, params, unsub).await?; + Ok(LightClientRpcSubscription { id, notifications }) + } +} + +/// A stream of notifications handed back when [`LightClientRpc::subscribe`] is called. +pub struct LightClientRpcSubscription { + notifications: mpsc::UnboundedReceiver, JsonRpcError>>, + id: String, +} + +impl LightClientRpcSubscription { + /// Return the subscription ID + pub fn id(&self) -> &str { + &self.id + } +} + +impl Stream for LightClientRpcSubscription { + type Item = Result, JsonRpcError>; + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.notifications.poll_recv(cx) + } +} + +/// A quick helper to spawn a task that works for WASM. +fn spawn(future: F) { + #[cfg(feature = "native")] + tokio::spawn(async move { + future.await; + }); + #[cfg(feature = "web")] + wasm_bindgen_futures::spawn_local(async move { + future.await; + }); } diff --git a/lightclient/src/platform/mod.rs b/lightclient/src/platform/mod.rs index 7a1182da66..34cb1b26f8 100644 --- a/lightclient/src/platform/mod.rs +++ b/lightclient/src/platform/mod.rs @@ -11,16 +11,16 @@ mod wasm_platform; #[cfg(feature = "web")] mod wasm_socket; -pub use helpers::build_platform; +pub use helpers::{build_platform, DefaultPlatform}; #[cfg(feature = "native")] mod helpers { use smoldot_light::platform::default::DefaultPlatform as Platform; use std::sync::Arc; - pub type PlatformType = Arc; + pub type DefaultPlatform = Arc; - pub fn build_platform() -> PlatformType { + pub fn build_platform() -> DefaultPlatform { Platform::new( "subxt-light-client".into(), env!("CARGO_PKG_VERSION").into(), @@ -32,9 +32,9 @@ mod helpers { mod helpers { use super::wasm_platform::SubxtPlatform as Platform; - pub type PlatformType = Platform; + pub type DefaultPlatform = Platform; - pub fn build_platform() -> PlatformType { + pub fn build_platform() -> DefaultPlatform { Platform::new() } } diff --git a/lightclient/src/rpc.rs b/lightclient/src/rpc.rs new file mode 100644 index 0000000000..6d84837020 --- /dev/null +++ b/lightclient/src/rpc.rs @@ -0,0 +1,132 @@ +// 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 serde::Deserialize; +use serde_json::value::RawValue; + +/// The RPC response from the light-client. +/// This can either be a response of a method, or a notification from a subscription. +#[derive(Debug, Clone)] +pub enum RpcResponse { + Method { + /// Response ID. + id: String, + /// The result of the method call. + result: Box, + }, + MethodError { + /// Response ID. + id: String, + /// Error. + error: Box, + }, + Notification { + /// RPC method that generated the notification. + method: String, + /// Subscription ID. + subscription_id: String, + /// Result. + result: Box, + }, + NotificationError { + /// RPC method that generated the notification. + method: String, + /// Subscription ID. + subscription_id: String, + /// Result. + error: Box, + }, +} + +impl std::str::FromStr for RpcResponse { + type Err = (); + + fn from_str(response: &str) -> Result { + // Valid response + #[derive(Deserialize, Debug)] + struct Response { + #[allow(unused)] + jsonrpc: String, + id: String, + result: Box, + } + + // Error response + #[derive(Deserialize)] + struct ResponseError { + #[allow(unused)] + jsonrpc: String, + id: String, + error: Box, + } + + // Valid notification (subscription) response + #[derive(Deserialize)] + struct Notification { + #[allow(unused)] + jsonrpc: String, + method: String, + params: NotificationResultParams, + } + #[derive(Deserialize)] + struct NotificationResultParams { + subscription: String, + result: Box, + } + + // Error notification (subscription) response + #[derive(Deserialize)] + struct NotificationError { + #[allow(unused)] + jsonrpc: String, + method: String, + params: NotificationErrorParams, + } + #[derive(Deserialize)] + struct NotificationErrorParams { + /// The ID of the subscription. + subscription: String, + error: Box, + } + + // Try deserializing the response payload to one of the above. We can + // do this more efficiently eg how jsonrpsee_types does. + + let result: Result = serde_json::from_str(response); + if let Ok(response) = result { + return Ok(RpcResponse::Method { + id: response.id, + result: response.result, + }); + } + let result: Result = serde_json::from_str(response); + if let Ok(response) = result { + return Ok(RpcResponse::Notification { + subscription_id: response.params.subscription, + method: response.method, + result: response.params.result, + }); + } + let result: Result = serde_json::from_str(response); + if let Ok(response) = result { + return Ok(RpcResponse::MethodError { + id: response.id, + error: response.error, + }); + } + let result: Result = serde_json::from_str(response); + if let Ok(response) = result { + return Ok(RpcResponse::NotificationError { + method: response.method, + subscription_id: response.params.subscription, + error: response.params.error, + }); + } + + // We couldn't decode into any of the above. We could pick one of the above` + // errors to return, but there's no real point since the string is obviously + // different from any of them. + Err(()) + } +} diff --git a/lightclient/src/shared_client.rs b/lightclient/src/shared_client.rs new file mode 100644 index 0000000000..d725030599 --- /dev/null +++ b/lightclient/src/shared_client.rs @@ -0,0 +1,47 @@ +// 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 smoldot_light as sl; +use std::sync::{Arc, Mutex}; + +/// This wraps [`smoldot_light::Client`] so that it can be cloned and shared. +#[derive(Clone)] +pub struct SharedClient { + client: Arc>>, +} + +impl From> + for SharedClient +{ + fn from(client: sl::Client) -> Self { + SharedClient { + client: Arc::new(Mutex::new(client)), + } + } +} + +impl SharedClient { + /// Delegates to [`smoldot_light::Client::json_rpc_request()`]. + pub(crate) fn json_rpc_request( + &self, + json_rpc_request: impl Into, + chain_id: sl::ChainId, + ) -> Result<(), sl::HandleRpcError> { + self.client + .lock() + .expect("mutex should not be poisoned") + .json_rpc_request(json_rpc_request, chain_id) + } + + /// Delegates to [`smoldot_light::Client::add_chain()`]. + pub(crate) fn add_chain( + &self, + config: sl::AddChainConfig<'_, TChain, impl Iterator>, + ) -> Result { + self.client + .lock() + .expect("mutex should not be poisoned") + .add_chain(config) + } +} diff --git a/subxt/Cargo.toml b/subxt/Cargo.toml index 9bba68432b..53c0074b3e 100644 --- a/subxt/Cargo.toml +++ b/subxt/Cargo.toml @@ -25,20 +25,20 @@ default = ["jsonrpsee", "native"] # Enable this for native (ie non web/wasm builds). # Exactly 1 of "web" and "native" is expected. native = [ - "jsonrpsee?/async-client", - "jsonrpsee?/client-ws-transport-native-tls", - "subxt-lightclient?/native", + "jsonrpsee?/async-client", + "jsonrpsee?/client-ws-transport-native-tls", + "subxt-lightclient?/native", "tokio-util" ] # Enable this for web/wasm builds. # Exactly 1 of "web" and "native" is expected. web = [ - "jsonrpsee?/async-wasm-client", - "jsonrpsee?/client-web-transport", - "getrandom/js", - "subxt-lightclient?/web", - "subxt-macro/web", + "jsonrpsee?/async-wasm-client", + "jsonrpsee?/client-web-transport", + "getrandom/js", + "subxt-lightclient?/web", + "subxt-macro/web", "instant/wasm-bindgen" ] @@ -46,7 +46,9 @@ web = [ unstable-reconnecting-rpc-client = ["dep:reconnecting-jsonrpsee-ws-client"] # Enable this to use jsonrpsee (allowing for example `OnlineClient::from_url`). -jsonrpsee = ["dep:jsonrpsee"] +jsonrpsee = [ + "dep:jsonrpsee", +] # Enable this to pull in extra Substrate dependencies which make it possible to # use the `sp_core::crypto::Pair` Signer implementation, as well as adding some @@ -61,7 +63,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", "tokio-stream"] +unstable-light-client = ["subxt-lightclient"] [dependencies] async-trait = { workspace = true } @@ -103,9 +105,6 @@ subxt-macro = { workspace = true } subxt-metadata = { workspace = true, features = ["std"] } subxt-lightclient = { workspace = true, optional = true, default-features = false } -# Light client support: -tokio-stream = { workspace = true, optional = true } - # Reconnecting jsonrpc ws client reconnecting-jsonrpsee-ws-client = { version = "0.3", optional = true } @@ -135,13 +134,13 @@ subxt-signer = { path = "../signer" } tracing-subscriber = { workspace = true } [[example]] -name = "light_client_tx_basic" -path = "examples/light_client_tx_basic.rs" +name = "light_client_basic" +path = "examples/light_client_basic.rs" required-features = ["unstable-light-client", "jsonrpsee"] [[example]] -name = "light_client_parachains" -path = "examples/light_client_parachains.rs" +name = "light_client_local_node" +path = "examples/light_client_local_node.rs" required-features = ["unstable-light-client", "jsonrpsee", "native"] [[example]] diff --git a/subxt/examples/light_client_basic.rs b/subxt/examples/light_client_basic.rs new file mode 100644 index 0000000000..3b4eba6f9e --- /dev/null +++ b/subxt/examples/light_client_basic.rs @@ -0,0 +1,47 @@ +#![allow(missing_docs)] +use futures::StreamExt; +use subxt::{client::OnlineClient, lightclient::LightClient, PolkadotConfig}; + +// 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(); + + // 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 polkadot_api = OnlineClient::::from_rpc_client(polkadot_rpc).await?; + let asset_hub_api = OnlineClient::::from_rpc_client(asset_hub_rpc).await?; + + // Use them! + let polkadot_sub = polkadot_api + .blocks() + .subscribe_finalized() + .await? + .map(|block| ("Polkadot", block)); + let parachain_sub = asset_hub_api + .blocks() + .subscribe_finalized() + .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?; + println!(" Chain {:?} hash={:?}", chain, block.hash()); + } + + Ok(()) +} diff --git a/subxt/examples/light_client_tx_basic.rs b/subxt/examples/light_client_local_node.rs similarity index 60% rename from subxt/examples/light_client_tx_basic.rs rename to subxt/examples/light_client_local_node.rs index 37ce6cb533..bcf492b41b 100644 --- a/subxt/examples/light_client_tx_basic.rs +++ b/subxt/examples/light_client_local_node.rs @@ -1,5 +1,10 @@ #![allow(missing_docs)] -use subxt::{client::LightClient, PolkadotConfig}; +use subxt::utils::fetch_chainspec_from_rpc_node; +use subxt::{ + client::OnlineClient, + lightclient::{ChainConfig, LightClient}, + PolkadotConfig, +}; use subxt_signer::sr25519::dev; // Generate an interface that we can use from the node's metadata. @@ -11,19 +16,23 @@ async fn main() -> Result<(), Box> { // The smoldot logs are informative: tracing_subscriber::fmt::init(); - // Create a light client by fetching the chain spec of a local running node. - // In this case, because we start one single node, the bootnodes must be overwritten - // for the light client to connect to the local node. + // Use a utility function to obtain a chain spec from a locally running node: + let chain_spec = fetch_chainspec_from_rpc_node("ws://127.0.0.1:9944").await?; + + // Configure the bootnodes of this chain spec. In this case, because we start one + // single node, the bootnodes must be overwritten for the light client to connect + // to the local node. // // The `12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp` is the P2P address // from a local polkadot node starting with // `--node-key 0000000000000000000000000000000000000000000000000000000000000001` - let api = LightClient::::builder() - .bootnodes([ - "/ip4/127.0.0.1/tcp/30333/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", - ]) - .build_from_url("ws://127.0.0.1:9944") - .await?; + let chain_config = ChainConfig::chain_spec(chain_spec.get()).set_bootnodes([ + "/ip4/127.0.0.1/tcp/30333/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + ])?; + + // Start the light client up, establishing a connection to the local node. + let (_light_client, chain_rpc) = LightClient::relay_chain(chain_config)?; + let api = OnlineClient::::from_rpc_client(chain_rpc).await?; // Build a balance transfer extrinsic. let dest = dev::bob().public_key().into(); diff --git a/subxt/examples/light_client_parachains.rs b/subxt/examples/light_client_parachains.rs deleted file mode 100644 index 227da26fe0..0000000000 --- a/subxt/examples/light_client_parachains.rs +++ /dev/null @@ -1,102 +0,0 @@ -#![allow(missing_docs)] -use futures::StreamExt; -use std::{iter, num::NonZeroU32}; -use subxt::{ - client::{LightClient, RawLightClient}, - PolkadotConfig, -}; - -// 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 smoldot logs are informative: - tracing_subscriber::fmt::init(); - - // Connecting to a parachain is a multi step process. - - // Step 1. Construct a new smoldot client. - let mut client = - subxt_lightclient::smoldot::Client::new(subxt_lightclient::smoldot::DefaultPlatform::new( - "subxt-example-light-client".into(), - "version-0".into(), - )); - - // Step 2. Connect to the relay chain of the parachain. For this example, the Polkadot relay chain. - let polkadot_connection = client - .add_chain(subxt_lightclient::smoldot::AddChainConfig { - specification: POLKADOT_SPEC, - json_rpc: subxt_lightclient::smoldot::AddChainConfigJsonRpc::Enabled { - max_pending_requests: NonZeroU32::new(128).unwrap(), - max_subscriptions: 1024, - }, - potential_relay_chains: iter::empty(), - database_content: "", - user_data: (), - }) - .expect("Light client chain added with valid spec; qed"); - let polkadot_json_rpc_responses = polkadot_connection - .json_rpc_responses - .expect("Light client configured with json rpc enabled; qed"); - let polkadot_chain_id = polkadot_connection.chain_id; - - // Step 3. Connect to the parachain. For this example, the Asset hub parachain. - let assethub_connection = client - .add_chain(subxt_lightclient::smoldot::AddChainConfig { - specification: ASSET_HUB_SPEC, - json_rpc: subxt_lightclient::smoldot::AddChainConfigJsonRpc::Enabled { - max_pending_requests: NonZeroU32::new(128).unwrap(), - max_subscriptions: 1024, - }, - // The chain specification of the asset hub parachain mentions that the identifier - // of its relay chain is `polkadot`. - potential_relay_chains: [polkadot_chain_id].into_iter(), - database_content: "", - user_data: (), - }) - .expect("Light client chain added with valid spec; qed"); - let parachain_json_rpc_responses = assethub_connection - .json_rpc_responses - .expect("Light client configured with json rpc enabled; qed"); - let parachain_chain_id = assethub_connection.chain_id; - - // Step 4. Turn the smoldot client into a raw client. - let raw_light_client = RawLightClient::builder() - .add_chain(polkadot_chain_id, polkadot_json_rpc_responses) - .add_chain(parachain_chain_id, parachain_json_rpc_responses) - .build(client) - .await?; - - // Step 5. Obtain a client to target the relay chain and the parachain. - let polkadot_api: LightClient = - raw_light_client.for_chain(polkadot_chain_id).await?; - let parachain_api: LightClient = - raw_light_client.for_chain(parachain_chain_id).await?; - - // Step 6. Subscribe to the finalized blocks of the chains. - let polkadot_sub = polkadot_api - .blocks() - .subscribe_finalized() - .await? - .map(|block| ("Polkadot", block)); - let parachain_sub = parachain_api - .blocks() - .subscribe_finalized() - .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?; - - println!(" Chain {:?} hash={:?}", chain, block.hash()); - } - - Ok(()) -} diff --git a/subxt/src/backend/legacy/mod.rs b/subxt/src/backend/legacy/mod.rs index 7649da1592..8d9f331fac 100644 --- a/subxt/src/backend/legacy/mod.rs +++ b/subxt/src/backend/legacy/mod.rs @@ -53,10 +53,10 @@ impl LegacyBackendBuilder { /// Given an [`RpcClient`] to use to make requests, this returns a [`LegacyBackend`], /// which implements the [`Backend`] trait. - pub fn build(self, client: RpcClient) -> LegacyBackend { + pub fn build(self, client: impl Into) -> LegacyBackend { LegacyBackend { storage_page_size: self.storage_page_size, - methods: LegacyRpcMethods::new(client), + methods: LegacyRpcMethods::new(client.into()), } } } diff --git a/subxt/src/backend/rpc/lightclient_impl.rs b/subxt/src/backend/rpc/lightclient_impl.rs new file mode 100644 index 0000000000..f4e0deec6a --- /dev/null +++ b/subxt/src/backend/rpc/lightclient_impl.rs @@ -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>, + ) -> RawRpcFuture<'a, Box> { + 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>, + 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, + } +} \ No newline at end of file diff --git a/subxt/src/backend/rpc/mod.rs b/subxt/src/backend/rpc/mod.rs index 19101dd5df..453fcf5a7f 100644 --- a/subxt/src/backend/rpc/mod.rs +++ b/subxt/src/backend/rpc/mod.rs @@ -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; diff --git a/subxt/src/backend/rpc/rpc_client.rs b/subxt/src/backend/rpc/rpc_client.rs index d960c010db..16dba9e6fb 100644 --- a/subxt/src/backend/rpc/rpc_client.rs +++ b/subxt/src/backend/rpc/rpc_client.rs @@ -79,6 +79,12 @@ impl RpcClient { } } +impl From 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() diff --git a/subxt/src/backend/unstable/mod.rs b/subxt/src/backend/unstable/mod.rs index 7095b90af1..4fe70bc374 100644 --- a/subxt/src/backend/unstable/mod.rs +++ b/subxt/src/backend/unstable/mod.rs @@ -75,9 +75,12 @@ impl UnstableBackendBuilder { /// 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, UnstableBackendDriver) { + pub fn build( + self, + client: impl Into, + ) -> (UnstableBackend, UnstableBackendDriver) { // 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::::from_methods(rpc_methods.clone()); let follow_stream_unpin = follow_stream_unpin::FollowStreamUnpin::::from_methods( diff --git a/subxt/src/book/usage/light_client.rs b/subxt/src/book/usage/light_client.rs index 33dbc6668b..c7b6e53318 100644 --- a/subxt/src/book/usage/light_client.rs +++ b/subxt/src/book/usage/light_client.rs @@ -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")] -//! ``` diff --git a/subxt/src/client/light_client/builder.rs b/subxt/src/client/light_client/builder.rs deleted file mode 100644 index 68db1631e1..0000000000 --- a/subxt/src/client/light_client/builder.rs +++ /dev/null @@ -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 { - max_pending_requests: NonZeroU32, - max_subscriptions: u32, - bootnodes: Option>, - potential_relay_chains: Option>, - _marker: std::marker::PhantomData, -} - -impl Default for LightClientBuilder { - 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 LightClientBuilder { - /// Create a new [`LightClientBuilder`]. - pub fn new() -> LightClientBuilder { - 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) -> 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, - ) -> 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>(self, url: Url) -> Result, 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>( - self, - url: Url, - ) -> Result, 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, 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, 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, -} - -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( - self, - client: smoldot::Client, - ) -> Result { - // 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( - raw_rpc: LightClientRpc, -) -> Result, Error> { - let chain_id = raw_rpc.chain_id(); - let rpc_client = RpcClient::new(raw_rpc); - let client = OnlineClient::::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) -> Result { - 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::, _>( - &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>; - 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(|_| 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 { - 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)) - } - } -} diff --git a/subxt/src/client/light_client/mod.rs b/subxt/src/client/light_client/mod.rs deleted file mode 100644 index a4b116652e..0000000000 --- a/subxt/src/client/light_client/mod.rs +++ /dev/null @@ -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( - &self, - chain_id: smoldot::ChainId, - ) -> Result, crate::Error> { - let raw_rpc = self.raw_rpc.for_chain(chain_id); - let rpc_client = RpcClient::new(raw_rpc.clone()); - let client = OnlineClient::::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 { - client: OnlineClient, - chain_id: smoldot::ChainId, -} - -impl LightClient { - /// Construct a [`LightClient`] using its builder interface. - pub fn builder() -> LightClientBuilder { - 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) -> ::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 { - >::tx(self) - } - - /// Work with events. - pub fn events(&self) -> EventsClient { - >::events(self) - } - - /// Work with storage. - pub fn storage(&self) -> StorageClient { - >::storage(self) - } - - /// Access constants. - pub fn constants(&self) -> ConstantsClient { - >::constants(self) - } - - /// Access custom types. - pub fn custom_values(&self) -> CustomValuesClient { - >::custom_values(self) - } - - /// Work with blocks. - pub fn blocks(&self) -> BlocksClient { - >::blocks(self) - } - - /// Work with runtime API. - pub fn runtime_api(&self) -> RuntimeApiClient { - >::runtime_api(self) - } - - /// Returns the chain ID of the current light-client. - pub fn chain_id(&self) -> smoldot::ChainId { - self.chain_id - } -} - -impl OnlineClientT for LightClient { - fn backend(&self) -> &dyn crate::backend::Backend { - self.client.backend() - } -} - -impl OfflineClientT for LightClient { - fn metadata(&self) -> crate::Metadata { - self.metadata() - } - - fn genesis_hash(&self) -> ::Hash { - self.genesis_hash() - } - - fn runtime_version(&self) -> crate::backend::RuntimeVersion { - self.runtime_version() - } -} diff --git a/subxt/src/client/light_client/rpc.rs b/subxt/src/client/light_client/rpc.rs deleted file mode 100644 index ea9fe996ba..0000000000 --- a/subxt/src/client/light_client/rpc.rs +++ /dev/null @@ -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>, - ) -> Result { - 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>, - ) -> RawRpcFuture<'a, Box> { - let client = self.clone(); - let chain_id = self.chain_id(); - - Box::pin(async move { - let params = match params { - Some(params) => serde_json::to_string(¶ms).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>, - 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(¶ms).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) - }) - } -} diff --git a/subxt/src/client/mod.rs b/subxt/src/client/mod.rs index c764af4b59..fe699c4a55 100644 --- a/subxt/src/client/mod.rs +++ b/subxt/src/client/mod.rs @@ -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, diff --git a/subxt/src/client/online_client.rs b/subxt/src/client/online_client.rs index 77718caa7f..1d38016e06 100644 --- a/subxt/src/client/online_client.rs +++ b/subxt/src/client/online_client.rs @@ -84,7 +84,10 @@ impl OnlineClient { impl OnlineClient { /// 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, Error> { + pub async fn from_rpc_client( + rpc_client: impl Into, + ) -> Result, 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 OnlineClient { genesis_hash: T::Hash, runtime_version: RuntimeVersion, metadata: impl Into, - rpc_client: RpcClient, + rpc_client: impl Into, ) -> Result, 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) } diff --git a/subxt/src/error/mod.rs b/subxt/src/error/mod.rs index 5dbf1c5897..c2f5691b63 100644 --- a/subxt/src/error/mod.rs +++ b/subxt/src/error/mod.rs @@ -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: diff --git a/subxt/src/lib.rs b/subxt/src/lib.rs index b50880e1ea..f8eb443eb3 100644 --- a/subxt/src/lib.rs +++ b/subxt/src/lib.rs @@ -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::{ diff --git a/subxt/src/utils/fetch_chain_spec.rs b/subxt/src/utils/fetch_chain_spec.rs new file mode 100644 index 0000000000..b2881276c3 --- /dev/null +++ b/subxt/src/utils/fetch_chain_spec.rs @@ -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, +) -> 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_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>; + 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_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 { + 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/subxt/src/utils/mod.rs b/subxt/src/utils/mod.rs index dc9320a303..7826ffcfad 100644 --- a/subxt/src/utils/mod.rs +++ b/subxt/src/utils/mod.rs @@ -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}; diff --git a/testing/integration-tests/src/light_client/mod.rs b/testing/integration-tests/src/light_client/mod.rs index 0694273ba5..235ba09f85 100644 --- a/testing/integration-tests/src/light_client/mod.rs +++ b/testing/integration-tests/src/light_client/mod.rs @@ -29,13 +29,10 @@ use crate::utils::node_runtime; use codec::Compact; -use subxt::{ - client::{LightClient, LightClientBuilder, OnlineClientT}, - config::PolkadotConfig, -}; +use subxt::{client::OnlineClient, config::PolkadotConfig, lightclient::LightClient}; use subxt_metadata::Metadata; -type Client = LightClient; +type Client = OnlineClient; // Check that we can subscribe to non-finalized blocks. async fn non_finalized_headers_subscription(api: &Client) -> Result<(), subxt::Error> { @@ -119,9 +116,11 @@ async fn dynamic_events(api: &Client) -> Result<(), subxt::Error> { #[tokio::test] async fn light_client_testing() -> Result<(), subxt::Error> { - let api: LightClient = LightClientBuilder::new() - .build_from_url("wss://rpc.polkadot.io:443") - .await?; + let chainspec = subxt::utils::fetch_chainspec_from_rpc_node("wss://rpc.polkadot.io:443") + .await + .unwrap(); + let (_lc, rpc) = LightClient::relay_chain(chainspec.get())?; + let api = Client::from_rpc_client(rpc).await?; non_finalized_headers_subscription(&api).await?; finalized_headers_subscription(&api).await?; diff --git a/testing/integration-tests/src/utils/node_proc.rs b/testing/integration-tests/src/utils/node_proc.rs index 65a27012bc..860937a6d7 100644 --- a/testing/integration-tests/src/utils/node_proc.rs +++ b/testing/integration-tests/src/utils/node_proc.rs @@ -11,9 +11,6 @@ use subxt::{ Config, OnlineClient, }; -#[cfg(feature = "unstable-light-client")] -use subxt::client::{LightClient, LightClientBuilder}; - /// Spawn a local substrate node for testing subxt. pub struct TestNodeProcess { // Keep a handle to the node; once it's dropped the node is killed. @@ -24,12 +21,7 @@ pub struct TestNodeProcess { legacy_client: RefCell>>, rpc_client: rpc::RpcClient, - - #[cfg(not(feature = "unstable-light-client"))] client: OnlineClient, - - #[cfg(feature = "unstable-light-client")] - client: LightClient, } impl TestNodeProcess @@ -92,16 +84,9 @@ where /// will use the legacy backend by default or the unstable backend if the /// "unstable-backend-client" feature is enabled, so that we can run each /// test against both. - #[cfg(not(feature = "unstable-light-client"))] pub fn client(&self) -> OnlineClient { self.client.clone() } - - /// Returns the subxt client connected to the running node. - #[cfg(feature = "unstable-light-client")] - pub fn client(&self) -> LightClient { - self.client.clone() - } } /// Construct a test node process. @@ -235,28 +220,41 @@ async fn build_unstable_client( } #[cfg(feature = "unstable-light-client")] -async fn build_light_client(proc: &SubstrateNode) -> Result, String> { +async fn build_light_client(proc: &SubstrateNode) -> Result, String> { + use subxt::lightclient::{ChainConfig, LightClient}; + // RPC endpoint. let ws_url = format!("ws://127.0.0.1:{}", proc.ws_port()); - // Step 1. Wait for a few blocks to be produced using the subxt client. + // Wait for a few blocks to be produced using the subxt client. let client = OnlineClient::::from_url(ws_url.clone()) .await .map_err(|err| format!("Failed to connect to node rpc at {ws_url}: {err}"))?; - super::wait_for_blocks(&client).await; - // Step 2. Construct the light client. - // P2p bootnode. + // Now, configure a light client; fetch the chain spec and modify the bootnodes. let bootnode = format!( "/ip4/127.0.0.1/tcp/{}/p2p/{}", proc.p2p_port(), proc.p2p_address() ); - LightClientBuilder::new() - .bootnodes([bootnode.as_str()]) - .build_from_url(ws_url.as_str()) + let chain_spec = subxt::utils::fetch_chainspec_from_rpc_node(ws_url.as_str()) .await - .map_err(|e| format!("Failed to construct light client {}", e)) + .map_err(|e| format!("Failed to obtain chain spec from local machine: {e}"))?; + + let chain_config = ChainConfig::chain_spec(chain_spec.get()) + .set_bootnodes([bootnode.as_str()]) + .map_err(|e| format!("Light client: cannot update boot nodes: {e}"))?; + + // Instantiate the light client. + let (_lightclient, rpc) = LightClient::relay_chain(chain_config) + .map_err(|e| format!("Light client: cannot add relay chain: {e}"))?; + + // Instantiate subxt client from this. + let api = OnlineClient::from_rpc_client(rpc) + .await + .map_err(|e| format!("Failed to build OnlineClient from light client RPC: {e}"))?; + + Ok(api) } diff --git a/testing/wasm-lightclient-tests/Cargo.lock b/testing/wasm-lightclient-tests/Cargo.lock index 49094a0384..3009f03623 100644 --- a/testing/wasm-lightclient-tests/Cargo.lock +++ b/testing/wasm-lightclient-tests/Cargo.lock @@ -455,12 +455,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.3" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" dependencies = [ - "darling_core 0.20.3", - "darling_macro 0.20.3", + "darling_core 0.20.8", + "darling_macro 0.20.8", ] [[package]] @@ -479,9 +479,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.3" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" dependencies = [ "fnv", "ident_case", @@ -504,11 +504,11 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.3" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ - "darling_core 0.20.3", + "darling_core 0.20.8", "quote", "syn 2.0.48", ] @@ -589,9 +589,9 @@ dependencies = [ [[package]] name = "either" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "equivalent" @@ -1114,18 +1114,18 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" -version = "0.3.67" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "jsonrpsee" -version = "0.21.0" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9579d0ca9fb30da026bac2f0f7d9576ec93489aeb7cd4971dd5b4617d82c79b2" +checksum = "87f3ae45a64cfc0882934f963be9431b2a165d667f53140358181f262aca0702" dependencies = [ "jsonrpsee-client-transport", "jsonrpsee-core", @@ -1135,9 +1135,9 @@ dependencies = [ [[package]] name = "jsonrpsee-client-transport" -version = "0.21.0" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9f9ed46590a8d5681975f126e22531698211b926129a40a2db47cbca429220" +checksum = "455fc882e56f58228df2aee36b88a1340eafd707c76af2fa68cf94b37d461131" dependencies = [ "futures-channel", "futures-util", @@ -1158,9 +1158,9 @@ dependencies = [ [[package]] name = "jsonrpsee-core" -version = "0.21.0" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "776d009e2f591b78c038e0d053a796f94575d66ca4e77dd84bfc5e81419e436c" +checksum = "b75568f4f9696e3a47426e1985b548e1a9fcb13372a5e320372acaf04aca30d1" dependencies = [ "anyhow", "async-lock", @@ -1183,9 +1183,9 @@ dependencies = [ [[package]] name = "jsonrpsee-http-client" -version = "0.21.0" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b7de9f3219d95985eb77fd03194d7c1b56c19bce1abfcc9d07462574b15572" +checksum = "9e7a95e346f55df84fb167b7e06470e196e7d5b9488a21d69c5d9732043ba7ba" dependencies = [ "async-trait", "hyper", @@ -1203,9 +1203,9 @@ dependencies = [ [[package]] name = "jsonrpsee-types" -version = "0.21.0" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3266dfb045c9174b24c77c2dfe0084914bb23a6b2597d70c9dc6018392e1cd1b" +checksum = "3467fd35feeee179f71ab294516bdf3a81139e7aeebdd860e46897c12e1a3368" dependencies = [ "anyhow", "beef", @@ -1504,18 +1504,18 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", @@ -1616,9 +1616,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.76" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -2036,9 +2036,9 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.195" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] @@ -2054,9 +2054,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.195" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", @@ -2065,9 +2065,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.111" +version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ "itoa", "ryu", @@ -2273,9 +2273,9 @@ dependencies = [ [[package]] name = "sp-core-hashing" -version = "13.0.0" +version = "15.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb8524f01591ee58b46cd83c9dbc0fcffd2fd730dabec4f59326cd58a00f17e2" +checksum = "1e0f4990add7b2cefdeca883c0efa99bb4d912cb2196120e1500c0cc099553b0" dependencies = [ "blake2b_simd", "byteorder", @@ -2311,7 +2311,7 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "subxt" -version = "0.33.0" +version = "0.34.0" dependencies = [ "async-trait", "base58", @@ -2339,14 +2339,13 @@ dependencies = [ "subxt-macro", "subxt-metadata", "thiserror", - "tokio-stream", "tracing", "url", ] [[package]] name = "subxt-codegen" -version = "0.33.0" +version = "0.34.0" dependencies = [ "frame-metadata 16.0.0", "getrandom", @@ -2366,7 +2365,7 @@ dependencies = [ [[package]] name = "subxt-lightclient" -version = "0.33.0" +version = "0.34.0" dependencies = [ "futures", "futures-timer", @@ -2391,24 +2390,27 @@ dependencies = [ [[package]] name = "subxt-macro" -version = "0.33.0" +version = "0.34.0" dependencies = [ - "darling 0.20.3", + "darling 0.20.8", "parity-scale-codec", "proc-macro-error", + "quote", + "scale-typegen", "subxt-codegen", "syn 2.0.48", ] [[package]] name = "subxt-metadata" -version = "0.33.0" +version = "0.34.0" dependencies = [ + "derive_more", "frame-metadata 16.0.0", + "hashbrown", "parity-scale-codec", "scale-info", "sp-core-hashing", - "thiserror", ] [[package]] @@ -2441,18 +2443,18 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "thiserror" -version = "1.0.56" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.56" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", @@ -2486,9 +2488,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.1" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", @@ -2774,9 +2776,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2784,9 +2786,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", @@ -2811,9 +2813,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2821,9 +2823,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", @@ -2834,9 +2836,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wasm-bindgen-test" diff --git a/testing/wasm-lightclient-tests/tests/wasm.rs b/testing/wasm-lightclient-tests/tests/wasm.rs index 92ef3543bb..845ae94e82 100644 --- a/testing/wasm-lightclient-tests/tests/wasm.rs +++ b/testing/wasm-lightclient-tests/tests/wasm.rs @@ -1,10 +1,7 @@ #![cfg(target_arch = "wasm32")] use futures_util::StreamExt; -use subxt::{ - client::{LightClient, LightClientBuilder}, - config::PolkadotConfig, -}; +use subxt::{client::OnlineClient, config::PolkadotConfig, lightclient::LightClient}; use wasm_bindgen_test::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -49,15 +46,25 @@ async fn light_client_works() { /// We connect to an RPC node because the light client can struggle to sync in /// time to a new local node for some reason. Because this can be brittle (eg RPC nodes can /// go down or have network issues), we try a few RPC nodes until we find one that works. -async fn connect_to_rpc_node() -> LightClient { +async fn connect_to_rpc_node() -> OnlineClient { let rpc_node_urls = [ "wss://rpc.polkadot.io", "wss://1rpc.io/dot", "wss://polkadot-public-rpc.blockops.network/ws", ]; + async fn do_connect( + url: &str, + ) -> Result, Box> + { + let chainspec = subxt::utils::fetch_chainspec_from_rpc_node(url).await?; + let (_lc, rpc) = LightClient::relay_chain(chainspec.get())?; + let api = OnlineClient::from_rpc_client(rpc).await?; + Ok(api) + } + for url in rpc_node_urls { - let res = LightClientBuilder::new().build_from_url(url).await; + let res = do_connect(url).await; match res { Ok(api) => return api,