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 #[wasm_bindgen]
70 pub fn parse_invite_code(invite_code: &str) -> Result<String, JsError> {
73 let invite_code =
74 InviteCode::from_str(&invite_code).map_err(|e| JsError::new(&e.to_string()))?;
75 let federation_id = invite_code.federation_id().to_string();
76 let url = invite_code.url().to_string();
77 let result = json!({
78 "url": url,
79 "federation_id": federation_id,
80 });
81 Ok(serde_json::to_string(&result).map_err(|e| JsError::new(&e.to_string()))?)
82 }
83
84 async fn client_builder(db: Database) -> Result<fedimint_client::ClientBuilder, anyhow::Error> {
85 let mut builder = fedimint_client::Client::builder(db).await?;
86 builder.with_module(MintClientInit);
87 builder.with_module(LightningClientInit::default());
88 builder.with_primary_module(1);
90 Ok(builder)
91 }
92
93 async fn open_inner(client_name: String) -> anyhow::Result<Option<WasmClient>> {
94 let db = Database::from(MemAndIndexedDb::new(&client_name).await?);
95 if !fedimint_client::Client::is_initialized(&db).await {
96 return Ok(None);
97 }
98 let client_secret = fedimint_client::Client::load_or_generate_client_secret(&db).await?;
99 let root_secret = PlainRootSecretStrategy::to_root_secret(&client_secret);
100 let builder = Self::client_builder(db).await?;
101 Ok(Some(Self {
102 client: Arc::new(builder.open(root_secret).await?),
103 }))
104 }
105
106 async fn join_federation_inner(
107 client_name: String,
108 invite_code: String,
109 ) -> anyhow::Result<WasmClient> {
110 let db = Database::from(MemAndIndexedDb::new(&client_name).await?);
111 let client_secret = fedimint_client::Client::load_or_generate_client_secret(&db).await?;
112 let root_secret = PlainRootSecretStrategy::to_root_secret(&client_secret);
113 let builder = Self::client_builder(db).await?;
114 let invite_code = InviteCode::from_str(&invite_code)?;
115 let config = fedimint_api_client::api::net::Connector::default()
116 .download_from_invite_code(&invite_code)
117 .await?;
118 let client = Arc::new(builder.join(root_secret, config, None).await?);
119 Ok(Self { client })
120 }
121
122 #[wasm_bindgen]
123 pub fn rpc(
128 &self,
129 module: &str,
130 method: &str,
131 payload: String,
132 cb: &js_sys::Function,
133 ) -> RpcHandle {
134 let (abort_handle, abort_registration) = AbortHandle::new_pair();
135 let rpc_handle = RpcHandle { abort_handle };
136
137 let client = self.client.clone();
138 let module = module.to_string();
139 let method = method.to_string();
140 let cb = cb.clone();
141
142 wasm_bindgen_futures::spawn_local(async move {
143 let future = async {
144 let mut stream = pin!(Self::rpc_inner(&client, &module, &method, payload));
145
146 while let Some(item) = stream.next().await {
147 let this = JsValue::null();
148 let _ = match item {
149 Ok(item) => cb.call1(
150 &this,
151 &JsValue::from_str(
152 &serde_json::to_string(&json!({"data": item})).unwrap(),
153 ),
154 ),
155 Err(err) => cb.call1(
156 &this,
157 &JsValue::from_str(
158 &serde_json::to_string(&json!({"error": err.to_string()})).unwrap(),
159 ),
160 ),
161 };
162 }
163
164 let _ = cb.call1(
166 &JsValue::null(),
167 &JsValue::from_str(&serde_json::to_string(&json!({"end": null})).unwrap()),
168 );
169 };
170
171 let abortable_future = Abortable::new(future, abort_registration);
172 let _ = abortable_future.await;
173 });
174
175 rpc_handle
176 }
177 fn rpc_inner<'a>(
178 client: &'a ClientHandleArc,
179 module: &'a str,
180 method: &'a str,
181 payload: String,
182 ) -> impl futures::Stream<Item = anyhow::Result<serde_json::Value>> + 'a {
183 try_stream! {
184 let payload: serde_json::Value = serde_json::from_str(&payload)?;
185 match module {
186 "" => {
187 let mut stream = client.handle_global_rpc(method.to_owned(), payload);
188 while let Some(item) = stream.next().await {
189 yield item?;
190 }
191 }
192 "ln" => {
193 let ln = client
194 .get_first_module::<LightningClientModule>()?
195 .inner();
196 let mut stream = ln.handle_rpc(method.to_owned(), payload).await;
197 while let Some(item) = stream.next().await {
198 yield item?;
199 }
200 }
201 "mint" => {
202 let mint = client
203 .get_first_module::<fedimint_mint_client::MintClientModule>()?
204 .inner();
205 let mut stream = mint.handle_rpc(method.to_owned(), payload).await;
206 while let Some(item) = stream.next().await {
207 yield item?;
208 }
209 }
210 _ => {
211 Err(anyhow::format_err!("module not found: {module}"))?;
212 unreachable!()
213 },
214 }
215 }
216 }
217}