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::ClientHandleArc;
11use fedimint_client::secret::{PlainRootSecretStrategy, RootSecretStrategy};
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 futures::StreamExt;
18use futures::future::{AbortHandle, Abortable};
19use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription};
20use serde_json::json;
21use wasm_bindgen::prelude::wasm_bindgen;
22use wasm_bindgen::{JsError, JsValue};
23
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_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 Bolt11InvoiceDescription::Direct(desc) => desc.to_string(),
109 Bolt11InvoiceDescription::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 async fn open_inner(client_name: String) -> anyhow::Result<Option<WasmClient>> {
121 let db = Database::from(MemAndIndexedDb::new(&client_name).await?);
122 if !fedimint_client::Client::is_initialized(&db).await {
123 return Ok(None);
124 }
125 let client_secret = fedimint_client::Client::load_or_generate_client_secret(&db).await?;
126 let root_secret = PlainRootSecretStrategy::to_root_secret(&client_secret);
127 let builder = Self::client_builder(db).await?;
128 Ok(Some(Self {
129 client: Arc::new(builder.open(root_secret).await?),
130 }))
131 }
132
133 async fn join_federation_inner(
134 client_name: String,
135 invite_code: String,
136 ) -> anyhow::Result<WasmClient> {
137 let db = Database::from(MemAndIndexedDb::new(&client_name).await?);
138 let client_secret = fedimint_client::Client::load_or_generate_client_secret(&db).await?;
139 let root_secret = PlainRootSecretStrategy::to_root_secret(&client_secret);
140 let builder = Self::client_builder(db).await?;
141 let invite_code = InviteCode::from_str(&invite_code)?;
142 let config = fedimint_api_client::api::net::Connector::default()
143 .download_from_invite_code(&invite_code)
144 .await?;
145 let client = Arc::new(builder.join(root_secret, config, None).await?);
146 Ok(Self { client })
147 }
148
149 #[wasm_bindgen]
150 pub fn rpc(
155 &self,
156 module: &str,
157 method: &str,
158 payload: String,
159 cb: &js_sys::Function,
160 ) -> RpcHandle {
161 let (abort_handle, abort_registration) = AbortHandle::new_pair();
162 let rpc_handle = RpcHandle { abort_handle };
163
164 let client = self.client.clone();
165 let module = module.to_string();
166 let method = method.to_string();
167 let cb = cb.clone();
168
169 wasm_bindgen_futures::spawn_local(async move {
170 let future = async {
171 let mut stream = pin!(Self::rpc_inner(&client, &module, &method, payload));
172
173 while let Some(item) = stream.next().await {
174 let this = JsValue::null();
175 let _ = match item {
176 Ok(item) => cb.call1(
177 &this,
178 &JsValue::from_str(
179 &serde_json::to_string(&json!({"data": item})).unwrap(),
180 ),
181 ),
182 Err(err) => cb.call1(
183 &this,
184 &JsValue::from_str(
185 &serde_json::to_string(&json!({"error": err.to_string()})).unwrap(),
186 ),
187 ),
188 };
189 }
190
191 let _ = cb.call1(
193 &JsValue::null(),
194 &JsValue::from_str(&serde_json::to_string(&json!({"end": null})).unwrap()),
195 );
196 };
197
198 let abortable_future = Abortable::new(future, abort_registration);
199 let _ = abortable_future.await;
200 });
201
202 rpc_handle
203 }
204 fn rpc_inner<'a>(
205 client: &'a ClientHandleArc,
206 module: &'a str,
207 method: &'a str,
208 payload: String,
209 ) -> impl futures::Stream<Item = anyhow::Result<serde_json::Value>> + 'a {
210 try_stream! {
211 let payload: serde_json::Value = serde_json::from_str(&payload)?;
212 match module {
213 "" => {
214 let mut stream = client.handle_global_rpc(method.to_owned(), payload);
215 while let Some(item) = stream.next().await {
216 yield item?;
217 }
218 }
219 "ln" => {
220 let ln = client
221 .get_first_module::<LightningClientModule>()?
222 .inner();
223 let mut stream = ln.handle_rpc(method.to_owned(), payload).await;
224 while let Some(item) = stream.next().await {
225 yield item?;
226 }
227 }
228 "mint" => {
229 let mint = client
230 .get_first_module::<fedimint_mint_client::MintClientModule>()?
231 .inner();
232 let mut stream = mint.handle_rpc(method.to_owned(), payload).await;
233 while let Some(item) = stream.next().await {
234 yield item?;
235 }
236 }
237 _ => {
238 Err(anyhow::format_err!("module not found: {module}"))?;
239 unreachable!()
240 },
241 }
242 }
243 }
244}