pezkuwi_subxt_lightclient/
lib.rs

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