mina_node_web/
lib.rs

1#![cfg(target_family = "wasm")]
2
3pub use mina_node_common::*;
4
5mod rayon;
6pub use rayon::init_rayon;
7
8mod node;
9pub use node::{Node, NodeBuilder};
10
11use ::node::{
12    account::AccountSecretKey,
13    core::{log, thread},
14    snark::{BlockVerifier, TransactionVerifier},
15    transition_frontier::genesis::GenesisConfig,
16};
17use anyhow::Context;
18use gloo_utils::format::JsValueSerdeExt;
19use ledger::proofs::provers::BlockProver;
20use mina_node_common::rpc::RpcSender;
21use wasm_bindgen::prelude::*;
22
23use crate::node::P2pTaskRemoteSpawner;
24
25/// Automatically run after wasm is loaded.
26#[wasm_bindgen(start)]
27fn main() {
28    thread::main_thread_init();
29    wasm_bindgen_futures::spawn_local(async {
30        console_error_panic_hook::set_once();
31        tracing::initialize(tracing::Level::DEBUG);
32
33        init_rayon().await.unwrap();
34    });
35}
36
37#[wasm_bindgen]
38pub fn build_env() -> JsValue {
39    JsValue::from_serde(&::node::BuildEnv::get()).unwrap_or_default()
40}
41
42fn parse_bp_key(key: JsValue) -> Option<AccountSecretKey> {
43    if key.is_falsy() {
44        return None;
45    }
46
47    if key.is_string() {
48        return Some(
49            key.as_string()
50                .unwrap()
51                .parse()
52                .expect("failed to parse passed block producer keys"),
53        );
54    }
55
56    let (encrypted, password) = if key.is_array() {
57        let arr: js_sys::Array = key.into();
58        let password = arr
59            .at(1)
60            .as_string()
61            .expect("invalid block_producer password");
62        let encrypted = arr
63            .at(0)
64            .into_serde()
65            .expect("block_producer encrypted key decode failed");
66        (encrypted, password)
67    } else {
68        panic!("unsupported block_producer keys type: {key:?}");
69    };
70
71    Some(
72        AccountSecretKey::from_encrypted(&encrypted, &password)
73            .expect("block_producer secret key decrypt failed"),
74    )
75}
76
77/// Starts a Mina node in a WASM environment and returns an RPC interface.
78///
79/// This is the main entry point for running a Mina node from JavaScript/WASM.
80/// It spawns the node in a separate thread, sets up all necessary components,
81/// and returns an RPC sender that can be used to communicate with the running
82/// node.
83///
84/// # Arguments
85///
86/// * `block_producer` - Block producer configuration as a JavaScript value.
87///   Can be one of:
88///   - `null`/`undefined`: No block production
89///   - `string`: Plain text secret key
90///   - `[encrypted_key, password]`: Array with encrypted key and password
91/// * `seed_nodes_urls` - Optional list of URLs to fetch peer lists from.
92///   Each URL should return newline-separated peer addresses. This is similar
93///   to the `--peer-list-url` flag in the native node, but allows multiple URLs
94///   to be supplied.
95/// * `seed_nodes_addresses` - Optional list of peer addresses to connect to
96///   directly, in [WebRTC Multiaddr-ish format](https://o1-labs.github.io/mina-rust/docs/developers/webrtc#address-format-differences)
97///   This is directly comparable to the `--peers` flag in the native node.
98/// * `genesis_config_url` - Optional URL to fetch genesis configuration from.
99///   Genesis config must be in bin_prot format. If not provided, uses the default
100///   devnet configuration.
101///
102/// # Returns
103///
104/// An `RpcSender` that can be used to send RPC commands to the running node.
105///
106/// # Panics
107///
108/// Panics if:
109/// - Block producer key parsing fails
110/// - Node setup or build fails
111/// - Genesis configuration cannot be fetched
112///
113/// # Example
114///
115/// ```javascript
116/// const rpc = await run(
117///   null,  // No block production
118///   ["https://bootnodes.minaprotocol.com/networks/devnet-webrtc.txt"],
119///   ["/PEER_ID/https/webrtc-peer-signaling.example.com/443"],
120///   null, // Use the default devnet configuration
121/// );
122/// ```
123#[wasm_bindgen]
124pub async fn run(
125    block_producer: JsValue,
126    seed_nodes_urls: Option<Vec<String>>,
127    seed_nodes_addresses: Option<Vec<String>>,
128    genesis_config_url: Option<String>,
129) -> RpcSender {
130    let block_producer = parse_bp_key(block_producer);
131
132    let (rpc_sender_tx, rpc_sender_rx) = ::node::core::channels::oneshot::channel();
133    let _ = thread::spawn(move || {
134        wasm_bindgen_futures::spawn_local(async move {
135            let mut node = setup_node(
136                block_producer,
137                seed_nodes_urls,
138                seed_nodes_addresses,
139                genesis_config_url,
140            )
141            .await;
142            let _ = rpc_sender_tx.send(node.rpc());
143            node.run_forever().await;
144        });
145
146        keep_worker_alive_cursed_hack();
147    });
148
149    rpc_sender_rx.await.unwrap()
150}
151
152async fn setup_node(
153    block_producer: Option<AccountSecretKey>,
154    seed_nodes_urls: Option<Vec<String>>,
155    seed_nodes_addresses: Option<Vec<String>>,
156    genesis_config_url: Option<String>,
157) -> mina_node_common::Node<NodeService> {
158    let block_verifier_index = BlockVerifier::make().await;
159    let work_verifier_index = TransactionVerifier::make().await;
160
161    let genesis_config = if let Some(genesis_config_url) = genesis_config_url {
162        let bytes = ::node::core::http::get_bytes(&genesis_config_url)
163            .await
164            .expect("failed to fetch genesis config");
165        GenesisConfig::Prebuilt(bytes.into()).into()
166    } else {
167        ::node::config::DEVNET_CONFIG.clone()
168    };
169
170    let mut node_builder: NodeBuilder = NodeBuilder::new(None, genesis_config);
171    node_builder
172        .block_verifier_index(block_verifier_index.clone())
173        .work_verifier_index(work_verifier_index.clone());
174
175    // TODO(binier): refactor
176    let mut all_raw_peers = seed_nodes_addresses.unwrap_or_default();
177
178    if let Some(seed_nodes_urls) = seed_nodes_urls {
179        for seed_nodes_url in seed_nodes_urls {
180            let peers = ::node::core::http::get_bytes(&seed_nodes_url).await;
181            match peers {
182                Ok(s) => {
183                    log::info!("Successfully fetched peers from {seed_nodes_url}");
184                    all_raw_peers.extend(String::from_utf8_lossy(&s).split("\n").map(String::from));
185                }
186                Err(e) => {
187                    log::error!("Failed to fetch peers from {seed_nodes_url}: {e}");
188                }
189            }
190        }
191    }
192
193    node_builder.initial_peers(
194        all_raw_peers
195            .iter()
196            .map(|s| s.trim())
197            .filter(|s| !s.is_empty())
198            .flat_map(|s| s.parse().ok())
199            .inspect(|p| log::debug!("Using peer: {p:?}")),
200    );
201
202    if let Some(bp_key) = block_producer {
203        thread::spawn(move || {
204            BlockProver::make(Some(block_verifier_index), Some(work_verifier_index));
205        });
206        node_builder.block_producer(bp_key, None);
207    }
208
209    node_builder
210        .p2p_custom_task_spawner(P2pTaskRemoteSpawner {})
211        .unwrap();
212    node_builder.gather_stats();
213    node_builder.build().context("node build failed!").unwrap()
214}
215
216fn keep_worker_alive_cursed_hack() {
217    wasm_bindgen::throw_str("Cursed hack to keep workers alive. See https://github.com/rustwasm/wasm-bindgen/issues/2945");
218}