subxt_lightclient/
lib.rs

1// Copyright 2019-2023 Parity Technologies (UK) Ltd.
2// This file is dual-licensed as Apache-2.0 or GPL-3.0.
3// see LICENSE for license details.
4
5//! A wrapper around [`smoldot_light`] which provides an light client capable of connecting
6//! to Substrate based chains.
7
8#![deny(missing_docs)]
9#![cfg_attr(docsrs, feature(doc_cfg))]
10
11#[cfg(any(
12    all(feature = "web", feature = "native"),
13    not(any(feature = "web", feature = "native"))
14))]
15compile_error!("subxt-lightclient: exactly one of the 'web' and 'native' features should be used.");
16
17mod platform;
18mod shared_client;
19// mod receiver;
20mod background;
21mod chain_config;
22mod rpc;
23
24use background::{BackgroundTask, BackgroundTaskHandle};
25use futures::Stream;
26use platform::DefaultPlatform;
27use serde_json::value::RawValue;
28use shared_client::SharedClient;
29use std::future::Future;
30use tokio::sync::mpsc;
31
32pub use chain_config::{ChainConfig, ChainConfigError};
33
34/// Things that can go wrong when constructing the [`LightClient`].
35#[derive(Debug, thiserror::Error)]
36pub enum LightClientError {
37    /// Error encountered while adding the chain to the light-client.
38    #[error("Failed to add the chain to the light client: {0}.")]
39    AddChainError(String),
40}
41
42/// Things that can go wrong calling methods of [`LightClientRpc`].
43#[derive(Debug, thiserror::Error)]
44pub enum LightClientRpcError {
45    /// Error response from the JSON-RPC server.
46    #[error(transparent)]
47    JsonRpcError(JsonRpcError),
48    /// Smoldot could not handle the RPC call.
49    #[error("Smoldot could not handle the RPC call: {0}.")]
50    SmoldotError(String),
51    /// Background task dropped.
52    #[error("The background task was dropped.")]
53    BackgroundTaskDropped,
54}
55
56/// An error response from the JSON-RPC server (ie smoldot) in response to
57/// a method call or as a subscription notification.
58#[derive(Debug, thiserror::Error)]
59#[error("RPC Error: {0}.")]
60pub struct JsonRpcError(Box<RawValue>);
61
62/// This represents a single light client connection to the network. Instantiate
63/// it with [`LightClient::relay_chain()`] to communicate with a relay chain, and
64/// then call [`LightClient::parachain()`] to establish connections to parachains.
65#[derive(Clone)]
66pub struct LightClient {
67    client: SharedClient<DefaultPlatform>,
68    relay_chain_id: smoldot_light::ChainId,
69}
70
71impl LightClient {
72    /// Given a chain spec, establish a connection to a relay chain. Any subsequent calls to
73    /// [`LightClient::parachain()`] will set this as the relay chain.
74    ///
75    /// # Panics
76    ///
77    /// The panic behaviour depends on the feature flag being used:
78    ///
79    /// ## Native
80    ///
81    /// Panics when called outside of a `tokio` runtime context.
82    ///
83    /// ## Web
84    ///
85    /// If smoldot panics, then the promise created will be leaked. For more details, see
86    /// https://docs.rs/wasm-bindgen-futures/latest/wasm_bindgen_futures/fn.future_to_promise.html.
87    pub fn relay_chain<'a>(
88        chain_config: impl Into<ChainConfig<'a>>,
89    ) -> Result<(Self, LightClientRpc), LightClientError> {
90        let mut client = smoldot_light::Client::new(platform::build_platform());
91        let chain_config = chain_config.into();
92        let chain_spec = chain_config.as_chain_spec();
93
94        let config = smoldot_light::AddChainConfig {
95            specification: chain_spec,
96            json_rpc: smoldot_light::AddChainConfigJsonRpc::Enabled {
97                max_pending_requests: u32::MAX.try_into().unwrap(),
98                max_subscriptions: u32::MAX,
99            },
100            database_content: "",
101            potential_relay_chains: std::iter::empty(),
102            user_data: (),
103        };
104
105        let added_chain = client
106            .add_chain(config)
107            .map_err(|err| LightClientError::AddChainError(err.to_string()))?;
108
109        let relay_chain_id = added_chain.chain_id;
110        let rpc_responses = added_chain
111            .json_rpc_responses
112            .expect("Light client RPC configured; qed");
113        let shared_client: SharedClient<_> = client.into();
114
115        let light_client_rpc =
116            LightClientRpc::new_raw(shared_client.clone(), relay_chain_id, rpc_responses);
117        let light_client = Self {
118            client: shared_client,
119            relay_chain_id,
120        };
121
122        Ok((light_client, light_client_rpc))
123    }
124
125    /// Given a chain spec, establish a connection to a parachain.
126    ///
127    /// # Panics
128    ///
129    /// The panic behaviour depends on the feature flag being used:
130    ///
131    /// ## Native
132    ///
133    /// Panics when called outside of a `tokio` runtime context.
134    ///
135    /// ## Web
136    ///
137    /// If smoldot panics, then the promise created will be leaked. For more details, see
138    /// https://docs.rs/wasm-bindgen-futures/latest/wasm_bindgen_futures/fn.future_to_promise.html.
139    pub fn parachain<'a>(
140        &self,
141        chain_config: impl Into<ChainConfig<'a>>,
142    ) -> Result<LightClientRpc, LightClientError> {
143        let chain_config = chain_config.into();
144        let chain_spec = chain_config.as_chain_spec();
145
146        let config = smoldot_light::AddChainConfig {
147            specification: chain_spec,
148            json_rpc: smoldot_light::AddChainConfigJsonRpc::Enabled {
149                max_pending_requests: u32::MAX.try_into().unwrap(),
150                max_subscriptions: u32::MAX,
151            },
152            database_content: "",
153            potential_relay_chains: std::iter::once(self.relay_chain_id),
154            user_data: (),
155        };
156
157        let added_chain = self
158            .client
159            .add_chain(config)
160            .map_err(|err| LightClientError::AddChainError(err.to_string()))?;
161
162        let chain_id = added_chain.chain_id;
163        let rpc_responses = added_chain
164            .json_rpc_responses
165            .expect("Light client RPC configured; qed");
166
167        Ok(LightClientRpc::new_raw(
168            self.client.clone(),
169            chain_id,
170            rpc_responses,
171        ))
172    }
173}
174
175/// This represents a single RPC connection to a specific chain, and is constructed by calling
176/// one of the methods on [`LightClient`]. Using this, you can make RPC requests to the chain.
177#[derive(Clone, Debug)]
178pub struct LightClientRpc {
179    handle: BackgroundTaskHandle,
180}
181
182impl LightClientRpc {
183    // Dev note: this would provide a "low leveL" interface if one is needed.
184    // Do we actually need to provide this, or can we entirely hide Smoldot?
185    pub(crate) fn new_raw<TPlat, TChain>(
186        client: impl Into<SharedClient<TPlat, TChain>>,
187        chain_id: smoldot_light::ChainId,
188        rpc_responses: smoldot_light::JsonRpcResponses<TPlat>,
189    ) -> Self
190    where
191        TPlat: smoldot_light::platform::PlatformRef + Send + 'static,
192        TChain: Send + 'static,
193    {
194        let (background_task, background_handle) =
195            BackgroundTask::new(client.into(), chain_id, rpc_responses);
196
197        // For now we spawn the background task internally, but later we can expose
198        // methods to give this back to the user so that they can exert backpressure.
199        spawn(async move { background_task.run().await });
200
201        LightClientRpc {
202            handle: background_handle,
203        }
204    }
205
206    /// Make an RPC request to a chain, getting back a result.
207    pub async fn request(
208        &self,
209        method: String,
210        params: Option<Box<RawValue>>,
211    ) -> Result<Box<RawValue>, LightClientRpcError> {
212        self.handle.request(method, params).await
213    }
214
215    /// Subscribe to some RPC method, getting back a stream of notifications.
216    pub async fn subscribe(
217        &self,
218        method: String,
219        params: Option<Box<RawValue>>,
220        unsub: String,
221    ) -> Result<LightClientRpcSubscription, LightClientRpcError> {
222        let (id, notifications) = self.handle.subscribe(method, params, unsub).await?;
223        Ok(LightClientRpcSubscription { id, notifications })
224    }
225}
226
227/// A stream of notifications handed back when [`LightClientRpc::subscribe`] is called.
228pub struct LightClientRpcSubscription {
229    notifications: mpsc::UnboundedReceiver<Result<Box<RawValue>, JsonRpcError>>,
230    id: String,
231}
232
233impl LightClientRpcSubscription {
234    /// Return the subscription ID
235    pub fn id(&self) -> &str {
236        &self.id
237    }
238}
239
240impl Stream for LightClientRpcSubscription {
241    type Item = Result<Box<RawValue>, JsonRpcError>;
242    fn poll_next(
243        mut self: std::pin::Pin<&mut Self>,
244        cx: &mut std::task::Context<'_>,
245    ) -> std::task::Poll<Option<Self::Item>> {
246        self.notifications.poll_recv(cx)
247    }
248}
249
250/// A quick helper to spawn a task that works for WASM.
251fn spawn<F: Future + Send + 'static>(future: F) {
252    #[cfg(feature = "native")]
253    tokio::spawn(async move {
254        future.await;
255    });
256    #[cfg(feature = "web")]
257    wasm_bindgen_futures::spawn_local(async move {
258        future.await;
259    });
260}