fedimint_client_wasm/
lib.rs
1#![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::secret::{PlainRootSecretStrategy, RootSecretStrategy};
11use fedimint_client::{ClientHandleArc, RootSecret};
12use fedimint_client_module::module::IClientModule;
13use fedimint_core::db::Database;
14use fedimint_core::invite_code::InviteCode;
15use fedimint_ln_client::{LightningClientInit, LightningClientModule};
16use fedimint_mint_client::MintClientInit;
17use fedimint_wallet_client::{WalletClientInit, WalletClientModule};
18use futures::StreamExt;
19use futures::future::{AbortHandle, Abortable};
20use lightning_invoice::Bolt11InvoiceDescriptionRef;
21use serde_json::json;
22use wasm_bindgen::prelude::wasm_bindgen;
23use wasm_bindgen::{JsError, JsValue};
24#[wasm_bindgen]
25pub struct WasmClient {
26 client: ClientHandleArc,
27}
28
29#[wasm_bindgen]
30pub struct RpcHandle {
31 abort_handle: AbortHandle,
32}
33
34#[wasm_bindgen]
35impl RpcHandle {
36 #[wasm_bindgen]
37 pub fn cancel(&self) {
38 self.abort_handle.abort();
39 }
40}
41
42#[wasm_bindgen]
43impl WasmClient {
44 #[wasm_bindgen]
45 pub async fn open(client_name: String) -> Result<Option<WasmClient>, JsError> {
54 Self::open_inner(client_name)
55 .await
56 .map_err(|x| JsError::new(&x.to_string()))
57 }
58
59 #[wasm_bindgen]
60 pub async fn join_federation(
62 client_name: String,
63 invite_code: String,
64 ) -> Result<WasmClient, JsError> {
65 Self::join_federation_inner(client_name, invite_code)
66 .await
67 .map_err(|x| JsError::new(&x.to_string()))
68 }
69
70 #[wasm_bindgen]
71 pub fn parse_invite_code(invite_code: &str) -> Result<String, JsError> {
74 let invite_code =
75 InviteCode::from_str(&invite_code).map_err(|e| JsError::new(&e.to_string()))?;
76 let federation_id = invite_code.federation_id().to_string();
77 let url = invite_code.url().to_string();
78 let result = json!({
79 "url": url,
80 "federation_id": federation_id,
81 });
82 Ok(serde_json::to_string(&result).map_err(|e| JsError::new(&e.to_string()))?)
83 }
84
85 async fn client_builder(db: Database) -> Result<fedimint_client::ClientBuilder, anyhow::Error> {
86 let mut builder = fedimint_client::Client::builder(db).await?;
87 builder.with_module(MintClientInit);
88 builder.with_module(LightningClientInit::default());
89 builder.with_module(WalletClientInit(None));
90 builder.with_primary_module(1);
91 Ok(builder)
92 }
93
94 #[wasm_bindgen]
95 pub fn parse_bolt11_invoice(invoice_str: &str) -> Result<String, JsError> {
98 let invoice = lightning_invoice::Bolt11Invoice::from_str(invoice_str)
99 .map_err(|e| JsError::new(&format!("Failed to parse Lightning invoice: {}", e)))?;
100
101 let amount_msat = invoice.amount_milli_satoshis().unwrap_or(0);
102 let amount_sat = amount_msat as f64 / 1000.0;
103
104 let expiry_seconds = invoice.expiry_time().as_secs();
105
106 let description = match invoice.description() {
108 Bolt11InvoiceDescriptionRef::Direct(desc) => desc.to_string(),
109 Bolt11InvoiceDescriptionRef::Hash(_) => "Description hash only".to_string(),
110 };
111
112 let response = json!({
113 "amount": amount_sat,
114 "expiry": expiry_seconds,
115 "memo": description,
116 });
117 Ok(serde_json::to_string(&response).map_err(|e| JsError::new(&e.to_string()))?)
118 }
119
120 #[wasm_bindgen]
121 pub async fn preview_federation(invite_code: String) -> Result<JsValue, JsError> {
122 let invite =
123 InviteCode::from_str(&invite_code).map_err(|e| JsError::new(&e.to_string()))?;
124 let client_config = fedimint_api_client::api::net::Connector::default()
125 .download_from_invite_code(&invite)
126 .await
127 .map_err(|e| JsError::new(&e.to_string()))?;
128 let json_config = client_config.to_json();
129 let federation_id = client_config.calculate_federation_id();
130
131 let preview = json!({
132 "config": json_config,
133 "federation_id": federation_id.to_string(),
134 });
135
136 Ok(JsValue::from_str(
137 &serde_json::to_string(&preview).map_err(|e| JsError::new(&e.to_string()))?,
138 ))
139 }
140
141 async fn open_inner(client_name: String) -> anyhow::Result<Option<WasmClient>> {
142 let db = Database::from(MemAndIndexedDb::new(&client_name).await?);
143 if !fedimint_client::Client::is_initialized(&db).await {
144 return Ok(None);
145 }
146 let client_secret = fedimint_client::Client::load_or_generate_client_secret(&db).await?;
147 let root_secret = PlainRootSecretStrategy::to_root_secret(&client_secret);
148 let builder = Self::client_builder(db).await?;
149 Ok(Some(Self {
150 client: Arc::new(
151 builder
152 .open(RootSecret::LegacyDoubleDerive(root_secret))
153 .await?,
154 ),
155 }))
156 }
157
158 async fn join_federation_inner(
159 client_name: String,
160 invite_code: String,
161 ) -> anyhow::Result<WasmClient> {
162 let db = Database::from(MemAndIndexedDb::new(&client_name).await?);
163 let client_secret = fedimint_client::Client::load_or_generate_client_secret(&db).await?;
164 let root_secret = PlainRootSecretStrategy::to_root_secret(&client_secret);
165 let builder = Self::client_builder(db).await?;
166 let invite_code = InviteCode::from_str(&invite_code)?;
167 let client = Arc::new(
168 builder
169 .preview(&invite_code)
170 .await?
171 .join(RootSecret::LegacyDoubleDerive(root_secret))
172 .await?,
173 );
174 Ok(Self { client })
175 }
176
177 #[wasm_bindgen]
178 pub fn rpc(
183 &self,
184 module: &str,
185 method: &str,
186 payload: String,
187 cb: &js_sys::Function,
188 ) -> RpcHandle {
189 let (abort_handle, abort_registration) = AbortHandle::new_pair();
190 let rpc_handle = RpcHandle { abort_handle };
191
192 let client = self.client.clone();
193 let module = module.to_string();
194 let method = method.to_string();
195 let cb = cb.clone();
196
197 wasm_bindgen_futures::spawn_local(async move {
198 let future = async {
199 let mut stream = pin!(Self::rpc_inner(&client, &module, &method, payload));
200
201 while let Some(item) = stream.next().await {
202 let this = JsValue::null();
203 let _ = match item {
204 Ok(item) => cb.call1(
205 &this,
206 &JsValue::from_str(
207 &serde_json::to_string(&json!({"data": item})).unwrap(),
208 ),
209 ),
210 Err(err) => cb.call1(
211 &this,
212 &JsValue::from_str(
213 &serde_json::to_string(&json!({"error": err.to_string()})).unwrap(),
214 ),
215 ),
216 };
217 }
218
219 let _ = cb.call1(
221 &JsValue::null(),
222 &JsValue::from_str(&serde_json::to_string(&json!({"end": null})).unwrap()),
223 );
224 };
225
226 let abortable_future = Abortable::new(future, abort_registration);
227 let _ = abortable_future.await;
228 });
229
230 rpc_handle
231 }
232 fn rpc_inner<'a>(
233 client: &'a ClientHandleArc,
234 module: &'a str,
235 method: &'a str,
236 payload: String,
237 ) -> impl futures::Stream<Item = anyhow::Result<serde_json::Value>> + 'a {
238 try_stream! {
239 let payload: serde_json::Value = serde_json::from_str(&payload)?;
240 match module {
241 "" => {
242 let mut stream = client.handle_global_rpc(method.to_owned(), payload);
243 while let Some(item) = stream.next().await {
244 yield item?;
245 }
246 }
247 "ln" => {
248 let ln = client
249 .get_first_module::<LightningClientModule>()?
250 .inner();
251 let mut stream = ln.handle_rpc(method.to_owned(), payload).await;
252 while let Some(item) = stream.next().await {
253 yield item?;
254 }
255 }
256 "mint" => {
257 let mint = client
258 .get_first_module::<fedimint_mint_client::MintClientModule>()?
259 .inner();
260 let mut stream = mint.handle_rpc(method.to_owned(), payload).await;
261 while let Some(item) = stream.next().await {
262 yield item?;
263 }
264 }
265 "wallet" => {
266 let wallet = client
267 .get_first_module::<WalletClientModule>()?
268 .inner();
269 let mut stream = wallet.handle_rpc(method.to_owned(), payload).await;
270 while let Some(item) = stream.next().await {
271 yield item?;
272 }
273 }
274 _ => {
275 Err(anyhow::format_err!("module not found: {module}"))?;
276 unreachable!()
277 },
278 }
279 }
280 }
281}