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    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        // FIXME: wallet module?
74        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    /// Call a fedimint client rpc the responses are returned using `cb`
109    /// callback. Each rpc call *can* return multiple responses by calling
110    /// `cb` multiple times. The returned RpcHandle can be used to cancel the
111    /// operation.
112    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                // Send the end message
150                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}