1#![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;
19mod 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#[derive(Debug, thiserror::Error)]
36pub enum LightClientError {
37 #[error("Failed to add the chain to the light client: {0}.")]
39 AddChainError(String),
40}
41
42#[derive(Debug, thiserror::Error)]
44pub enum LightClientRpcError {
45 #[error(transparent)]
47 JsonRpcError(JsonRpcError),
48 #[error("Smoldot could not handle the RPC call: {0}.")]
50 SmoldotError(String),
51 #[error("The background task was dropped.")]
53 BackgroundTaskDropped,
54}
55
56#[derive(Debug, thiserror::Error)]
59#[error("RPC Error: {0}.")]
60pub struct JsonRpcError(Box<RawValue>);
61
62impl JsonRpcError {
63 pub fn try_deserialize<'a, T: serde::de::Deserialize<'a>>(
65 &'a self,
66 ) -> Result<T, serde_json::Error> {
67 serde_json::from_str(self.0.get())
68 }
69}
70
71#[derive(Clone)]
75pub struct LightClient {
76 client: SharedClient<DefaultPlatform>,
77 relay_chain_id: smoldot_light::ChainId,
78}
79
80impl LightClient {
81 pub fn relay_chain<'a>(
97 chain_config: impl Into<ChainConfig<'a>>,
98 ) -> Result<(Self, LightClientRpc), LightClientError> {
99 let mut client = smoldot_light::Client::new(platform::build_platform());
100 let chain_config = chain_config.into();
101 let chain_spec = chain_config.as_chain_spec();
102
103 let config = smoldot_light::AddChainConfig {
104 specification: chain_spec,
105 json_rpc: smoldot_light::AddChainConfigJsonRpc::Enabled {
106 max_pending_requests: u32::MAX.try_into().unwrap(),
107 max_subscriptions: u32::MAX,
108 },
109 database_content: "",
110 potential_relay_chains: std::iter::empty(),
111 user_data: (),
112 };
113
114 let added_chain = client
115 .add_chain(config)
116 .map_err(|err| LightClientError::AddChainError(err.to_string()))?;
117
118 let relay_chain_id = added_chain.chain_id;
119 let rpc_responses = added_chain
120 .json_rpc_responses
121 .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 {
127 client: shared_client,
128 relay_chain_id,
129 };
130
131 Ok((light_client, light_client_rpc))
132 }
133
134 pub fn parachain<'a>(
149 &self,
150 chain_config: impl Into<ChainConfig<'a>>,
151 ) -> Result<LightClientRpc, LightClientError> {
152 let chain_config = chain_config.into();
153 let chain_spec = chain_config.as_chain_spec();
154
155 let config = smoldot_light::AddChainConfig {
156 specification: chain_spec,
157 json_rpc: smoldot_light::AddChainConfigJsonRpc::Enabled {
158 max_pending_requests: u32::MAX.try_into().unwrap(),
159 max_subscriptions: u32::MAX,
160 },
161 database_content: "",
162 potential_relay_chains: std::iter::once(self.relay_chain_id),
163 user_data: (),
164 };
165
166 let added_chain = self
167 .client
168 .add_chain(config)
169 .map_err(|err| LightClientError::AddChainError(err.to_string()))?;
170
171 let chain_id = added_chain.chain_id;
172 let rpc_responses = added_chain
173 .json_rpc_responses
174 .expect("Light client RPC configured; qed");
175
176 Ok(LightClientRpc::new_raw(
177 self.client.clone(),
178 chain_id,
179 rpc_responses,
180 ))
181 }
182}
183
184#[derive(Clone, Debug)]
187pub struct LightClientRpc {
188 handle: BackgroundTaskHandle,
189}
190
191impl LightClientRpc {
192 pub(crate) fn new_raw<TPlat, TChain>(
195 client: impl Into<SharedClient<TPlat, TChain>>,
196 chain_id: smoldot_light::ChainId,
197 rpc_responses: smoldot_light::JsonRpcResponses<TPlat>,
198 ) -> Self
199 where
200 TPlat: smoldot_light::platform::PlatformRef + Send + 'static,
201 TChain: Send + 'static,
202 {
203 let (background_task, background_handle) =
204 BackgroundTask::new(client.into(), chain_id, rpc_responses);
205
206 spawn(async move { background_task.run().await });
209
210 LightClientRpc {
211 handle: background_handle,
212 }
213 }
214
215 pub async fn request(
217 &self,
218 method: String,
219 params: Option<Box<RawValue>>,
220 ) -> Result<Box<RawValue>, LightClientRpcError> {
221 self.handle.request(method, params).await
222 }
223
224 pub async fn subscribe(
226 &self,
227 method: String,
228 params: Option<Box<RawValue>>,
229 unsub: String,
230 ) -> Result<LightClientRpcSubscription, LightClientRpcError> {
231 let (id, notifications) = self.handle.subscribe(method, params, unsub).await?;
232 Ok(LightClientRpcSubscription { id, notifications })
233 }
234}
235
236pub struct LightClientRpcSubscription {
238 notifications: mpsc::UnboundedReceiver<Result<Box<RawValue>, JsonRpcError>>,
239 id: String,
240}
241
242impl LightClientRpcSubscription {
243 pub fn id(&self) -> &str {
245 &self.id
246 }
247}
248
249impl Stream for LightClientRpcSubscription {
250 type Item = Result<Box<RawValue>, JsonRpcError>;
251 fn poll_next(
252 mut self: std::pin::Pin<&mut Self>,
253 cx: &mut std::task::Context<'_>,
254 ) -> std::task::Poll<Option<Self::Item>> {
255 self.notifications.poll_recv(cx)
256 }
257}
258
259fn spawn<F: Future + Send + 'static>(future: F) {
261 #[cfg(feature = "native")]
262 tokio::spawn(async move {
263 future.await;
264 });
265 #[cfg(feature = "web")]
266 wasm_bindgen_futures::spawn_local(async move {
267 future.await;
268 });
269}