fedimint_client_wasm/
lib.rs1#![cfg(target_family = "wasm")]
2mod db;
3
4use std::pin::pin;
5use std::str::FromStr;
6use std::sync::Arc;
7
8use async_stream::try_stream;
9use db::MemAndIndexedDb;
10use fedimint_client::module::IClientModule;
11use fedimint_client::secret::{PlainRootSecretStrategy, RootSecretStrategy};
12use fedimint_client::ClientHandleArc;
13use fedimint_core::db::Database;
14use fedimint_core::invite_code::InviteCode;
15use fedimint_ln_client::{LightningClientInit, LightningClientModule};
16use fedimint_mint_client::MintClientInit;
17use futures::future::{AbortHandle, Abortable};
18use futures::StreamExt;
19use serde_json::json;
20use wasm_bindgen::prelude::wasm_bindgen;
21use wasm_bindgen::{JsError, JsValue};
22
23#[wasm_bindgen]
24pub struct WasmClient {
25 client: ClientHandleArc,
26}
27
28#[wasm_bindgen]
29pub struct RpcHandle {
30 abort_handle: AbortHandle,
31}
32
33#[wasm_bindgen]
34impl RpcHandle {
35 #[wasm_bindgen]
36 pub fn cancel(&self) {
37 self.abort_handle.abort();
38 }
39}
40
41#[wasm_bindgen]
42impl WasmClient {
43 #[wasm_bindgen]
44 pub async fn open(client_name: String) -> Result<Option<WasmClient>, JsError> {
53 Self::open_inner(client_name)
54 .await
55 .map_err(|x| JsError::new(&x.to_string()))
56 }
57
58 #[wasm_bindgen]
59 pub async fn join_federation(
61 client_name: String,
62 invite_code: String,
63 ) -> Result<WasmClient, JsError> {
64 Self::join_federation_inner(client_name, invite_code)
65 .await
66 .map_err(|x| JsError::new(&x.to_string()))
67 }
68
69 async fn client_builder(db: Database) -> Result<fedimint_client::ClientBuilder, anyhow::Error> {
70 let mut builder = fedimint_client::Client::builder(db).await?;
71 builder.with_module(MintClientInit);
72 builder.with_module(LightningClientInit::default());
73 builder.with_primary_module(1);
75 Ok(builder)
76 }
77
78 async fn open_inner(client_name: String) -> anyhow::Result<Option<WasmClient>> {
79 let db = Database::from(MemAndIndexedDb::new(&client_name).await?);
80 if !fedimint_client::Client::is_initialized(&db).await {
81 return Ok(None);
82 }
83 let client_secret = fedimint_client::Client::load_or_generate_client_secret(&db).await?;
84 let root_secret = PlainRootSecretStrategy::to_root_secret(&client_secret);
85 let builder = Self::client_builder(db).await?;
86 Ok(Some(Self {
87 client: Arc::new(builder.open(root_secret).await?),
88 }))
89 }
90
91 async fn join_federation_inner(
92 client_name: String,
93 invite_code: String,
94 ) -> anyhow::Result<WasmClient> {
95 let db = Database::from(MemAndIndexedDb::new(&client_name).await?);
96 let client_secret = fedimint_client::Client::load_or_generate_client_secret(&db).await?;
97 let root_secret = PlainRootSecretStrategy::to_root_secret(&client_secret);
98 let builder = Self::client_builder(db).await?;
99 let invite_code = InviteCode::from_str(&invite_code)?;
100 let config = fedimint_api_client::api::net::Connector::default()
101 .download_from_invite_code(&invite_code)
102 .await?;
103 let client = Arc::new(builder.join(root_secret, config, None).await?);
104 Ok(Self { client })
105 }
106
107 #[wasm_bindgen]
108 pub fn rpc(
113 &self,
114 module: &str,
115 method: &str,
116 payload: String,
117 cb: &js_sys::Function,
118 ) -> RpcHandle {
119 let (abort_handle, abort_registration) = AbortHandle::new_pair();
120 let rpc_handle = RpcHandle { abort_handle };
121
122 let client = self.client.clone();
123 let module = module.to_string();
124 let method = method.to_string();
125 let cb = cb.clone();
126
127 wasm_bindgen_futures::spawn_local(async move {
128 let future = async {
129 let mut stream = pin!(Self::rpc_inner(&client, &module, &method, payload));
130
131 while let Some(item) = stream.next().await {
132 let this = JsValue::null();
133 let _ = match item {
134 Ok(item) => cb.call1(
135 &this,
136 &JsValue::from_str(
137 &serde_json::to_string(&json!({"data": item})).unwrap(),
138 ),
139 ),
140 Err(err) => cb.call1(
141 &this,
142 &JsValue::from_str(
143 &serde_json::to_string(&json!({"error": err.to_string()})).unwrap(),
144 ),
145 ),
146 };
147 }
148
149 let _ = cb.call1(
151 &JsValue::null(),
152 &JsValue::from_str(&serde_json::to_string(&json!({"end": null})).unwrap()),
153 );
154 };
155
156 let abortable_future = Abortable::new(future, abort_registration);
157 let _ = abortable_future.await;
158 });
159
160 rpc_handle
161 }
162 fn rpc_inner<'a>(
163 client: &'a ClientHandleArc,
164 module: &'a str,
165 method: &'a str,
166 payload: String,
167 ) -> impl futures::Stream<Item = anyhow::Result<serde_json::Value>> + 'a {
168 try_stream! {
169 let payload: serde_json::Value = serde_json::from_str(&payload)?;
170 match module {
171 "" => {
172 let mut stream = client.handle_global_rpc(method.to_owned(), payload);
173 while let Some(item) = stream.next().await {
174 yield item?;
175 }
176 }
177 "ln" => {
178 let ln = client
179 .get_first_module::<LightningClientModule>()?
180 .inner();
181 let mut stream = ln.handle_rpc(method.to_owned(), payload).await;
182 while let Some(item) = stream.next().await {
183 yield item?;
184 }
185 }
186 "mint" => {
187 let mint = client
188 .get_first_module::<fedimint_mint_client::MintClientModule>()?
189 .inner();
190 let mut stream = mint.handle_rpc(method.to_owned(), payload).await;
191 while let Some(item) = stream.next().await {
192 yield item?;
193 }
194 }
195 _ => {
196 Err(anyhow::format_err!("module not found: {module}"))?;
197 unreachable!()
198 },
199 }
200 }
201 }
202}