Skip to main content

fedimint_client_wasm/
lib.rs

1#![cfg(target_family = "wasm")]
2
3use std::sync::Arc;
4
5use fedimint_client_rpc::{RpcGlobalState, RpcRequest, RpcResponse, RpcResponseHandler};
6use fedimint_core::db::Database;
7use fedimint_cursed_redb::MemAndRedb;
8use wasm_bindgen::prelude::{JsError, JsValue, wasm_bindgen};
9use web_sys::FileSystemSyncAccessHandle;
10
11/// Runs automatically when the wasm module is instantiated, before any
12/// exported function can be called; calling it again is harmless.
13///
14/// Without a hook, a panic anywhere in the module (including the RPC tasks
15/// spawned via `wasm_bindgen_futures::spawn_local`) traps with a message-less
16/// `RuntimeError: unreachable executed` and the panic message and Rust
17/// location are lost. Log them to the console instead.
18///
19/// When triaging, trust the first logged panic: after it, the executor's
20/// poisoned state may log an unrelated `BorrowMutError` panic as well.
21#[wasm_bindgen(start)]
22fn install_panic_hook() {
23    console_error_panic_hook::set_once();
24}
25
26struct JsFunctionWrapper(js_sys::Function);
27
28impl RpcResponseHandler for JsFunctionWrapper {
29    fn handle_response(&self, response: RpcResponse) {
30        let response = serde_json::to_string(&response).expect(
31            "RpcResponse is numbers, strings and serde_json::Value; serialization cannot fail",
32        );
33        if let Err(err) = self
34            .0
35            .call1(&JsValue::null(), &JsValue::from_str(&response))
36        {
37            // There is no tracing subscriber in the wasm client, so log
38            // straight to the console; swallowing this leaves the request
39            // hanging with zero diagnostics.
40            web_sys::console::error_2(&JsValue::from_str("RPC response callback threw"), &err);
41        }
42    }
43}
44
45#[wasm_bindgen]
46struct RpcHandler {
47    state: Arc<RpcGlobalState>,
48}
49
50#[wasm_bindgen]
51impl RpcHandler {
52    #[wasm_bindgen(constructor)]
53    pub async fn new(sync_handle: FileSystemSyncAccessHandle) -> Result<RpcHandler, JsError> {
54        // Return errors instead of panicking: a panic in an async export
55        // leaves the returned `Promise` unsettled forever, so the caller
56        // would hang instead of getting a rejection it can catch.
57        let cursed_db = MemAndRedb::new(sync_handle)
58            .map_err(|err| JsError::new(&format!("Failed to open client database: {err:#}")))?;
59        let database = Database::new(cursed_db, Default::default());
60        let connectors = fedimint_connectors::ConnectorRegistry::build_from_client_defaults()
61            .bind()
62            .await
63            .map_err(|err| JsError::new(&format!("Failed to bind client connectors: {err:#}")))?;
64
65        let state = Arc::new(RpcGlobalState::new(connectors, database));
66
67        Ok(Self { state })
68    }
69
70    #[wasm_bindgen]
71    pub fn rpc(&self, request: String, cb: js_sys::Function) -> Result<(), JsError> {
72        let request: RpcRequest = serde_json::from_str(&request)
73            .map_err(|e| JsError::new(&format!("Failed to parse request: {}", e)))?;
74
75        let handled = self
76            .state
77            .clone()
78            .handle_rpc(request, JsFunctionWrapper(cb));
79
80        if let Some(task) = handled.task {
81            wasm_bindgen_futures::spawn_local(task);
82        }
83        Ok(())
84    }
85}