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::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    /// Open fedimint client with already joined federation.
45    ///
46    /// After you have joined a federation, you can reopen the fedimint client
47    /// with same client_name. Opening client with same name at same time is
48    /// not supported. You can close the current client by calling
49    /// `client.free()`. NOTE: The client will remain active until all the
50    /// running rpc calls have finished.
51    // WasmClient::free is auto generated by wasm bindgen.
52    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    /// Open a fedimint client by join a federation.
60    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    /// Parse an invite code and extract its components without joining the
71    /// federation
72    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        // FIXME: wallet module?
89        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    /// Call a fedimint client rpc the responses are returned using `cb`
124    /// callback. Each rpc call *can* return multiple responses by calling
125    /// `cb` multiple times. The returned RpcHandle can be used to cancel the
126    /// operation.
127    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                // Send the end message
165                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}