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