1#![deny(missing_docs)]
9
10#[cfg(any(
11 all(feature = "web", feature = "native"),
12 not(any(feature = "web", feature = "native"))
13))]
14compile_error!("subxt-lightclient: exactly one of the 'web' and 'native' features should be used.");
15
16mod platform;
17mod shared_client;
18mod background;
20mod chain_config;
21mod rpc;
22
23use background::{BackgroundTask, BackgroundTaskHandle};
24use futures::Stream;
25use platform::DefaultPlatform;
26use serde_json::value::RawValue;
27use shared_client::SharedClient;
28use std::future::Future;
29use tokio::sync::mpsc;
30
31pub use chain_config::{ChainConfig, ChainConfigError};
32
33#[derive(Debug, thiserror::Error)]
35pub enum LightClientError {
36 #[error("Failed to add the chain to the light client: {0}.")]
38 AddChainError(String),
39}
40
41#[derive(Debug, thiserror::Error)]
43pub enum LightClientRpcError {
44 #[error(transparent)]
46 JsonRpcError(JsonRpcError),
47 #[error("Smoldot could not handle the RPC call: {0}.")]
49 SmoldotError(String),
50 #[error("The background task was dropped.")]
52 BackgroundTaskDropped,
53}
54
55#[derive(Debug, thiserror::Error)]
58#[error("RPC Error: {0}.")]
59pub struct JsonRpcError(Box<RawValue>);
60
61impl JsonRpcError {
62 pub fn try_deserialize<'a, T: serde::de::Deserialize<'a>>(
64 &'a self,
65 ) -> Result<T, serde_json::Error> {
66 serde_json::from_str(self.0.get())
67 }
68}
69
70#[derive(Clone)]
74pub struct LightClient {
75 client: SharedClient<DefaultPlatform>,
76 relay_chain_id: smoldot_light::ChainId,
77}
78
79impl LightClient {
80 pub fn relay_chain<'a>(
96 chain_config: impl Into<ChainConfig<'a>>,
97 ) -> Result<(Self, LightClientRpc), LightClientError> {
98 let mut client = smoldot_light::Client::new(platform::build_platform());
99 let chain_config = chain_config.into();
100 let chain_spec = chain_config.as_chain_spec();
101
102 let config = smoldot_light::AddChainConfig {
103 specification: chain_spec,
104 json_rpc: smoldot_light::AddChainConfigJsonRpc::Enabled {
105 max_pending_requests: u32::MAX.try_into().unwrap(),
106 max_subscriptions: u32::MAX,
107 },
108 database_content: "",
109 potential_relay_chains: std::iter::empty(),
110 user_data: (),
111 statement_protocol_config: None,
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 statement_protocol_config: None,
165 };
166
167 let added_chain = self
168 .client
169 .add_chain(config)
170 .map_err(|err| LightClientError::AddChainError(err.to_string()))?;
171
172 let chain_id = added_chain.chain_id;
173 let rpc_responses = added_chain
174 .json_rpc_responses
175 .expect("Light client RPC configured; qed");
176
177 Ok(LightClientRpc::new_raw(
178 self.client.clone(),
179 chain_id,
180 rpc_responses,
181 ))
182 }
183}
184
185#[derive(Clone, Debug)]
188pub struct LightClientRpc {
189 handle: BackgroundTaskHandle,
190}
191
192impl LightClientRpc {
193 pub(crate) fn new_raw<TPlat, TChain>(
196 client: impl Into<SharedClient<TPlat, TChain>>,
197 chain_id: smoldot_light::ChainId,
198 rpc_responses: smoldot_light::JsonRpcResponses<TPlat>,
199 ) -> Self
200 where
201 TPlat: smoldot_light::platform::PlatformRef + Send + 'static,
202 TChain: Send + 'static,
203 {
204 let (background_task, background_handle) =
205 BackgroundTask::new(client.into(), chain_id, rpc_responses);
206
207 spawn(async move { background_task.run().await });
210
211 LightClientRpc {
212 handle: background_handle,
213 }
214 }
215
216 pub async fn request(
218 &self,
219 method: String,
220 params: Option<Box<RawValue>>,
221 ) -> Result<Box<RawValue>, LightClientRpcError> {
222 self.handle.request(method, params).await
223 }
224
225 pub async fn subscribe(
227 &self,
228 method: String,
229 params: Option<Box<RawValue>>,
230 unsub: String,
231 ) -> Result<LightClientRpcSubscription, LightClientRpcError> {
232 let (id, notifications) = self.handle.subscribe(method, params, unsub).await?;
233 Ok(LightClientRpcSubscription { id, notifications })
234 }
235}
236
237pub struct LightClientRpcSubscription {
239 notifications: mpsc::UnboundedReceiver<Result<Box<RawValue>, JsonRpcError>>,
240 id: String,
241}
242
243impl LightClientRpcSubscription {
244 pub fn id(&self) -> &str {
246 &self.id
247 }
248}
249
250impl Stream for LightClientRpcSubscription {
251 type Item = Result<Box<RawValue>, JsonRpcError>;
252 fn poll_next(
253 mut self: std::pin::Pin<&mut Self>,
254 cx: &mut std::task::Context<'_>,
255 ) -> std::task::Poll<Option<Self::Item>> {
256 self.notifications.poll_recv(cx)
257 }
258}
259
260fn spawn<F: Future + Send + 'static>(future: F) {
262 #[cfg(feature = "native")]
263 tokio::spawn(async move {
264 future.await;
265 });
266 #[cfg(feature = "web")]
267 wasm_bindgen_futures::spawn_local(async move {
268 future.await;
269 });
270}