extern crate alloc;
use alloc::sync::Arc;
use core::error::Error;
use core::fmt::Write;
use idxdb_store::IdxdbStore;
use js_sys::{Function, Reflect};
use miden_client::builder::{ClientBuilder, DEFAULT_GRPC_TIMEOUT_MS};
use miden_client::crypto::RandomCoin;
use miden_client::note_transport::NoteTransportClient;
use miden_client::note_transport::grpc::GrpcNoteTransportClient;
use miden_client::rpc::{Endpoint, GrpcClient, NodeRpcClient};
use miden_client::store::Store;
use miden_client::testing::mock::MockRpcApi;
use miden_client::testing::note_transport::MockNoteTransportApi;
use miden_client::{Client, ClientError, DebugMode, ErrorHint, Felt};
use models::code_builder::CodeBuilder;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use wasm_bindgen::prelude::*;
pub mod account;
pub mod export;
pub mod helpers;
pub mod import;
#[macro_use]
pub(crate) mod miden_array;
pub mod mock;
pub mod models;
pub mod new_account;
pub mod new_transactions;
pub mod note_transport;
pub mod notes;
pub mod rpc_client;
pub mod settings;
pub mod sync;
pub mod tags;
pub mod transactions;
pub mod utils;
pub mod keystore_api;
mod web_keystore;
mod web_keystore_callbacks;
mod web_keystore_db;
use tracing::Level;
use tracing_subscriber::layer::SubscriberExt;
pub use web_keystore::WebKeyStore;
pub(crate) type ClientAuth = WebKeyStore<RandomCoin>;
const BASE_STORE_NAME: &str = "MidenClientDB";
#[wasm_bindgen(js_name = "setupLogging")]
pub fn setup_logging(log_level: &str) {
let level = match log_level.to_lowercase().as_str() {
"error" => Some(Level::ERROR),
"warn" => Some(Level::WARN),
"info" => Some(Level::INFO),
"debug" => Some(Level::DEBUG),
"trace" => Some(Level::TRACE),
_ => None,
};
if let Some(level) = level {
let config = tracing_wasm::WASMLayerConfigBuilder::new().set_max_level(level).build();
let _ = tracing::subscriber::set_global_default(
tracing_subscriber::registry().with(tracing_wasm::WASMLayer::new(config)),
);
}
}
#[wasm_bindgen]
pub struct WebClient {
inner: Option<Client<ClientAuth>>,
mock_rpc_api: Option<Arc<MockRpcApi>>,
mock_note_transport_api: Option<Arc<MockNoteTransportApi>>,
}
impl Default for WebClient {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen]
impl WebClient {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
console_error_panic_hook::set_once();
WebClient {
inner: None,
mock_rpc_api: None,
mock_note_transport_api: None,
}
}
#[wasm_bindgen(js_name = "storeIdentifier")]
pub fn store_identifier(&self) -> Result<String, JsValue> {
Ok(self.get_inner()?.store_identifier().to_string())
}
pub(crate) fn get_inner(&self) -> Result<&Client<ClientAuth>, JsValue> {
self.inner.as_ref().ok_or_else(|| JsValue::from_str("Client not initialized"))
}
pub(crate) fn get_mut_inner(&mut self) -> Option<&mut Client<ClientAuth>> {
self.inner.as_mut()
}
pub(crate) fn inner_keystore(&self) -> Result<&Arc<ClientAuth>, JsValue> {
self.inner
.as_ref()
.and_then(|c| c.authenticator())
.ok_or_else(|| JsValue::from_str("Client not initialized"))
}
#[wasm_bindgen(getter)]
pub fn keystore(&self) -> Result<keystore_api::WebKeystoreApi, JsValue> {
let ks = self.inner_keystore()?.clone();
Ok(keystore_api::WebKeystoreApi::new(ks))
}
#[wasm_bindgen(js_name = "createClient")]
pub async fn create_client(
&mut self,
node_url: Option<String>,
node_note_transport_url: Option<String>,
seed: Option<Vec<u8>>,
store_name: Option<String>,
debug_mode: Option<bool>,
) -> Result<JsValue, JsValue> {
let endpoint = node_url.map_or(Ok(Endpoint::testnet()), |url| {
Endpoint::try_from(url.as_str()).map_err(|_| JsValue::from_str("Invalid node URL"))
})?;
let web_rpc_client = Arc::new(GrpcClient::new(&endpoint, DEFAULT_GRPC_TIMEOUT_MS));
let note_transport_client = node_note_transport_url.map(|url| {
Arc::new(GrpcNoteTransportClient::new(url, DEFAULT_GRPC_TIMEOUT_MS))
as Arc<dyn NoteTransportClient>
});
let store_name =
store_name.unwrap_or(format!("{}_{}", BASE_STORE_NAME, endpoint.to_network_id()));
let store = Arc::new(
IdxdbStore::new(store_name.clone())
.await
.map_err(|_| JsValue::from_str("Failed to initialize IdxdbStore"))?,
);
let rng = create_rng(seed)?;
let keystore =
Arc::new(WebKeyStore::new_with_callbacks(rng, store_name.clone(), None, None, None));
self.setup_client(
web_rpc_client,
store,
keystore,
rng,
note_transport_client,
debug_mode.unwrap_or(false),
)
.await?;
Ok(JsValue::from_str("Client created successfully"))
}
#[wasm_bindgen(js_name = "createClientWithExternalKeystore")]
#[allow(clippy::too_many_arguments)]
pub async fn create_client_with_external_keystore(
&mut self,
node_url: Option<String>,
node_note_transport_url: Option<String>,
seed: Option<Vec<u8>>,
store_name: Option<String>,
get_key_cb: Option<Function>,
insert_key_cb: Option<Function>,
sign_cb: Option<Function>,
debug_mode: Option<bool>,
) -> Result<JsValue, JsValue> {
let endpoint = node_url.map_or(Ok(Endpoint::testnet()), |url| {
Endpoint::try_from(url.as_str()).map_err(|_| JsValue::from_str("Invalid node URL"))
})?;
let web_rpc_client = Arc::new(GrpcClient::new(&endpoint, DEFAULT_GRPC_TIMEOUT_MS));
let note_transport_client = node_note_transport_url.map(|url| {
Arc::new(GrpcNoteTransportClient::new(url, DEFAULT_GRPC_TIMEOUT_MS))
as Arc<dyn NoteTransportClient>
});
let store_name =
store_name.unwrap_or(format!("{}_{}", BASE_STORE_NAME, endpoint.to_network_id()));
let store = Arc::new(
IdxdbStore::new(store_name.clone())
.await
.map_err(|_| JsValue::from_str("Failed to initialize IdxdbStore"))?,
);
let rng = create_rng(seed)?;
let keystore = Arc::new(WebKeyStore::new_with_callbacks(
rng,
store_name.clone(),
get_key_cb,
insert_key_cb,
sign_cb,
));
self.setup_client(
web_rpc_client,
store,
keystore,
rng,
note_transport_client,
debug_mode.unwrap_or(false),
)
.await?;
Ok(JsValue::from_str("Client created successfully"))
}
async fn setup_client(
&mut self,
rpc_client: Arc<dyn NodeRpcClient>,
store: Arc<dyn Store>,
keystore: Arc<ClientAuth>,
rng: RandomCoin,
note_transport_client: Option<Arc<dyn NoteTransportClient>>,
debug_mode: bool,
) -> Result<(), JsValue> {
let mut builder = ClientBuilder::new()
.rpc(rpc_client)
.rng(Box::new(rng))
.store(store)
.authenticator(keystore)
.in_debug_mode(if debug_mode {
DebugMode::Enabled
} else {
DebugMode::Disabled
});
if let Some(transport) = note_transport_client {
builder = builder.note_transport(transport);
}
let mut client = builder
.build()
.await
.map_err(|err| js_error_with_context(err, "Failed to create client"))?;
client
.ensure_genesis_in_place()
.await
.map_err(|err| js_error_with_context(err, "Failed to ensure genesis in place"))?;
self.inner = Some(client);
Ok(())
}
#[wasm_bindgen(js_name = "createCodeBuilder")]
pub fn create_code_builder(&self) -> Result<CodeBuilder, JsValue> {
let Some(client) = &self.inner else {
return Err("client was not initialized before instancing CodeBuilder".into());
};
Ok(CodeBuilder::from_source_manager(client.code_builder().source_manager().clone()))
}
}
pub(crate) fn create_rng(seed: Option<Vec<u8>>) -> Result<RandomCoin, JsValue> {
let mut rng = match seed {
Some(seed_bytes) => {
if seed_bytes.len() == 32 {
let mut seed_array = [0u8; 32];
seed_array.copy_from_slice(&seed_bytes);
StdRng::from_seed(seed_array)
} else {
return Err(JsValue::from_str("Seed must be exactly 32 bytes"));
}
},
None => StdRng::from_os_rng(),
};
let coin_seed: [u64; 4] = rng.random();
Ok(RandomCoin::new(coin_seed.map(Felt::new).into()))
}
fn js_error_with_context<T>(err: T, context: &str) -> JsValue
where
T: Error + 'static,
{
let mut error_string = context.to_string();
let mut source = Some(&err as &dyn Error);
while let Some(err) = source {
write!(error_string, ": {err}").expect("writing to string should always succeed");
source = err.source();
}
let help = hint_from_error(&err);
let js_error: JsValue = JsError::new(&error_string).into();
if let Some(help) = help {
let _ = Reflect::set(&js_error, &JsValue::from_str("help"), &JsValue::from_str(&help));
}
js_error
}
fn hint_from_error(err: &(dyn Error + 'static)) -> Option<String> {
if let Some(client_error) = err.downcast_ref::<ClientError>() {
return Option::<ErrorHint>::from(client_error).map(ErrorHint::into_help_message);
}
err.source().and_then(hint_from_error)
}