miden-client-web 0.14.0

Web Client library that facilitates interaction with the Miden network
use miden_client::account::AccountBuilder as NativeAccountBuilder;
use miden_client::account::component::BasicWallet;
use miden_client::auth::NoAuth;
use wasm_bindgen::prelude::*;

use crate::js_error_with_context;
use crate::models::account::Account;
use crate::models::account_component::AccountComponent;
use crate::models::account_storage_mode::AccountStorageMode;
use crate::models::account_type::AccountType;
use crate::models::word::Word;

#[wasm_bindgen]
pub struct AccountBuilderResult {
    account: Account,
    seed: Word,
}

#[wasm_bindgen]
impl AccountBuilderResult {
    /// Returns the built account.
    #[wasm_bindgen(getter)]
    pub fn account(&self) -> Account {
        self.account.clone()
    }

    /// Returns the seed used to derive the account ID.
    #[wasm_bindgen(getter)]
    pub fn seed(&self) -> Word {
        self.seed.clone()
    }
}

#[wasm_bindgen]
#[derive(Clone)]
pub struct AccountBuilder(NativeAccountBuilder);

#[wasm_bindgen]
impl AccountBuilder {
    /// Creates a new account builder from a 32-byte initial seed.
    #[wasm_bindgen(constructor)]
    pub fn new(init_seed: Vec<u8>) -> Result<AccountBuilder, JsValue> {
        let seed_array: [u8; 32] = init_seed
            .try_into()
            .map_err(|_| JsValue::from_str("Seed must be exactly 32 bytes"))?;
        Ok(AccountBuilder(NativeAccountBuilder::new(seed_array)))
    }

    /// Sets the account type (regular, faucet, etc.).
    ///
    /// Accepts either a numeric WASM enum value (0–3) or a string name
    /// (`"FungibleFaucet"`, `"NonFungibleFaucet"`,
    /// `"RegularAccountImmutableCode"`, `"RegularAccountUpdatableCode"`).
    #[wasm_bindgen(js_name = "accountType")]
    pub fn account_type(mut self, account_type: &JsValue) -> Result<AccountBuilder, JsValue> {
        let at = parse_account_type(account_type)?;
        self.0 = self.0.account_type(at.into());
        Ok(self)
    }

    // TODO: AccountStorageMode as Enum
    /// Sets the storage mode (public/private) for the account.
    #[wasm_bindgen(js_name = "storageMode")]
    pub fn storage_mode(mut self, storage_mode: &AccountStorageMode) -> Self {
        self.0 = self.0.storage_mode(storage_mode.into());
        self
    }

    /// Adds a component to the account.
    #[wasm_bindgen(js_name = "withComponent")]
    pub fn with_component(mut self, account_component: &AccountComponent) -> Self {
        self.0 = self.0.with_component(account_component);
        self
    }

    /// Adds an authentication component to the account.
    #[wasm_bindgen(js_name = "withAuthComponent")]
    pub fn with_auth_component(mut self, account_component: &AccountComponent) -> Self {
        self.0 = self.0.with_auth_component(account_component);
        self
    }

    /// Adds a no-auth component to the account (for public accounts).
    #[wasm_bindgen(js_name = "withNoAuthComponent")]
    pub fn with_no_auth_component(mut self) -> Self {
        self.0 = self.0.with_auth_component(NoAuth);
        self
    }

    #[wasm_bindgen(js_name = "withBasicWalletComponent")]
    pub fn with_basic_wallet_component(mut self) -> Self {
        self.0 = self.0.with_component(BasicWallet);
        self
    }

    /// Builds the account and returns it together with the derived seed.
    pub fn build(self) -> Result<AccountBuilderResult, JsValue> {
        let account = self
            .0
            .build()
            .map_err(|err| js_error_with_context(err, "Failed to build account"))?;
        let seed = account.seed().expect("newly built account should always contain a seed");
        Ok(AccountBuilderResult {
            account: account.into(),
            seed: seed.into(),
        })
    }
}

/// Parses a `JsValue` into an `AccountType`, accepting either a numeric enum
/// value (0–3) or a string variant name.
fn parse_account_type(value: &JsValue) -> Result<AccountType, JsValue> {
    if let Some(n) = value.as_f64() {
        // Values 0–3 are exactly representable as f64; direct comparison is safe.
        match account_type_from_f64(n) {
            Some(at) => Ok(at),
            None => Err(JsValue::from_str(&format!(
                "Unknown account type: {n}. Expected 0 (FungibleFaucet), \
                 1 (NonFungibleFaucet), 2 (RegularAccountImmutableCode), \
                 or 3 (RegularAccountUpdatableCode)"
            ))),
        }
    } else if let Some(s) = value.as_string() {
        match s.as_str() {
            "FungibleFaucet" => Ok(AccountType::FungibleFaucet),
            "NonFungibleFaucet" => Ok(AccountType::NonFungibleFaucet),
            "RegularAccountImmutableCode" => Ok(AccountType::RegularAccountImmutableCode),
            "RegularAccountUpdatableCode" => Ok(AccountType::RegularAccountUpdatableCode),
            _ => Err(JsValue::from_str(&format!(
                "Unknown account type: \"{s}\". Expected \"FungibleFaucet\", \
                 \"NonFungibleFaucet\", \"RegularAccountImmutableCode\", \
                 or \"RegularAccountUpdatableCode\""
            ))),
        }
    } else {
        Err(JsValue::from_str("accountType must be a number (0–3) or a string variant name"))
    }
}

/// Maps a JS number (f64) to an `AccountType` variant. Returns `None` for
/// unrecognised values. Small integers 0–3 are exactly representable in f64,
/// so direct equality is correct here.
#[allow(clippy::float_cmp)]
fn account_type_from_f64(n: f64) -> Option<AccountType> {
    if n == 0.0 {
        Some(AccountType::FungibleFaucet)
    } else if n == 1.0 {
        Some(AccountType::NonFungibleFaucet)
    } else if n == 2.0 {
        Some(AccountType::RegularAccountImmutableCode)
    } else if n == 3.0 {
        Some(AccountType::RegularAccountUpdatableCode)
    } else {
        None
    }
}