diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 45bba65d2..4f128df31 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,6 +17,19 @@ jobs: rustup +nightly component add clippy - name: make - clippy run: make clippy + + clippy-wasm: + name: Clippy WASM + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@main + - name: Install Rust with clippy + run: | + rustup update --no-self-update nightly + rustup target add wasm32-unknown-unknown --toolchain nightly + rustup +nightly component add clippy + - name: make - clippy-wasm + run: make clippy-wasm rustfmt: name: rustfmt diff --git a/.gitignore b/.gitignore index 8e803b34c..2aa8c3288 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ debug/ target/ miden-node/ +dist/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html diff --git a/CHANGELOG.md b/CHANGELOG.md index a854fdebf..55863e3d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## v0.5.0 (TBD) +* Added the Web Client Crate * [BREAKING] Refactored `Client` to merge submit_transaction and prove_transaction (#445) * Tracked token symbols with config file (#441). * [BREAKING] Refactored `TransactionRequest` to represent a generalized transaction (#438). diff --git a/Cargo.toml b/Cargo.toml index b524003ae..b0208d3ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ resolver = "2" members = [ "bin/miden-cli", "crates/rust-client", + "crates/web-client", "tests" ] @@ -15,6 +16,11 @@ authors = ["miden contributors"] repository = "https://github.com/0xPolygonMiden/miden-client" [workspace.dependencies] +miden-lib = { default-features = false, git = "https://github.com/0xPolygonMiden/miden-base", branch = "next"} +miden-objects = { default-features = false, features = ["serde"], git = "https://github.com/0xPolygonMiden/miden-base", branch = "next" } +miden-tx = { default-features = false , git = "https://github.com/0xPolygonMiden/miden-base", branch = "next" } rand = { version = "0.8" } +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0", features = ["raw_value"] } tokio = { version = "1.38", features = ["rt-multi-thread", "net", "macros"] } tracing = { version = "0.1" } diff --git a/Makefile b/Makefile index c207871d9..f71394a6d 100644 --- a/Makefile +++ b/Makefile @@ -6,21 +6,30 @@ help: ## Show description of all commands # --- Variables ----------------------------------------------------------------------------------- -FEATURES_CLIENT="testing, concurrent" -FEATURES_CLI="testing, concurrent" -NODE_FEATURES_TESTING="testing" +FEATURES_WEB_CLIENT=--features "testing" +FEATURES_CLIENT=--features "testing, concurrent" +FEATURES_CLI=--features "testing, concurrent" +NODE_FEATURES_TESTING=--features "testing" WARNINGS=RUSTDOCFLAGS="-D warnings" NODE_BRANCH="next" # --- Linting ------------------------------------------------------------------------------------- .PHONY: clippy - clippy: ## Run Clippy with configs - cargo +nightly clippy --workspace --all-targets --features $(FEATURES_CLI) -- -D warnings + clippy: ## Runs Clippy with configs + cargo +nightly clippy --workspace --exclude miden-client-web --all-targets $(FEATURES_CLI) -- -D warnings + +.PHONY: clippy-wasm + clippy-wasm: ## Runs Clippy for the miden-client-web package + cargo +nightly clippy --package miden-client-web --target wasm32-unknown-unknown --all-targets $(FEATURES_WEB_CLIENT) -- -D warnings .PHONY: fix -fix: ## Run Fix with configs - cargo +nightly fix --allow-staged --allow-dirty --all-targets --features $(FEATURES_CLI) +fix: ## Runs Fix with configs + cargo +nightly fix --workspace --exclude miden-client-web --allow-staged --allow-dirty --all-targets $(FEATURES_CLI) + +.PHONY: fix-wasm +fix-wasm: ## Runs Fix for the miden-client-web package + cargo +nightly fix --package miden-client-web --target wasm32-unknown-unknown --allow-staged --allow-dirty --all-targets $(FEATURES_WEB_CLIENT) .PHONY: format format: ## Run format using nightly toolchain @@ -31,7 +40,7 @@ format-check: ## Run format using nightly toolchain but only in check mode cargo +nightly fmt --all --check .PHONY: lint -lint: format fix clippy ## Run all linting tasks at once (clippy, fixing, formatting) +lint: format fix clippy fix-wasm clippy-wasm ## Runs all linting tasks at once (clippy, fixing, formatting) # --- Documentation site -------------------------------------------------------------------------- @@ -52,14 +61,14 @@ doc-serve: doc-deps ## Serve documentation site .PHONY: doc doc: ## Generate & check rust documentation. You'll need `jq` in order for this to run. @cd crates/rust-client && \ - FEATURES=$$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "miden-client") | .features | keys[] | select(. != "web-tonic")' | tr '\n' ',') && \ + FEATURES=$$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "miden-client") | .features | keys[] | select(. != "web-tonic" and . != "idxdb")' | tr '\n' ',') && \ RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --features "$$FEATURES" --keep-going --release # --- Testing ------------------------------------------------------------------------------------- .PHONY: test test: ## Run tests - cargo nextest run --release --lib --features $(FEATURES_CLIENT) + cargo nextest run --workspace --exclude miden-client-web --release --lib $(FEATURES_CLIENT) .PHONY: test-deps test-deps: ## Install dependencies for tests @@ -69,12 +78,12 @@ test-deps: ## Install dependencies for tests .PHONY: integration-test integration-test: ## Run integration tests - cargo nextest run --release --test=integration --features $(FEATURES_CLI) --no-default-features + cargo nextest run --workspace --exclude miden-client-web --release --test=integration $(FEATURES_CLI) --no-default-features .PHONY: integration-test-full integration-test-full: ## Run the integration test binary with ignored tests included - cargo nextest run --release --test=integration --features $(FEATURES_CLI) - cargo nextest run --release --test=integration --features $(FEATURES_CLI) --run-ignored ignored-only -- test_import_genesis_accounts_can_be_used_for_transactions + cargo nextest run --workspace --exclude miden-client-web --release --test=integration $(FEATURES_CLI) + cargo nextest run --workspace --exclude miden-client-web --release --test=integration $(FEATURES_CLI) --run-ignored ignored-only -- test_import_genesis_accounts_can_be_used_for_transactions .PHONY: kill-node kill-node: ## Kill node process @@ -89,31 +98,31 @@ node: ## Setup node directory if [ -d miden-node ]; then cd miden-node ; else git clone https://github.com/0xPolygonMiden/miden-node.git && cd miden-node; fi cd miden-node && git checkout $(NODE_BRANCH) && git pull origin $(NODE_BRANCH) && cargo update cd miden-node && rm -rf miden-store.sqlite3* - cd miden-node && cargo run --bin miden-node --features $(NODE_FEATURES_TESTING) -- make-genesis --inputs-path ../tests/config/genesis.toml --force + cd miden-node && cargo run --bin miden-node $(NODE_FEATURES_TESTING) -- make-genesis --inputs-path ../tests/config/genesis.toml --force .PHONY: start-node start-node: ## Run node. This requires the node repo to be present at `miden-node` - cd miden-node && cargo run --bin miden-node --features $(NODE_FEATURES_TESTING) -- start --config ../tests/config/miden-node.toml node + cd miden-node && cargo run --bin miden-node $(NODE_FEATURES_TESTING) -- start --config ../tests/config/miden-node.toml node # --- Installing ---------------------------------------------------------------------------------- -install: ## Install the CLI binary - cargo install --features $(FEATURES_CLI) --path bin/miden-cli +install: ## Installs the CLI binary + cargo install $(FEATURES_CLI) --path bin/miden-cli # --- Building ------------------------------------------------------------------------------------ -build: ## Build the CLI binary and client library in release mode - cargo build --release --features $(FEATURES_CLI) +build: ## Builds the CLI binary and client library in release mode + cargo build --workspace --exclude miden-client-web --release $(FEATURES_CLI) -build-wasm: ## Build the client library for wasm32 - cargo build --target wasm32-unknown-unknown --features idxdb,web-tonic --no-default-features --package miden-client +build-wasm: ## Builds the client library for wasm32 + cargo build --package miden-client-web --target wasm32-unknown-unknown $(FEATURES_WEB_CLIENT) # --- Check --------------------------------------------------------------------------------------- .PHONY: check -check: ## Check CLI and std client for errors without code generation - cargo check --release --features $(FEATURES_CLI) +check: ## Builds the CLI binary and client library in release mode + cargo check --workspace --exclude miden-client-web --release $(FEATURES_CLI) .PHONY: check-wasm -check-wasm: ## Check WASM client for errors without code generation - cargo check --target wasm32-unknown-unknown --features idxdb,web-tonic --no-default-features --package miden-client +check-wasm: ## Builds the client library for wasm32 + cargo check --package miden-client-web --target wasm32-unknown-unknown $(FEATURES_WEB_CLIENT) diff --git a/bin/miden-cli/src/commands/new_transactions.rs b/bin/miden-cli/src/commands/new_transactions.rs index 2a30d6896..57263c58e 100644 --- a/bin/miden-cli/src/commands/new_transactions.rs +++ b/bin/miden-cli/src/commands/new_transactions.rs @@ -6,10 +6,11 @@ use miden_client::{ assets::{Asset, FungibleAsset}, auth::TransactionAuthenticator, crypto::{Digest, FeltRng}, - notes::{NoteId, NoteType as MidenNoteType}, + notes::{get_input_note_with_id_prefix, NoteId, NoteType as MidenNoteType}, rpc::NodeRpcClient, store::Store, transactions::{ + build_swap_tag, request::{PaymentTransactionData, SwapTransactionData, TransactionTemplate}, TransactionResult, }, @@ -18,11 +19,8 @@ use miden_client::{ use tracing::info; use crate::{ - create_dynamic_table, get_input_note_with_id_prefix, - utils::{ - build_swap_tag, get_input_acc_id_by_prefix_or_default, parse_account_id, - parse_fungible_asset, - }, + create_dynamic_table, + utils::{get_input_acc_id_by_prefix_or_default, parse_account_id, parse_fungible_asset}, }; #[derive(Debug, Clone, Copy, ValueEnum)] diff --git a/bin/miden-cli/src/commands/notes.rs b/bin/miden-cli/src/commands/notes.rs index f8671716e..c91f26a34 100644 --- a/bin/miden-cli/src/commands/notes.rs +++ b/bin/miden-cli/src/commands/notes.rs @@ -5,16 +5,14 @@ use miden_client::{ assets::Asset, auth::TransactionAuthenticator, crypto::{Digest, FeltRng}, - notes::{NoteConsumability, NoteInputs, NoteMetadata}, + notes::{get_input_note_with_id_prefix, NoteConsumability, NoteInputs, NoteMetadata}, rpc::NodeRpcClient, store::{InputNoteRecord, NoteFilter as ClientNoteFilter, OutputNoteRecord, Store}, transactions::known_script_roots::{P2ID, P2IDR, SWAP}, Client, ClientError, IdPrefixFetchError, }; -use crate::{ - create_dynamic_table, get_input_note_with_id_prefix, get_output_note_with_id_prefix, Parser, -}; +use crate::{create_dynamic_table, get_output_note_with_id_prefix, Parser}; #[derive(Clone, Debug, ValueEnum)] pub enum NoteFilter { diff --git a/bin/miden-cli/src/lib.rs b/bin/miden-cli/src/lib.rs index db3aa57a5..8f8bf7c64 100644 --- a/bin/miden-cli/src/lib.rs +++ b/bin/miden-cli/src/lib.rs @@ -7,10 +7,7 @@ use miden_client::{ auth::{StoreAuthenticator, TransactionAuthenticator}, crypto::{FeltRng, RpoRandomCoin}, rpc::{NodeRpcClient, TonicRpcClient}, - store::{ - sqlite_store::SqliteStore, InputNoteRecord, NoteFilter as ClientNoteFilter, - OutputNoteRecord, Store, - }, + store::{sqlite_store::SqliteStore, NoteFilter as ClientNoteFilter, OutputNoteRecord, Store}, Client, ClientError, Felt, IdPrefixFetchError, }; use rand::Rng; @@ -155,58 +152,6 @@ pub fn create_dynamic_table(headers: &[&str]) -> Table { table } -/// Returns the client input note whose ID starts with `note_id_prefix` -/// -/// # Errors -/// -/// - Returns [IdPrefixFetchError::NoMatch] if we were unable to find any note where -/// `note_id_prefix` is a prefix of its id. -/// - Returns [IdPrefixFetchError::MultipleMatches] if there were more than one note found -/// where `note_id_prefix` is a prefix of its id. -pub(crate) fn get_input_note_with_id_prefix< - N: NodeRpcClient, - R: FeltRng, - S: Store, - A: TransactionAuthenticator, ->( - client: &Client, - note_id_prefix: &str, -) -> Result { - let mut input_note_records = client - .get_input_notes(ClientNoteFilter::All) - .map_err(|err| { - tracing::error!("Error when fetching all notes from the store: {err}"); - IdPrefixFetchError::NoMatch(format!("note ID prefix {note_id_prefix}").to_string()) - })? - .into_iter() - .filter(|note_record| note_record.id().to_hex().starts_with(note_id_prefix)) - .collect::>(); - - if input_note_records.is_empty() { - return Err(IdPrefixFetchError::NoMatch( - format!("note ID prefix {note_id_prefix}").to_string(), - )); - } - if input_note_records.len() > 1 { - let input_note_record_ids = input_note_records - .iter() - .map(|input_note_record| input_note_record.id()) - .collect::>(); - tracing::error!( - "Multiple notes found for the prefix {}: {:?}", - note_id_prefix, - input_note_record_ids - ); - return Err(IdPrefixFetchError::MultipleMatches( - format!("note ID prefix {note_id_prefix}").to_string(), - )); - } - - Ok(input_note_records - .pop() - .expect("input_note_records should always have one element")) -} - /// Returns the client output note whose ID starts with `note_id_prefix` /// /// # Errors diff --git a/bin/miden-cli/src/utils.rs b/bin/miden-cli/src/utils.rs index 1d9bc09d4..5d8ac8b86 100644 --- a/bin/miden-cli/src/utils.rs +++ b/bin/miden-cli/src/utils.rs @@ -9,13 +9,8 @@ use figment::{ Figment, }; use miden_client::{ - accounts::AccountId, - auth::TransactionAuthenticator, - crypto::FeltRng, - notes::{NoteError, NoteExecutionHint, NoteTag, NoteType}, - rpc::NodeRpcClient, - store::Store, - Client, + accounts::AccountId, auth::TransactionAuthenticator, crypto::FeltRng, rpc::NodeRpcClient, + store::Store, Client, }; use tracing::info; @@ -139,45 +134,6 @@ pub fn parse_fungible_asset(arg: &str) -> Result<(u64, AccountId), String> { Ok((amount, faucet_id)) } -/// Returns a note tag for a swap note with the specified parameters. -/// -/// Use case ID for the returned tag is set to 0. -/// -/// Tag payload is constructed by taking asset tags (8 bits of faucet ID) and concatenating them -/// together as offered_asset_tag + requested_asset tag. -/// -/// Network execution hint for the returned tag is set to `Local`. -/// -/// Based on miden-base's implementation () -/// -/// TODO: we should make the function in base public and once that gets released use that one and -/// delete this implementation. -pub fn build_swap_tag( - note_type: NoteType, - offered_asset_faucet_id: AccountId, - requested_asset_faucet_id: AccountId, -) -> Result { - const SWAP_USE_CASE_ID: u16 = 0; - - // get bits 4..12 from faucet IDs of both assets, these bits will form the tag payload; the - // reason we skip the 4 most significant bits is that these encode metadata of underlying - // faucets and are likely to be the same for many different faucets. - - let offered_asset_id: u64 = offered_asset_faucet_id.into(); - let offered_asset_tag = (offered_asset_id >> 52) as u8; - - let requested_asset_id: u64 = requested_asset_faucet_id.into(); - let requested_asset_tag = (requested_asset_id >> 52) as u8; - - let payload = ((offered_asset_tag as u16) << 8) | (requested_asset_tag as u16); - - let execution = NoteExecutionHint::Local; - match note_type { - NoteType::Public => NoteTag::for_public_use_case(SWAP_USE_CASE_ID, payload, execution), - _ => NoteTag::for_local_use_case(SWAP_USE_CASE_ID, payload), - } -} - /// Returns the token symbol map from the config file. pub fn load_token_map() -> Result { let (config, _) = load_config_file()?; diff --git a/crates/rust-client/Cargo.toml b/crates/rust-client/Cargo.toml index a2f6c7f98..e43e9d075 100644 --- a/crates/rust-client/Cargo.toml +++ b/crates/rust-client/Cargo.toml @@ -32,15 +32,15 @@ chrono = { version = "0.4", optional = false } getrandom = { version = "0.2", features = ["js"], optional = true } hex = { version = "0.4" , optional = true} lazy_static = { version = "1.5", optional = true } -miden-lib = { default-features = false, git = "https://github.com/0xPolygonMiden/miden-base", branch = "next"} -miden-objects = { default-features = false, features = ["serde"], git = "https://github.com/0xPolygonMiden/miden-base", branch = "next" } -miden-tx = { default-features = false , git = "https://github.com/0xPolygonMiden/miden-base", branch = "next" } +miden-lib = { workspace = true } +miden-objects = { workspace = true } +miden-tx = { workspace = true } prost = { version = "0.12", optional = true, default-features = false, features = ["derive"] } rand = { workspace = true } rusqlite = { version = "0.31", features = ["vtab", "array", "bundled"], optional = true } rusqlite_migration = { version = "1.0", optional = true } -serde = { version = "1.0", features = ["derive"] } -serde_json = { version = "1.0", features = ["raw_value"] } +serde = { workspace = true } +serde_json = { workspace = true } serde-wasm-bindgen = { version = "0.6", optional = true } thiserror = { version = "1.0", optional = true } tokio = { workspace = true , optional = true } diff --git a/crates/rust-client/src/lib.rs b/crates/rust-client/src/lib.rs index f1120cc5a..24775ccb4 100644 --- a/crates/rust-client/src/lib.rs +++ b/crates/rust-client/src/lib.rs @@ -72,9 +72,7 @@ pub mod testing { pub use miden_objects::accounts::account_id::testing::*; } -use alloc::rc::Rc; -#[cfg(feature = "testing")] -use alloc::vec::Vec; +use alloc::{rc::Rc, vec::Vec}; use miden_objects::crypto::rand::FeltRng; use miden_tx::{auth::TransactionAuthenticator, TransactionExecutor}; @@ -150,7 +148,10 @@ impl Client &mut self.rpc_api } - #[cfg(any(test, feature = "testing"))] + // TODO: the idxdb feature access here is temporary and should be removed in the future once + // a good solution to the syncrhonous store access in the store authenticator is found. + // https://github.com/0xPolygonMiden/miden-base/issues/705 + #[cfg(any(test, feature = "testing", feature = "idxdb"))] pub fn store(&mut self) -> &S { &self.store } diff --git a/crates/rust-client/src/notes/mod.rs b/crates/rust-client/src/notes/mod.rs index d7a2e6955..470cf74ec 100644 --- a/crates/rust-client/src/notes/mod.rs +++ b/crates/rust-client/src/notes/mod.rs @@ -8,7 +8,7 @@ use winter_maybe_async::{maybe_async, maybe_await}; use crate::{ rpc::{NodeRpcClient, NoteDetails}, store::{InputNoteRecord, NoteFilter, NoteStatus, OutputNoteRecord, Store, StoreError}, - Client, ClientError, + Client, ClientError, IdPrefixFetchError, }; mod note_screener; @@ -296,3 +296,55 @@ impl Client .map_err(ClientError::TransactionExecutorError) } } + +/// Returns the client input note whose ID starts with `note_id_prefix` +/// +/// # Errors +/// +/// - Returns [IdPrefixFetchError::NoMatch] if we were unable to find any note where +/// `note_id_prefix` is a prefix of its id. +/// - Returns [IdPrefixFetchError::MultipleMatches] if there were more than one note found +/// where `note_id_prefix` is a prefix of its id. +#[maybe_async] +pub fn get_input_note_with_id_prefix< + N: NodeRpcClient, + R: FeltRng, + S: Store, + A: TransactionAuthenticator, +>( + client: &Client, + note_id_prefix: &str, +) -> Result { + let mut input_note_records = maybe_await!(client.get_input_notes(NoteFilter::All)) + .map_err(|err| { + tracing::error!("Error when fetching all notes from the store: {err}"); + IdPrefixFetchError::NoMatch(format!("note ID prefix {note_id_prefix}").to_string()) + })? + .into_iter() + .filter(|note_record| note_record.id().to_hex().starts_with(note_id_prefix)) + .collect::>(); + + if input_note_records.is_empty() { + return Err(IdPrefixFetchError::NoMatch( + format!("note ID prefix {note_id_prefix}").to_string(), + )); + } + if input_note_records.len() > 1 { + let input_note_record_ids = input_note_records + .iter() + .map(|input_note_record| input_note_record.id()) + .collect::>(); + tracing::error!( + "Multiple notes found for the prefix {}: {:?}", + note_id_prefix, + input_note_record_ids + ); + return Err(IdPrefixFetchError::MultipleMatches( + format!("note ID prefix {note_id_prefix}").to_string(), + )); + } + + Ok(input_note_records + .pop() + .expect("input_note_records should always have one element")) +} diff --git a/crates/rust-client/src/rpc/mod.rs b/crates/rust-client/src/rpc/mod.rs index 59c6aa4f7..e51a27b30 100644 --- a/crates/rust-client/src/rpc/mod.rs +++ b/crates/rust-client/src/rpc/mod.rs @@ -14,6 +14,9 @@ use miden_objects::{ BlockHeader, Digest, }; +#[cfg(all(feature = "tonic", feature = "web-tonic"))] +compile_error!("features `tonic` and `web-tonic` are mutually exclusive"); + #[cfg(any(feature = "tonic", feature = "web-tonic"))] mod domain; diff --git a/crates/rust-client/src/store/mod.rs b/crates/rust-client/src/store/mod.rs index 6f8d2b82e..e6859dca5 100644 --- a/crates/rust-client/src/store/mod.rs +++ b/crates/rust-client/src/store/mod.rs @@ -18,6 +18,9 @@ pub mod data_store; mod errors; pub use errors::*; +#[cfg(all(feature = "sqlite", feature = "idxdb"))] +compile_error!("features `sqlite` and `idxdb` are mutually exclusive"); + #[cfg(feature = "sqlite")] pub mod sqlite_store; diff --git a/crates/rust-client/src/store/web_store/accounts/mod.rs b/crates/rust-client/src/store/web_store/accounts/mod.rs index e6cde47c7..e1ef8cf82 100644 --- a/crates/rust-client/src/store/web_store/accounts/mod.rs +++ b/crates/rust-client/src/store/web_store/accounts/mod.rs @@ -188,10 +188,7 @@ impl WebStore { } /// Returns an [AuthSecretKey] by a public key represented by a [Word] - pub(crate) fn get_account_auth_by_pub_key( - &self, - pub_key: Word, - ) -> Result { + pub fn get_account_auth_by_pub_key(&self, pub_key: Word) -> Result { let pub_key_bytes = pub_key.to_bytes(); let js_value = idxdb_get_account_auth_by_pub_key(pub_key_bytes); @@ -205,8 +202,7 @@ impl WebStore { /// Fetches an [AuthSecretKey] by a public key represented by a [Word] and caches it in the store. /// This is used in the web_client so adding this to ignore the dead code warning. - #[allow(dead_code)] - pub(crate) async fn fetch_and_cache_account_auth_by_pub_key( + pub async fn fetch_and_cache_account_auth_by_pub_key( &self, account_id: String, ) -> Result { diff --git a/crates/rust-client/src/store/web_store/accounts/models.rs b/crates/rust-client/src/store/web_store/accounts/models.rs index aa871e21e..3dd918f95 100644 --- a/crates/rust-client/src/store/web_store/accounts/models.rs +++ b/crates/rust-client/src/store/web_store/accounts/models.rs @@ -1,6 +1,6 @@ use alloc::{string::String, vec::Vec}; -use base64::decode as base64_decode; +use base64::{engine::general_purpose, Engine as _}; use serde::{de::Error, Deserialize, Deserializer, Serialize}; #[derive(Serialize, Deserialize)] @@ -47,7 +47,9 @@ where D: Deserializer<'de>, { let base64_str: String = Deserialize::deserialize(deserializer)?; - base64_decode(&base64_str).map_err(|e| Error::custom(format!("Base64 decode error: {}", e))) + general_purpose::STANDARD + .decode(&base64_str) + .map_err(|e| Error::custom(format!("Base64 decode error: {}", e))) } fn base64_to_vec_u8_optional<'de, D>(deserializer: D) -> Result>, D::Error> @@ -56,7 +58,8 @@ where { let base64_str: Option = Option::deserialize(deserializer)?; match base64_str { - Some(str) => base64_decode(&str) + Some(str) => general_purpose::STANDARD + .decode(&str) .map(Some) .map_err(|e| Error::custom(format!("Base64 decode error: {}", e))), None => Ok(None), diff --git a/crates/rust-client/src/store/web_store/js/chainData.js b/crates/rust-client/src/store/web_store/js/chainData.js index a567c6e1e..fa992b410 100644 --- a/crates/rust-client/src/store/web_store/js/chainData.js +++ b/crates/rust-client/src/store/web_store/js/chainData.js @@ -15,7 +15,7 @@ export async function insertBlockHeader( blockNum: blockNum, header: header, chainMmrPeaks: chainMmrPeaks, - hasClientNotes: hasClientNotes + hasClientNotes: hasClientNotes.toString() }; const existingBlockHeader = await blockHeaders.get(blockNum); @@ -23,7 +23,15 @@ export async function insertBlockHeader( if (!existingBlockHeader) { await blockHeaders.add(data); } else { - console.log("Block header already exists, ignoring."); + console.log("Block header already exists, checking for update."); + + // Update the hasClientNotes if the existing value is false + if (existingBlockHeader.hasClientNotes === 'false' && hasClientNotes) { + await blockHeaders.update(blockNum, { hasClientNotes: hasClientNotes.toString() }); + console.log("Updated hasClientNotes to true."); + } else { + console.log("No update needed for hasClientNotes."); + } } } catch (err) { console.error("Failed to insert block header: ", err); @@ -65,7 +73,7 @@ export async function getBlockHeaders( block_num: results[index].blockNum, header: results[index].header, chain_mmr: results[index].chainMmrPeaks, - has_client_notes: results[index].hasClientNotes + has_client_notes: results[index].hasClientNotes === "true" } } }); @@ -82,9 +90,17 @@ export async function getTrackedBlockHeaders() { // Fetch all records matching the given root const allMatchingRecords = await blockHeaders .where('hasClientNotes') - .equals(true) + .equals("true") .toArray(); - return allMatchingRecords; + // Convert hasClientNotes from string to boolean + const processedRecords = allMatchingRecords.map(record => ({ + block_num: record.blockNum, + header: record.header, + chain_mmr: record.chainMmrPeaks, + has_client_notes: record.hasClientNotes === 'true' + })); + + return processedRecords; } catch (err) { console.error("Failed to get tracked block headers: ", err); throw err; diff --git a/crates/rust-client/src/store/web_store/js/notes.js b/crates/rust-client/src/store/web_store/js/notes.js index bd7647f0f..80a496d5b 100644 --- a/crates/rust-client/src/store/web_store/js/notes.js +++ b/crates/rust-client/src/store/web_store/js/notes.js @@ -39,7 +39,7 @@ export async function getInputNotes( notes = await inputNotes .where('status') .equals(status) - .and(note => note.ignored === false) + .and(note => note.ignored === "false") .toArray(); } @@ -54,7 +54,7 @@ export async function getIgnoredInputNotes() { try { const notes = await inputNotes .where('ignored') - .equals(true) + .equals("true") .toArray(); return await processInputNotes(notes); @@ -68,7 +68,7 @@ export async function getIgnoredOutputNotes() { try { const notes = await outputNotes .where('ignored') - .equals(true) + .equals("true") .toArray(); return await processOutputNotes(notes); @@ -152,10 +152,10 @@ export async function insertInputNote( status: status, metadata: metadata ? metadata : null, details: details, - inclusionProof: inclusionProof ? JSON.stringify(inclusionProof) : null, + inclusionProof: inclusionProof ? inclusionProof : null, consumerTransactionId: null, createdAt: serializedCreatedAt, - ignored: ignored, + ignored: ignored.toString(), importedTag: importedTag ? importedTag : null }; @@ -201,10 +201,10 @@ export async function insertOutputNote( status: status, metadata: metadata, details: details ? details : null, - inclusionProof: inclusionProof ? JSON.stringify(inclusionProof) : null, + inclusionProof: inclusionProof ? inclusionProof : null, consumerTransactionId: null, createdAt: serializedCreatedAt, - ignored: false, + ignored: "false", imported_tag: null }; @@ -338,10 +338,11 @@ async function processInputNotes( created_at: note.createdAt, submitted_at: note.submittedAt ? note.submittedAt : null, nullifier_height: note.nullifierHeight ? note.nullifierHeight : null, - ignored: note.ignored, + ignored: note.ignored === "true", imported_tag: note.importedTag ? note.importedTag : null }; })); + return processedNotes; } @@ -392,7 +393,7 @@ async function processOutputNotes( created_at: note.createdAt, submitted_at: note.submittedAt ? note.submittedAt : null, nullifier_height: note.nullifierHeight ? note.nullifierHeight : null, - ignored: note.ignored, + ignored: note.ignored === "true", imported_tag: note.importedTag ? note.importedTag : null }; })); diff --git a/crates/rust-client/src/store/web_store/js/schema.js b/crates/rust-client/src/store/web_store/js/schema.js index 86d59ea80..75712cb76 100644 --- a/crates/rust-client/src/store/web_store/js/schema.js +++ b/crates/rust-client/src/store/web_store/js/schema.js @@ -36,14 +36,14 @@ db.version(1).stores({ [Table.AccountStorage]: indexes('root'), [Table.AccountVaults]: indexes('root'), [Table.AccountAuth]: indexes('accountId', 'pubKey'), - [Table.Accounts]: indexes('[id+nonce]', 'codeRoot', 'storageRoot', 'vaultRoot'), + [Table.Accounts]: indexes('[id+nonce]', 'codeRoot', 'storageRoot', 'vaultRoot', 'accountHash'), [Table.Transactions]: indexes('id'), [Table.TransactionScripts]: indexes('scriptHash'), - [Table.InputNotes]: indexes('noteId', 'recipient', 'status'), - [Table.OutputNotes]: indexes('noteId', 'recipient', 'status'), + [Table.InputNotes]: indexes('noteId', 'recipient', 'status', 'importedTag', 'ignored'), + [Table.OutputNotes]: indexes('noteId', 'recipient', 'status', 'importedTag', 'ignored'), [Table.NotesScripts]: indexes('scriptHash'), [Table.StateSync]: indexes('id'), - [Table.BlockHeaders]: indexes('blockNum'), + [Table.BlockHeaders]: indexes('blockNum', 'hasClientNotes'), [Table.ChainMmrNodes]: indexes('id'), }); diff --git a/crates/rust-client/src/store/web_store/js/sync.js b/crates/rust-client/src/store/web_store/js/sync.js index cba06c475..bce0f2959 100644 --- a/crates/rust-client/src/store/web_store/js/sync.js +++ b/crates/rust-client/src/store/web_store/js/sync.js @@ -161,7 +161,7 @@ async function updateBlockHeader( blockNum: blockNum, header: blockHeader, chainMmrPeaks: chainMmrPeaks, - hasClientNotes: hasClientNotes + hasClientNotes: hasClientNotes.toString() }; await tx.blockHeaders.add(data); @@ -283,7 +283,7 @@ async function updateCommittedTransactions( } } -async function updateIgnoredNotesForTag( +export async function updateIgnoredNotesForTag( tag ) { try { diff --git a/crates/rust-client/src/store/web_store/notes/models.rs b/crates/rust-client/src/store/web_store/notes/models.rs index 0ef204cff..c279d053f 100644 --- a/crates/rust-client/src/store/web_store/notes/models.rs +++ b/crates/rust-client/src/store/web_store/notes/models.rs @@ -1,6 +1,6 @@ use alloc::{string::String, vec::Vec}; -use base64::decode as base64_decode; +use base64::{engine::general_purpose, Engine as _}; use serde::{de::Error, Deserialize, Deserializer, Serialize}; #[derive(Serialize, Deserialize)] @@ -44,7 +44,9 @@ where D: Deserializer<'de>, { let base64_str: String = Deserialize::deserialize(deserializer)?; - base64_decode(&base64_str).map_err(|e| Error::custom(format!("Base64 decode error: {}", e))) + general_purpose::STANDARD + .decode(&base64_str) + .map_err(|e| Error::custom(format!("Base64 decode error: {}", e))) } fn base64_to_vec_u8_optional<'de, D>(deserializer: D) -> Result>, D::Error> @@ -53,7 +55,8 @@ where { let base64_str: Option = Option::deserialize(deserializer)?; match base64_str { - Some(str) => base64_decode(&str) + Some(str) => general_purpose::STANDARD + .decode(&str) .map(Some) .map_err(|e| Error::custom(format!("Base64 decode error: {}", e))), None => Ok(None), diff --git a/crates/rust-client/src/store/web_store/notes/utils.rs b/crates/rust-client/src/store/web_store/notes/utils.rs index 9584c2be2..037d28d8a 100644 --- a/crates/rust-client/src/store/web_store/notes/utils.rs +++ b/crates/rust-client/src/store/web_store/notes/utils.rs @@ -317,7 +317,7 @@ pub fn parse_input_note_idxdb_object( }; let imported_tag_as_u32: Option = - note_idxdb.imported_tag.as_ref().map(|tag| tag.parse::().ok()).flatten(); + note_idxdb.imported_tag.as_ref().and_then(|tag| tag.parse::().ok()); Ok(InputNoteRecord::new( id, diff --git a/crates/rust-client/src/store/web_store/transactions/models.rs b/crates/rust-client/src/store/web_store/transactions/models.rs index 016b92f04..1b742edac 100644 --- a/crates/rust-client/src/store/web_store/transactions/models.rs +++ b/crates/rust-client/src/store/web_store/transactions/models.rs @@ -1,6 +1,6 @@ use alloc::{string::String, vec::Vec}; -use base64::decode as base64_decode; +use base64::{engine::general_purpose, Engine as _}; use serde::{de::Error, Deserialize, Deserializer, Serialize}; #[derive(Serialize, Deserialize)] @@ -26,7 +26,9 @@ where D: Deserializer<'de>, { let base64_str: String = Deserialize::deserialize(deserializer)?; - base64_decode(&base64_str).map_err(|e| Error::custom(format!("Base64 decode error: {}", e))) + general_purpose::STANDARD + .decode(&base64_str) + .map_err(|e| Error::custom(format!("Base64 decode error: {}", e))) } fn base64_to_vec_u8_optional<'de, D>(deserializer: D) -> Result>, D::Error> @@ -35,7 +37,8 @@ where { let base64_str: Option = Option::deserialize(deserializer)?; match base64_str { - Some(str) => base64_decode(&str) + Some(str) => general_purpose::STANDARD + .decode(&str) .map(Some) .map_err(|e| Error::custom(format!("Base64 decode error: {}", e))), None => Ok(None), diff --git a/crates/rust-client/src/transactions/mod.rs b/crates/rust-client/src/transactions/mod.rs index 7f176013f..f53c45672 100644 --- a/crates/rust-client/src/transactions/mod.rs +++ b/crates/rust-client/src/transactions/mod.rs @@ -10,9 +10,9 @@ use miden_objects::{ accounts::{AccountDelta, AccountId, AccountType}, assembly::ProgramAst, assets::FungibleAsset, - notes::{Note, NoteDetails, NoteId, NoteType}, + notes::{Note, NoteDetails, NoteExecutionHint, NoteId, NoteTag, NoteType}, transaction::{InputNotes, TransactionArgs}, - Digest, Felt, FieldElement, Word, + Digest, Felt, FieldElement, NoteError, Word, }; use miden_tx::{auth::TransactionAuthenticator, ProvingOptions, TransactionProver}; use request::{TransactionRequestError, TransactionScriptTemplate}; @@ -502,3 +502,42 @@ pub fn notes_from_output(output_notes: &OutputNotes) -> impl Iterator) +/// +/// TODO: we should make the function in base public and once that gets released use that one and +/// delete this implementation. +pub fn build_swap_tag( + note_type: NoteType, + offered_asset_faucet_id: AccountId, + requested_asset_faucet_id: AccountId, +) -> Result { + const SWAP_USE_CASE_ID: u16 = 0; + + // get bits 4..12 from faucet IDs of both assets, these bits will form the tag payload; the + // reason we skip the 4 most significant bits is that these encode metadata of underlying + // faucets and are likely to be the same for many different faucets. + + let offered_asset_id: u64 = offered_asset_faucet_id.into(); + let offered_asset_tag = (offered_asset_id >> 52) as u8; + + let requested_asset_id: u64 = requested_asset_faucet_id.into(); + let requested_asset_tag = (requested_asset_id >> 52) as u8; + + let payload = ((offered_asset_tag as u16) << 8) | (requested_asset_tag as u16); + + let execution = NoteExecutionHint::Local; + match note_type { + NoteType::Public => NoteTag::for_public_use_case(SWAP_USE_CASE_ID, payload, execution), + _ => NoteTag::for_local_use_case(SWAP_USE_CASE_ID, payload), + } +} diff --git a/crates/web-client/Cargo.toml b/crates/web-client/Cargo.toml new file mode 100644 index 000000000..3631af525 --- /dev/null +++ b/crates/web-client/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "miden-client-web" +version = "0.5.0" +description = "Web Client library that facilitates interaction with the Miden rollup" +readme = "README.md" +categories = ["no-std"] +keywords = ["miden", "client", "web", "wasm"] +license.workspace = true +authors.workspace = true +repository.workspace = true +rust-version.workspace = true +edition.workspace = true + +[lib] +crate-type = ["cdylib"] + +[features] +testing = ["miden-client/testing"] + +[dependencies] +getrandom = { version = "0.2", features = ["js"] } +miden-client = { path = "../rust-client", features = ["idxdb", "web-tonic"] } +miden-lib = { workspace = true } +miden-objects = { workspace = true } +miden-tx = { workspace = true } +rand = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +serde-wasm-bindgen = { version = "0.6" } +wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } +wasm-bindgen-futures = { version = "0.4" } + +[dev-dependencies] +miden-client = { path = "../rust-client", features = ["testing", "concurrent", "idxdb", "web-tonic"] } +miden-lib = { default-features = false, features = ["testing"], git = "https://github.com/0xPolygonMiden/miden-base", branch = "next" } +miden-objects = {default-features = false, features = ["serde", "testing"], git = "https://github.com/0xPolygonMiden/miden-base", branch = "next" } diff --git a/crates/web-client/README.md b/crates/web-client/README.md new file mode 100644 index 000000000..454a6997f --- /dev/null +++ b/crates/web-client/README.md @@ -0,0 +1,456 @@ +# @demox-labs/miden-sdk +The @demox-labs/miden-sdk is a toolkit designed for interacting with the Miden virtual machine. It offers essential tools and functionalities for developers aiming to integrate or utilize Miden VM capabilities in their applications. + +## Installation +To install the package via npm, run the following command: + +```javascript +npm i @demox-labs/miden-sdk +``` + +For yarn: +```javascript +yarn add @demox-labs/miden-sdk +``` + +## Usage + +```typescript +import { WebClient } from "@demox-labs/miden-sdk"; + +const webClient = new WebClient(); +await webClient.create_client(); + +// Use WebClient to create accounts, notes, transactions, etc. +// This will create a mutable, off-chain account and store it in IndexedDB +const accountId = await webClient.new_wallet("OffChain", true); +``` + +## Examples +### The WebClient +The WebClient is your gateway to creating and interacting with anything miden vm related. +Example: +```typescript +// Creates a new WebClient instance which can then be configured after +const webClient = new WebClient(); + +// Creates the internal client of a previously instantiated WebClient. +// Can provide `node_url` as an optional parameter. Defaults to "http://18.203.155.106:57291" which is the URL +// of the remote miden node. +await webClient.create_client(); +``` +Example specifying a specific node URL: +```typescript +const webClient = new WebClient(); + +let remote_node_url = "http://18.203.155.106:57291" +await webClient.create_client(remote_node_url); +``` + +### Accounts +You can use the WebClient to create and retrieve account information. +```typescript +const webClient = new WebClient(); +await webClient.create_client(); + +/** + * Creates a new wallet account. + * + * @param storage_mode String. Either "OffChain" or "OnChain". + * @param mutable Boolean. Whether the wallet code is mutable or not + * + * Returns: Wallet Id + */ +const walletId = await webClient.new_wallet("OffChain", true); + +/** + * Creates a new faucet account. + * + * @param storage_mode String. Either "OffChain" or "OnChain". + * @param non_fungible Boolean. Whether the faucet is non_fungible or not. NOTE: Non-fungible faucets are not supported yet + * @param token_symbol String. Token symbol of the token the faucet creates + * @param decimals String. Decimal precision of token. + * @param max_supply String. Maximum token supply + */ +const faucetId = await webClient.new_faucet("OffChain", true, "TOK", 6, 1_000_000) + +/** + * Returns all accounts. Both wallets and faucets. Returns the following object per account + * { + * id: string + * nonce: string + * vault_root: string + * storage_root: string + * code_root: string + * }/ +const accounts = await webClient.get_accounts() +console.log(accounts[0].id) // Prints account id of first account retrieved as hex value + +// Gets a single account by id +const account = await webClient.get_account("0x9258fec00ad6d9bc"); + +// Imports an account. This example adds a simple button to an HTML page, creates a listener for an account file selection, serializes that file into bytes, then calls the client to import it. + + +document.getElementById('accountFileInput').addEventListener('change', function(event) { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + + reader.onload = async function(e) { + let webClient = await createMidenWebClient(); + const arrayBuffer = e.target.result; + const byteArray = new Uint8Array(arrayBuffer); + + await webClient.importAccount(accountAsBytes); + }; + + reader.readAsArrayBuffer(file); + } +}); +``` + +### Transactions +You can use the WebClient to facilitate transactions between accounts. + +Let's mint some tokens for our wallet from our faucet: +```typescript +const webClient = new WebClient(); +await webClient.create_client(); +const walletId = await webClient.new_wallet("OffChain", true); +const faucetId = await webClient.new_faucet("OffChain", true, "TOK", 6, 1_000_000); + +// Syncs web client with node state. +await webClient.sync_state(); +// Caches faucet account auth. A workaround to allow for synchronicity in the transaction flow. +await webClient.fetch_and_cache_account_auth_by_pub_key(faucetId); + +/** + * Mints 10_000 tokens for the previously created wallet via a Private Note and returns a transaction the following result object: + * { + * transaction_id: string + * created_note_ids: string[] + * } + */ +const newTxnResult = await webClient.new_mint_transaction(walletId, faucetId, "Private", 10_000); +console.log(newTxnResult.created_note_ids); // Prints the list of note ids created from this transaction + +// Sync state again +await webClient.sync_state(); + +/** + * Gets all of your existing transactions + * Returns string[] of transaction ids + */ +const transactions = await webClient.get_transactions() +``` + +### Notes +You can use the WebClient to query for existing notes, export notes, and import notes + +Here is an example of how to import a note from a file (generated, say, from the faucet at https://testnet.miden.io/ for a given account). This code exposes a simple button on an HTML page for a user to select a file. A listener is setup to capture this event, serialize the note file, and import it. +```typescript +let webClient = await createMidenWebClient(); +let walletAccount = await webClient.new_wallet("OffChain", true); +console.log(walletAccount); // Prints the id that can be used to plug in to the deployed Miden faucet + + + +document.getElementById('noteFileInput').addEventListener('change', async function(event) { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + + reader.onload = async function(e) { + let webClient = await createMidenWebClient(); + + const arrayBuffer = e.target.result; + const byteArray = new Uint8Array(arrayBuffer); + + await webClient.import_note(byteArray, true); // imports the file generated from the faucet previously + }; + + reader.readAsArrayBuffer(file); + } +}); +``` + +Example of exporting a note: +```typescript +console.log("testExportNote started"); + +let webClient = await createMidenWebClient(); + +// Create a faucet and mint a mint transaction +let faucetId = await createNewFaucet(webClient, "OffChain", false, "DEN", "10", "1000000"); +await syncState(webClient); +await new Promise(r => setTimeout(r, 20000)); // Artificial delays to ensure sync is processed on remote node before continuing + +await webClient.fetch_and_cache_account_auth_by_pub_key(faucetId); + +let mintTransactionResult = await createNewMintTransaction( + webClient, + "0x9186b96f559e852f", // Insert target account id here + faucetId, + "Private", + "1000" +); +await new Promise(r => setTimeout(r, 20000)); +await syncState(webClient); + +// Take the note created from the mint transaction, serialize it, and download it via the browser immediately +let result = await exportNote(webClient, mintTransactionResult.created_note_ids[0]); + +const blob = new Blob([result], {type: 'application/octet-stream'}); + +// Create a URL for the Blob +const url = URL.createObjectURL(blob); + +// Create a temporary anchor element +const a = document.createElement('a'); +a.href = url; +a.download = 'exportNoteTest.mno'; // Specify the file name + +// Append the anchor to the document +document.body.appendChild(a); + +// Programmatically click the anchor to trigger the download +a.click(); + +// Remove the anchor from the document +document.body.removeChild(a); + +// Revoke the object URL to free up resources +URL.revokeObjectURL(url); +``` + +Get All Input Notes Example: +```typescript +let webClient = await createMidenWebClient(); + +/** + * get_input_notes takes a filter to retrieve notes based on a specific status. The options are the following: + * "All" + * "Consumed" + * "Committed" + * "Expected" + * "Processing" + */ +const notes = await webClient.get_input_notes("All") +``` + +## API Reference + +```typescript +/** + * @returns {Promise} + * + * Example of returned object: + * { + * id: string, + * nonce: string, + * vault_root: string, + * storage_root: string, + * code_root: string + * } + */ +get_accounts(): Promise; + +/** + * @param {string} account_id + * @returns {Promise} + */ +get_account(account_id: string): Promise; + +/** + * @param {any} pub_key_bytes + * @returns {any} + */ +get_account_auth_by_pub_key(pub_key_bytes: any): any; + +/** + * @param {string} account_id + * @returns {Promise} + */ +fetch_and_cache_account_auth_by_pub_key(account_id: string): Promise; + +/** + * @param {string} note_id + * @param {string} export_type + * @returns {Promise} + * + * export_type can be any of the following: + * + * "Full" + * "Partial" + * "Id" + */ +export_note(note_id: string, export_type: string): Promise; + +/** + * @param {any} account_bytes + * @returns created account id as {Promise} + * + */ +import_account(account_bytes: any): Promise; + +/** + * @param {string} note_bytes + * @param {boolean} verify + * @returns {Promise} + */ +import_note(note_bytes: string, verify: boolean): Promise; + +/** + * @param {string} storage_type + * @param {boolean} mutable + * @returns {Promise} + */ +new_wallet(storage_type: string, mutable: boolean): Promise; + +/** + * @param {string} storage_type + * @param {boolean} non_fungible + * @param {string} token_symbol + * @param {string} decimals + * @param {string} max_supply + * @returns {Promise} + */ +new_faucet(storage_type: string, non_fungible: boolean, token_symbol: string, decimals: string, max_supply: string): Promise; + +/** + * @param {string} target_account_id + * @param {string} faucet_id + * @param {string} note_type + * @param {string} amount + * @returns {Promise} + * + * Example of a NewTransactionResult object: + * { + * transaction_id: string, + * created_note_ids: string[] + * } + */ +new_mint_transaction(target_account_id: string, faucet_id: string, note_type: string, amount: string): Promise; + +/** + * @param {string} sender_account_id + * @param {string} target_account_id + * @param {string} faucet_id + * @param {string} note_type + * @param {string} amount + * @param {string | undefined} [recall_height] + * @returns {Promise} + * + * Example of a NewTransactionResult object: + * { + * transaction_id: string, + * created_note_ids: string[] + * } + */ +new_send_transaction(sender_account_id: string, target_account_id: string, faucet_id: string, note_type: string, amount: string, recall_height?: string): Promise; + +/** + * @param {string} account_id + * @param {(string)[]} list_of_notes + * @returns {Promise} + * + * Example of a NewTransactionResult object: + * { + * transaction_id: string, + * created_note_ids: string[] + * } + */ +new_consume_transaction(account_id: string, list_of_notes: (string)[]): Promise; + +/** + * @param {string} sender_account_id + * @param {string} offered_asset_faucet_id + * @param {string} offered_asset_amount + * @param {string} requested_asset_faucet_id + * @param {string} requested_asset_amount + * @param {string} note_type + * @returns {Promise} + * + * Example of a NewSwapTransactionResult object: + * { + * transaction_id: string, + * expected_output_note_ids: string[], + *. expected_partial_note_ids: string[], + * payback_note_tag: string, + * } + */ +new_swap_transaction(sender_account_id: string, offered_asset_faucet_id: string, offered_asset_amount: string, requested_asset_faucet_id: string, requested_asset_amount: string, note_type: string): Promise; + +/** + * @param {any} filter + * @returns {Promise} + * + * Examples of valid filters: + * "All" + * "Consumed" + * "Committed" + * "Expected" + * "Processing" + */ +get_input_notes(filter: any): Promise; + +/** + * @param {string} note_id + * @returns note id as {Promise} + */ +get_input_note(note_id: string): Promise; + +/** + * @param {any} filter + * @returns {Promise} + */ +get_output_notes(filter: any): Promise; + +/** + * @param {string} note_id + * @returns {Promise} + * + * Examples of valid filters: + * "All" + * "Consumed" + * "Committed" + * "Expected" + * "Processing" + */ +get_output_note(note_id: string): Promise; + +/** + * @returns block number of latest block you synced to {Promise} + */ +sync_state(): Promise; + +/** + * @returns list of existing transaction ids {Promise} + */ +get_transactions(): Promise; + +/** + * @param {string} tag + * @returns {Promise} + */ +add_tag(tag: string): Promise; + +/** + */ +constructor(); + +/** + * @param {string | undefined} [node_url] + * @returns {Promise} + */ +create_client(node_url?: string): Promise; +``` + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/crates/web-client/js/index.js b/crates/web-client/js/index.js new file mode 100644 index 000000000..018f0c94a --- /dev/null +++ b/crates/web-client/js/index.js @@ -0,0 +1,13 @@ +import wasm from "../dist/wasm.js"; + +const { + WebClient +} = await wasm({ + importHook: () => { + return new URL("assets/miden_client_web.wasm", import.meta.url); // the name before .wasm needs to match the package name in Cargo.toml + }, +}); + +export { + WebClient, +}; diff --git a/crates/web-client/js/types/index.d.ts b/crates/web-client/js/types/index.d.ts new file mode 100644 index 000000000..8b35a9a68 --- /dev/null +++ b/crates/web-client/js/types/index.d.ts @@ -0,0 +1,6 @@ +export { + WebClient, + NewTransactionResult, + SerializedAccountStub, + NewSwapTransactionResult +} from "./crates/miden_client"; diff --git a/crates/web-client/js/wasm.js b/crates/web-client/js/wasm.js new file mode 100644 index 000000000..aa6e8957d --- /dev/null +++ b/crates/web-client/js/wasm.js @@ -0,0 +1 @@ +export { default } from "../Cargo.toml"; diff --git a/crates/web-client/package.json b/crates/web-client/package.json new file mode 100644 index 000000000..e38b995f9 --- /dev/null +++ b/crates/web-client/package.json @@ -0,0 +1,34 @@ +{ + "name": "@demox-labs/miden-sdk", + "version": "0.0.5", + "description": "Polygon Miden Wasm SDK", + "collaborators": [ + "Polygon Miden", + "Demox Labs " + ], + "type": "module", + "main": "./dist/index.js", + "browser": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist", + "../LICENSE.md" + ], + "scripts": { + "build": "rimraf dist && rollup -c rollup.config.js && cpr js/types dist && rimraf dist/wasm*" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-node-resolve": "^15.2.3", + "@wasm-tool/rollup-plugin-rust": "^2.4.5", + "cpr": "^3.0.1", + "rimraf": "^5.0.1", + "rollup": "^3.27.2" + }, + "dependencies": { + "dexie": "^4.0.1" + } +} diff --git a/crates/web-client/rollup.config.js b/crates/web-client/rollup.config.js new file mode 100644 index 000000000..1da7c81b1 --- /dev/null +++ b/crates/web-client/rollup.config.js @@ -0,0 +1,58 @@ +import rust from "@wasm-tool/rollup-plugin-rust"; +import resolve from "@rollup/plugin-node-resolve"; +import commonjs from "@rollup/plugin-commonjs"; + +/** + * Rollup configuration file for building a Cargo project and creating a WebAssembly (WASM) module. + * The configuration sets up two build processes: + * 1. Compiling Rust code into WASM using the @wasm-tool/rollup-plugin-rust plugin, with specific + * cargo arguments to enable WebAssembly features and set maximum memory limits. + * 2. Resolving and bundling the generated WASM module along with the main JavaScript file + * (`index.js`) into the `dist` directory. + * + * The first configuration targets `wasm.js` to generate the WASM module, while the second + * configuration targets `index.js` for the main entry point of the application. + * Both configurations output ES module format files with source maps for easier debugging. + */ +export default [ + { + input: { + wasm: "./js/wasm.js", + }, + output: { + dir: `dist`, + format: "es", + sourcemap: true, + assetFileNames: "assets/[name][extname]", + }, + plugins: [ + rust({ + cargoArgs: [ + "--features", "testing", + "--config", `build.rustflags=["-C", "target-feature=+atomics,+bulk-memory,+mutable-globals", "-C", "link-arg=--max-memory=4294967296"]`, + "--no-default-features", + ], + + experimental: { + typescriptDeclarationDir: "dist/crates", + }, + }), + resolve(), + commonjs(), + ], + }, + { + input: { + index: "./js/index.js", + }, + output: { + dir: `dist`, + format: "es", + sourcemap: true, + }, + plugins: [ + resolve(), + commonjs(), + ], + } +]; diff --git a/crates/web-client/src/account.rs b/crates/web-client/src/account.rs new file mode 100644 index 000000000..cab30ec3a --- /dev/null +++ b/crates/web-client/src/account.rs @@ -0,0 +1,64 @@ +use miden_objects::accounts::AccountId; +use wasm_bindgen::prelude::*; + +use crate::{models::accounts::SerializedAccountStub, WebClient}; + +#[wasm_bindgen] +impl WebClient { + pub async fn get_accounts(&mut self) -> Result { + if let Some(client) = self.get_mut_inner() { + let account_tuples = client.get_account_stubs().await.unwrap(); + let accounts: Vec = account_tuples + .into_iter() + .map(|(account, _)| { + SerializedAccountStub::new( + account.id().to_string(), + account.nonce().to_string(), + account.vault_root().to_string(), + account.storage_root().to_string(), + account.code_commitment().to_string(), + ) + }) + .collect(); + + let accounts_as_js_value = + serde_wasm_bindgen::to_value(&accounts).unwrap_or_else(|_| { + wasm_bindgen::throw_val(JsValue::from_str("Serialization error")) + }); + + Ok(accounts_as_js_value) + } else { + Err(JsValue::from_str("Client not initialized")) + } + } + + pub async fn get_account(&mut self, account_id: String) -> Result { + if let Some(client) = self.get_mut_inner() { + let native_account_id = AccountId::from_hex(&account_id).unwrap(); + + let result = client.get_account(native_account_id).await.unwrap(); + + serde_wasm_bindgen::to_value(&result.0.id().to_string()) + .map_err(|e| JsValue::from_str(&e.to_string())) + } else { + Err(JsValue::from_str("Client not initialized")) + } + } + + pub async fn fetch_and_cache_account_auth_by_pub_key( + &mut self, + account_id: String, + ) -> Result { + if let Some(client) = self.get_mut_inner() { + let _ = client + .store() + .fetch_and_cache_account_auth_by_pub_key(account_id) + .await + .unwrap(); + + Ok(JsValue::from_str("Okay, it worked")) + } else { + Err(JsValue::from_str("Client not initialized")) + } + } +} diff --git a/crates/web-client/src/export.rs b/crates/web-client/src/export.rs new file mode 100644 index 000000000..748cd6917 --- /dev/null +++ b/crates/web-client/src/export.rs @@ -0,0 +1,72 @@ +use miden_client::{store::NoteFilter, utils::Serializable}; +use miden_objects::{notes::NoteFile, Digest}; +use wasm_bindgen::prelude::*; + +use crate::WebClient; + +#[derive(Clone, Debug)] +pub enum ExportType { + Id, + Full, + Partial, +} + +#[wasm_bindgen] +impl WebClient { + pub async fn export_note( + &mut self, + note_id: String, + export_type: String, + ) -> Result { + if let Some(client) = self.get_mut_inner() { + let note_id = Digest::try_from(note_id) + .map_err(|err| { + JsValue::from_str(&format!("Failed to parse input note id: {}", err)) + })? + .into(); + + let mut output_notes = + client.get_output_notes(NoteFilter::Unique(note_id)).await.map_err(|err| { + JsValue::from_str(&format!("Failed to get output notes: {}", err)) + })?; + + let output_note = + output_notes.pop().ok_or_else(|| JsValue::from_str("No output note found"))?; + + let export_type = match export_type.as_str() { + "Id" => ExportType::Id, + "Full" => ExportType::Full, + "Partial" => ExportType::Partial, + _ => ExportType::Partial, + }; + + let note_file = match export_type { + ExportType::Id => NoteFile::NoteId(output_note.id()), + ExportType::Full => match output_note.inclusion_proof() { + Some(inclusion_proof) => NoteFile::NoteWithProof( + output_note.clone().try_into().map_err(|err| { + JsValue::from_str(&format!("Failed to convert output note: {}", err)) + })?, + inclusion_proof.clone(), + ), + None => return Err(JsValue::from_str("Note does not have inclusion proof")), + }, + ExportType::Partial => NoteFile::NoteDetails( + output_note.clone().try_into().map_err(|err| { + JsValue::from_str(&format!("Failed to convert output note: {}", err)) + })?, + Some(output_note.metadata().tag()), + ), + }; + + let input_note_bytes = note_file.to_bytes(); + + let serialized_input_note_bytes = serde_wasm_bindgen::to_value(&input_note_bytes) + .map_err(|_| JsValue::from_str("Serialization error"))?; + + Ok(serialized_input_note_bytes) + } else { + Err(JsValue::from_str("Client not initialized")) + } + } +} diff --git a/crates/web-client/src/import.rs b/crates/web-client/src/import.rs new file mode 100644 index 000000000..6f154b636 --- /dev/null +++ b/crates/web-client/src/import.rs @@ -0,0 +1,49 @@ +use miden_objects::{accounts::AccountData, notes::NoteFile, utils::Deserializable}; +use serde_wasm_bindgen::from_value; +use wasm_bindgen::prelude::*; + +use crate::WebClient; + +#[wasm_bindgen] +impl WebClient { + pub async fn import_account(&mut self, account_bytes: JsValue) -> Result { + if let Some(client) = self.get_mut_inner() { + let account_bytes_result: Vec = from_value(account_bytes).unwrap(); + let account_data = AccountData::read_from_bytes(&account_bytes_result) + .map_err(|err| err.to_string())?; + let account_id = account_data.account.id().to_string(); + + match client.import_account(account_data).await { + Ok(_) => { + let message = format!("Imported account with ID: {}", account_id); + Ok(JsValue::from_str(&message)) + }, + Err(err) => { + let error_message = format!("Failed to import account: {:?}", err); + Err(JsValue::from_str(&error_message)) + }, + } + } else { + Err(JsValue::from_str("Client not initialized")) + } + } + + pub async fn import_note(&mut self, note_bytes: JsValue) -> Result { + if let Some(client) = self.get_mut_inner() { + let note_bytes_result: Vec = from_value(note_bytes).unwrap(); + + let note_file = + NoteFile::read_from_bytes(¬e_bytes_result).map_err(|err| err.to_string())?; + + match client.import_note(note_file).await { + Ok(note_id) => Ok(JsValue::from_str(note_id.to_string().as_str())), + Err(err) => { + let error_message = format!("Failed to import note: {:?}", err); + Err(JsValue::from_str(&error_message)) + }, + } + } else { + Err(JsValue::from_str("Client not initialized")) + } + } +} diff --git a/crates/web-client/src/lib.rs b/crates/web-client/src/lib.rs new file mode 100644 index 000000000..ea8fc9465 --- /dev/null +++ b/crates/web-client/src/lib.rs @@ -0,0 +1,79 @@ +extern crate alloc; +use alloc::rc::Rc; + +use miden_client::{ + auth::StoreAuthenticator, rpc::WebTonicRpcClient, store::web_store::WebStore, Client, +}; +use miden_objects::{crypto::rand::RpoRandomCoin, Felt}; +use rand::{rngs::StdRng, Rng, SeedableRng}; +use wasm_bindgen::prelude::*; + +pub mod account; +pub mod export; +pub mod import; +pub mod models; +pub mod new_account; +pub mod new_transactions; +pub mod notes; +pub mod sync; +pub mod tags; +pub mod transactions; + +#[wasm_bindgen] +pub struct WebClient { + inner: Option< + Client< + WebTonicRpcClient, + RpoRandomCoin, + WebStore, + StoreAuthenticator, + >, + >, +} + +impl Default for WebClient { + fn default() -> Self { + Self::new() + } +} + +#[wasm_bindgen] +impl WebClient { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + WebClient { inner: None } + } + + pub(crate) fn get_mut_inner( + &mut self, + ) -> Option< + &mut Client< + WebTonicRpcClient, + RpoRandomCoin, + WebStore, + StoreAuthenticator, + >, + > { + self.inner.as_mut() + } + + pub async fn create_client(&mut self, node_url: Option) -> Result { + let mut rng = StdRng::from_entropy(); + let coin_seed: [u64; 4] = rng.gen(); + + let rng = RpoRandomCoin::new(coin_seed.map(Felt::new)); + let web_store: WebStore = WebStore::new() + .await + .map_err(|_| JsValue::from_str("Failed to initialize WebStore"))?; + let web_store = Rc::new(web_store); + let authenticator: StoreAuthenticator = + StoreAuthenticator::new_with_rng(web_store.clone(), rng); + let web_rpc_client = WebTonicRpcClient::new( + &node_url.unwrap_or_else(|| "http://18.203.155.106:57291".to_string()), + ); + + self.inner = Some(Client::new(web_rpc_client, rng, web_store, authenticator, false)); + + Ok(JsValue::from_str("Client created successfully")) + } +} diff --git a/crates/web-client/src/models/accounts.rs b/crates/web-client/src/models/accounts.rs new file mode 100644 index 000000000..a8257f187 --- /dev/null +++ b/crates/web-client/src/models/accounts.rs @@ -0,0 +1,56 @@ +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +#[derive(Serialize, Deserialize)] +pub struct SerializedAccountStub { + id: String, + nonce: String, + vault_root: String, + storage_root: String, + code_root: String, +} + +#[wasm_bindgen] +impl SerializedAccountStub { + pub fn new( + id: String, + nonce: String, + vault_root: String, + storage_root: String, + code_root: String, + ) -> SerializedAccountStub { + SerializedAccountStub { + id, + nonce, + vault_root, + storage_root, + code_root, + } + } + + #[wasm_bindgen(getter)] + pub fn id(&self) -> String { + self.id.clone() + } + + #[wasm_bindgen(getter)] + pub fn nonce(&self) -> String { + self.nonce.clone() + } + + #[wasm_bindgen(getter)] + pub fn vault_root(&self) -> String { + self.vault_root.clone() + } + + #[wasm_bindgen(getter)] + pub fn storage_root(&self) -> String { + self.storage_root.clone() + } + + #[wasm_bindgen(getter)] + pub fn code_root(&self) -> String { + self.code_root.clone() + } +} diff --git a/crates/web-client/src/models/mod.rs b/crates/web-client/src/models/mod.rs new file mode 100644 index 000000000..c0963f953 --- /dev/null +++ b/crates/web-client/src/models/mod.rs @@ -0,0 +1,2 @@ +pub mod accounts; +pub mod transactions; diff --git a/crates/web-client/src/models/transactions.rs b/crates/web-client/src/models/transactions.rs new file mode 100644 index 000000000..ac8d44785 --- /dev/null +++ b/crates/web-client/src/models/transactions.rs @@ -0,0 +1,73 @@ +use wasm_bindgen::{prelude::*, JsValue}; + +#[wasm_bindgen] +pub struct NewTransactionResult { + transaction_id: String, + created_note_ids: Vec, +} + +#[wasm_bindgen] +impl NewTransactionResult { + pub fn new(transaction_id: String, created_note_ids: Vec) -> NewTransactionResult { + NewTransactionResult { transaction_id, created_note_ids } + } + + #[wasm_bindgen(getter)] + pub fn transaction_id(&self) -> String { + self.transaction_id.clone() + } + + #[wasm_bindgen(getter)] + pub fn created_note_ids(&self) -> JsValue { + serde_wasm_bindgen::to_value(&self.created_note_ids).unwrap() + } +} + +#[wasm_bindgen] +pub struct NewSwapTransactionResult { + transaction_id: String, + expected_output_note_ids: Vec, + expected_partial_note_ids: Vec, + payback_note_tag: String, +} + +#[wasm_bindgen] +impl NewSwapTransactionResult { + pub fn new( + transaction_id: String, + expected_output_note_ids: Vec, + expected_partial_note_ids: Vec, + payback_note_tag: Option, + ) -> NewSwapTransactionResult { + NewSwapTransactionResult { + transaction_id, + expected_output_note_ids, + expected_partial_note_ids, + payback_note_tag: payback_note_tag.unwrap_or_default(), + } + } + + pub fn set_note_tag(&mut self, payback_note_tag: String) { + self.payback_note_tag = payback_note_tag + } + + #[wasm_bindgen(getter)] + pub fn transaction_id(&self) -> String { + self.transaction_id.clone() + } + + #[wasm_bindgen(getter)] + pub fn expected_output_note_ids(&self) -> JsValue { + serde_wasm_bindgen::to_value(&self.expected_output_note_ids).unwrap() + } + + #[wasm_bindgen(getter)] + pub fn expected_partial_note_ids(&self) -> JsValue { + serde_wasm_bindgen::to_value(&self.expected_partial_note_ids).unwrap() + } + + #[wasm_bindgen(getter)] + pub fn payback_note_tag(&self) -> String { + self.payback_note_tag.clone() + } +} diff --git a/crates/web-client/src/new_account.rs b/crates/web-client/src/new_account.rs new file mode 100644 index 000000000..0a6e8ddda --- /dev/null +++ b/crates/web-client/src/new_account.rs @@ -0,0 +1,76 @@ +use miden_client::accounts::AccountTemplate; +use miden_objects::{accounts::AccountStorageType, assets::TokenSymbol}; +use wasm_bindgen::prelude::*; + +use crate::WebClient; + +#[wasm_bindgen] +impl WebClient { + pub async fn new_wallet( + &mut self, + storage_type: String, + mutable: bool, + ) -> Result { + if let Some(client) = self.get_mut_inner() { + let client_template = AccountTemplate::BasicWallet { + mutable_code: mutable, + storage_type: match storage_type.as_str() { + "OffChain" => AccountStorageType::OffChain, + "OnChain" => AccountStorageType::OnChain, + _ => return Err(JsValue::from_str("Invalid storage mode")), + }, + }; + + match client.new_account(client_template).await { + Ok((account, _)) => serde_wasm_bindgen::to_value(&account.id().to_string()) + .map_err(|e| JsValue::from_str(&e.to_string())), + Err(err) => { + let error_message = format!("Failed to create new account: {:?}", err); + Err(JsValue::from_str(&error_message)) + }, + } + } else { + Err(JsValue::from_str("Client not initialized")) + } + } + + pub async fn new_faucet( + &mut self, + storage_type: String, + non_fungible: bool, + token_symbol: String, + decimals: String, + max_supply: String, + ) -> Result { + if non_fungible { + return Err(JsValue::from_str("Non-fungible faucets are not supported yet")); + } + + if let Some(client) = self.get_mut_inner() { + let client_template = AccountTemplate::FungibleFaucet { + token_symbol: TokenSymbol::new(&token_symbol) + .map_err(|e| JsValue::from_str(&e.to_string()))?, + decimals: decimals.parse::().map_err(|e| JsValue::from_str(&e.to_string()))?, + max_supply: max_supply + .parse::() + .map_err(|e| JsValue::from_str(&e.to_string()))?, + storage_type: match storage_type.as_str() { + "OffChain" => AccountStorageType::OffChain, + "OnChain" => AccountStorageType::OnChain, + _ => return Err(JsValue::from_str("Invalid storage mode")), + }, + }; + + match client.new_account(client_template).await { + Ok((account, _)) => serde_wasm_bindgen::to_value(&account.id().to_string()) + .map_err(|e| JsValue::from_str(&e.to_string())), + Err(err) => { + let error_message = format!("Failed to create new account: {:?}", err); + Err(JsValue::from_str(&error_message)) + }, + } + } else { + Err(JsValue::from_str("Client not initialized")) + } + } +} diff --git a/crates/web-client/src/new_transactions.rs b/crates/web-client/src/new_transactions.rs new file mode 100644 index 000000000..85fef33e6 --- /dev/null +++ b/crates/web-client/src/new_transactions.rs @@ -0,0 +1,253 @@ +use miden_client::{ + notes::get_input_note_with_id_prefix, + transactions::{ + build_swap_tag, + request::{PaymentTransactionData, SwapTransactionData, TransactionTemplate}, + }, +}; +use miden_objects::{accounts::AccountId, assets::FungibleAsset, notes::NoteType as MidenNoteType}; +use wasm_bindgen::prelude::*; + +use crate::{ + models::transactions::{NewSwapTransactionResult, NewTransactionResult}, + WebClient, +}; + +#[wasm_bindgen] +impl WebClient { + pub async fn new_mint_transaction( + &mut self, + target_account_id: String, + faucet_id: String, + note_type: String, + amount: String, + ) -> Result { + if let Some(client) = self.get_mut_inner() { + let target_account_id = AccountId::from_hex(&target_account_id).unwrap(); + let faucet_id = AccountId::from_hex(&faucet_id).unwrap(); + let amount_as_u64: u64 = amount.parse::().map_err(|err| err.to_string())?; + let fungible_asset = + FungibleAsset::new(faucet_id, amount_as_u64).map_err(|err| err.to_string())?; + let note_type = match note_type.as_str() { + "Public" => MidenNoteType::Public, + "Private" => MidenNoteType::Private, + _ => MidenNoteType::Private, + }; + + let mint_transaction_template = TransactionTemplate::MintFungibleAsset( + fungible_asset, + target_account_id, + note_type, + ); + + let mint_transaction_request = client + .build_transaction_request(mint_transaction_template.clone()) + .await + .unwrap(); + + let mint_transaction_execution_result = + client.new_transaction(mint_transaction_request).await.unwrap(); + + let result = NewTransactionResult::new( + mint_transaction_execution_result.executed_transaction().id().to_string(), + mint_transaction_execution_result + .created_notes() + .iter() + .map(|note| note.id().to_string()) + .collect(), + ); + + client.submit_transaction(mint_transaction_execution_result).await.unwrap(); + + Ok(result) + } else { + Err(JsValue::from_str("Client not initialized")) + } + } + + pub async fn new_send_transaction( + &mut self, + sender_account_id: String, + target_account_id: String, + faucet_id: String, + note_type: String, + amount: String, + recall_height: Option, + ) -> Result { + if let Some(client) = self.get_mut_inner() { + let sender_account_id = AccountId::from_hex(&sender_account_id).unwrap(); + let target_account_id = AccountId::from_hex(&target_account_id).unwrap(); + let faucet_id = AccountId::from_hex(&faucet_id).unwrap(); + let amount_as_u64: u64 = amount.parse::().map_err(|err| err.to_string())?; + let fungible_asset = FungibleAsset::new(faucet_id, amount_as_u64) + .map_err(|err| err.to_string())? + .into(); + + let note_type = match note_type.as_str() { + "Public" => MidenNoteType::Public, + "Private" => MidenNoteType::Private, + _ => MidenNoteType::Private, + }; + let payment_transaction = + PaymentTransactionData::new(fungible_asset, sender_account_id, target_account_id); + + let send_transaction_template: TransactionTemplate; + if let Some(recall_height) = recall_height { + let recall_height_as_u32: u32 = + recall_height.parse::().map_err(|err| err.to_string())?; + send_transaction_template = TransactionTemplate::PayToIdWithRecall( + payment_transaction, + recall_height_as_u32, + note_type, + ); + } else { + send_transaction_template = + TransactionTemplate::PayToId(payment_transaction, note_type); + } + + let send_transaction_request = client + .build_transaction_request(send_transaction_template.clone()) + .await + .unwrap(); + let send_transaction_execution_result = + client.new_transaction(send_transaction_request).await.unwrap(); + let result = NewTransactionResult::new( + send_transaction_execution_result.executed_transaction().id().to_string(), + send_transaction_execution_result + .created_notes() + .iter() + .map(|note| note.id().to_string()) + .collect(), + ); + + client.submit_transaction(send_transaction_execution_result).await.unwrap(); + + Ok(result) + } else { + Err(JsValue::from_str("Client not initialized")) + } + } + + pub async fn new_consume_transaction( + &mut self, + account_id: String, + list_of_notes: Vec, + ) -> Result { + if let Some(client) = self.get_mut_inner() { + let account_id = AccountId::from_hex(&account_id).unwrap(); + let mut result = Vec::new(); + for note_id in list_of_notes { + match get_input_note_with_id_prefix(client, ¬e_id).await { + Ok(note_record) => result.push(note_record.id()), + Err(err) => return Err(JsValue::from_str(&err.to_string())), + } + } + + let consume_transaction_template = + TransactionTemplate::ConsumeNotes(account_id, result); + + let consume_transaction_request = client + .build_transaction_request(consume_transaction_template.clone()) + .await + .unwrap(); + let consume_transaction_execution_result = + client.new_transaction(consume_transaction_request).await.unwrap(); + let result = NewTransactionResult::new( + consume_transaction_execution_result.executed_transaction().id().to_string(), + consume_transaction_execution_result + .created_notes() + .iter() + .map(|note| note.id().to_string()) + .collect(), + ); + + client.submit_transaction(consume_transaction_execution_result).await.unwrap(); + + Ok(result) + } else { + Err(JsValue::from_str("Client not initialized")) + } + } + + pub async fn new_swap_transaction( + &mut self, + sender_account_id: String, + offered_asset_faucet_id: String, + offered_asset_amount: String, + requested_asset_faucet_id: String, + requested_asset_amount: String, + note_type: String, + ) -> Result { + if let Some(client) = self.get_mut_inner() { + let sender_account_id = AccountId::from_hex(&sender_account_id).unwrap(); + + let offered_asset_faucet_id = AccountId::from_hex(&offered_asset_faucet_id).unwrap(); + let offered_asset_amount_as_u64: u64 = + offered_asset_amount.parse::().map_err(|err| err.to_string())?; + let offered_fungible_asset = + FungibleAsset::new(offered_asset_faucet_id, offered_asset_amount_as_u64) + .map_err(|err| err.to_string())? + .into(); + + let requested_asset_faucet_id = + AccountId::from_hex(&requested_asset_faucet_id).unwrap(); + let requested_asset_amount_as_u64: u64 = + requested_asset_amount.parse::().map_err(|err| err.to_string())?; + let requested_fungible_asset = + FungibleAsset::new(requested_asset_faucet_id, requested_asset_amount_as_u64) + .map_err(|err| err.to_string())? + .into(); + + let note_type = match note_type.as_str() { + "Public" => MidenNoteType::Public, + "Private" => MidenNoteType::Private, + _ => MidenNoteType::Private, + }; + + let swap_transaction = SwapTransactionData::new( + sender_account_id, + offered_fungible_asset, + requested_fungible_asset, + ); + + let swap_transaction_template = TransactionTemplate::Swap(swap_transaction, note_type); + + let swap_transaction_request = client + .build_transaction_request(swap_transaction_template.clone()) + .await + .unwrap(); + let swap_transaction_execution_result = + client.new_transaction(swap_transaction_request.clone()).await.unwrap(); + let mut result = NewSwapTransactionResult::new( + swap_transaction_execution_result.executed_transaction().id().to_string(), + swap_transaction_request + .expected_output_notes() + .map(|note| note.id().to_string()) + .collect(), + swap_transaction_request + .expected_future_notes() + .map(|note| note.id().to_string()) + .collect(), + None, + ); + + client.submit_transaction(swap_transaction_execution_result).await.unwrap(); + + if let TransactionTemplate::Swap(swap_data, note_type) = swap_transaction_template { + let payback_note_tag_u32: u32 = build_swap_tag( + note_type, + swap_data.offered_asset().faucet_id(), + swap_data.requested_asset().faucet_id(), + ) + .map_err(|err| err.to_string())? + .into(); + + result.set_note_tag(payback_note_tag_u32.to_string()); + } + + Ok(result) + } else { + Err(JsValue::from_str("Client not initialized")) + } + } +} diff --git a/crates/web-client/src/notes.rs b/crates/web-client/src/notes.rs new file mode 100644 index 000000000..0f0e302b3 --- /dev/null +++ b/crates/web-client/src/notes.rs @@ -0,0 +1,88 @@ +use miden_client::store::{InputNoteRecord, NoteFilter, OutputNoteRecord}; +use miden_objects::{notes::NoteId, Digest}; +use serde::{Deserialize, Serialize}; +use serde_wasm_bindgen::from_value; +use wasm_bindgen::prelude::*; + +use crate::WebClient; + +#[derive(Serialize, Deserialize)] +pub enum WebClientNoteFilter { + All, + Consumed, + Committed, + Expected, + Processing, +} + +#[wasm_bindgen] +impl WebClient { + pub async fn get_input_notes(&mut self, filter: JsValue) -> Result { + if let Some(client) = self.get_mut_inner() { + let filter: WebClientNoteFilter = from_value(filter).unwrap(); + let native_filter = match filter { + WebClientNoteFilter::All => NoteFilter::All, + WebClientNoteFilter::Consumed => NoteFilter::Consumed, + WebClientNoteFilter::Committed => NoteFilter::Committed, + WebClientNoteFilter::Expected => NoteFilter::Expected, + WebClientNoteFilter::Processing => NoteFilter::Processing, + }; + + let notes: Vec = client.get_input_notes(native_filter).await.unwrap(); + let note_ids = notes.iter().map(|note| note.id().to_string()).collect::>(); + + serde_wasm_bindgen::to_value(¬e_ids).map_err(|e| JsValue::from_str(&e.to_string())) + } else { + Err(JsValue::from_str("Client not initialized")) + } + } + + pub async fn get_input_note(&mut self, note_id: String) -> Result { + if let Some(client) = self.get_mut_inner() { + let note_id: NoteId = Digest::try_from(note_id) + .map_err(|err| format!("Failed to parse input note id: {}", err))? + .into(); + let note: InputNoteRecord = client.get_input_note(note_id).await.unwrap(); + + serde_wasm_bindgen::to_value(¬e.id().to_string()) + .map_err(|e| JsValue::from_str(&e.to_string())) + } else { + Err(JsValue::from_str("Client not initialized")) + } + } + + pub async fn get_output_notes(&mut self, filter: JsValue) -> Result { + if let Some(client) = self.get_mut_inner() { + let filter: WebClientNoteFilter = from_value(filter).unwrap(); + let native_filter = match filter { + WebClientNoteFilter::All => NoteFilter::All, + WebClientNoteFilter::Consumed => NoteFilter::Consumed, + WebClientNoteFilter::Committed => NoteFilter::Committed, + WebClientNoteFilter::Expected => NoteFilter::Expected, + WebClientNoteFilter::Processing => NoteFilter::Processing, + }; + + let notes: Vec = + client.get_output_notes(native_filter).await.unwrap(); + let note_ids = notes.iter().map(|note| note.id().to_string()).collect::>(); + + serde_wasm_bindgen::to_value(¬e_ids).map_err(|e| JsValue::from_str(&e.to_string())) + } else { + Err(JsValue::from_str("Client not initialized")) + } + } + + pub async fn get_output_note(&mut self, note_id: String) -> Result { + if let Some(client) = self.get_mut_inner() { + let note_id: NoteId = Digest::try_from(note_id) + .map_err(|err| format!("Failed to parse output note id: {}", err))? + .into(); + let note: OutputNoteRecord = client.get_output_note(note_id).await.unwrap(); + + serde_wasm_bindgen::to_value(¬e.id().to_string()) + .map_err(|e| JsValue::from_str(&e.to_string())) + } else { + Err(JsValue::from_str("Client not initialized")) + } + } +} diff --git a/crates/web-client/src/sync.rs b/crates/web-client/src/sync.rs new file mode 100644 index 000000000..2adbb01ab --- /dev/null +++ b/crates/web-client/src/sync.rs @@ -0,0 +1,19 @@ +use wasm_bindgen::prelude::*; + +use crate::WebClient; + +#[wasm_bindgen] +impl WebClient { + pub async fn sync_state(&mut self, update_ignored: bool) -> Result { + if let Some(client) = self.get_mut_inner() { + let mut sync_summary = client.sync_state().await.unwrap(); + if update_ignored { + sync_summary.combine_with(&client.update_ignored_notes().await.unwrap()); + } + + Ok(JsValue::from_f64(sync_summary.block_num as f64)) + } else { + Err(JsValue::from_str("Client not initialized")) + } + } +} diff --git a/crates/web-client/src/tags.rs b/crates/web-client/src/tags.rs new file mode 100644 index 000000000..940f87da7 --- /dev/null +++ b/crates/web-client/src/tags.rs @@ -0,0 +1,43 @@ +use miden_objects::notes::NoteTag; +use wasm_bindgen::prelude::*; + +use crate::WebClient; + +#[wasm_bindgen] +impl WebClient { + pub async fn add_tag(&mut self, tag: String) -> Result { + if let Some(client) = self.get_mut_inner() { + let note_tag_as_u32 = tag.parse::().unwrap(); + let note_tag: NoteTag = note_tag_as_u32.into(); + client.add_note_tag(note_tag).await.unwrap(); + + Ok(JsValue::from_str("Okay, it worked")) + } else { + Err(JsValue::from_str("Client not initialized")) + } + } + + pub async fn remove_tag(&mut self, tag: String) -> Result { + if let Some(client) = self.get_mut_inner() { + let note_tag_as_u32 = tag.parse::().unwrap(); + let note_tag: NoteTag = note_tag_as_u32.into(); + client.remove_note_tag(note_tag).await.unwrap(); + + Ok(JsValue::from_str("Okay, it worked")) + } else { + Err(JsValue::from_str("Client not initialized")) + } + } + + pub async fn list_tags(&mut self) -> Result { + if let Some(client) = self.get_mut_inner() { + let tags: Vec = client.get_note_tags().await.unwrap(); + + // call toString() on each tag + let result = tags.iter().map(|tag| tag.to_string()).collect::>(); + serde_wasm_bindgen::to_value(&result).map_err(|e| JsValue::from_str(&e.to_string())) + } else { + Err(JsValue::from_str("Client not initialized")) + } + } +} diff --git a/crates/web-client/src/transactions.rs b/crates/web-client/src/transactions.rs new file mode 100644 index 000000000..ac694f435 --- /dev/null +++ b/crates/web-client/src/transactions.rs @@ -0,0 +1,24 @@ +use miden_client::{store::TransactionFilter, transactions::TransactionRecord}; +use wasm_bindgen::prelude::*; + +use crate::WebClient; + +#[wasm_bindgen] +impl WebClient { + pub async fn get_transactions(&mut self) -> Result { + if let Some(client) = self.get_mut_inner() { + let transactions: Vec = client + .get_transactions(TransactionFilter::All) + .await + .map_err(|e| JsValue::from_str(&format!("Error fetching transactions: {:?}", e)))?; + + let transaction_ids: Vec = + transactions.iter().map(|transaction| transaction.id.to_string()).collect(); + + serde_wasm_bindgen::to_value(&transaction_ids) + .map_err(|e| JsValue::from_str(&e.to_string())) + } else { + Err(JsValue::from_str("Client not initialized")) + } + } +} diff --git a/crates/web-client/test.html b/crates/web-client/test.html new file mode 100644 index 000000000..812a61340 --- /dev/null +++ b/crates/web-client/test.html @@ -0,0 +1,1052 @@ + + + + + WASM Example + + + + + + + + + + \ No newline at end of file diff --git a/crates/web-client/yarn.lock b/crates/web-client/yarn.lock new file mode 100644 index 000000000..dafe99195 --- /dev/null +++ b/crates/web-client/yarn.lock @@ -0,0 +1,641 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@iarna/toml@^2.2.5": + version "2.2.5" + resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c" + integrity sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@jridgewell/sourcemap-codec@^1.4.15": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@rollup/plugin-commonjs@^25.0.7": + version "25.0.8" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.8.tgz#c77e608ab112a666b7f2a6bea625c73224f7dd34" + integrity sha512-ZEZWTK5n6Qde0to4vS9Mr5x/0UZoqCxPVR9KRUjU4kA2sO7GEUn1fop0DAwpO6z0Nw/kJON9bDmSxdWxO/TT1A== + dependencies: + "@rollup/pluginutils" "^5.0.1" + commondir "^1.0.1" + estree-walker "^2.0.2" + glob "^8.0.3" + is-reference "1.2.1" + magic-string "^0.30.3" + +"@rollup/plugin-node-resolve@^15.2.3": + version "15.2.3" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz#e5e0b059bd85ca57489492f295ce88c2d4b0daf9" + integrity sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ== + dependencies: + "@rollup/pluginutils" "^5.0.1" + "@types/resolve" "1.20.2" + deepmerge "^4.2.2" + is-builtin-module "^3.2.1" + is-module "^1.0.0" + resolve "^1.22.1" + +"@rollup/pluginutils@^5.0.1", "@rollup/pluginutils@^5.0.2": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.0.tgz#7e53eddc8c7f483a4ad0b94afb1f7f5fd3c771e0" + integrity sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^2.0.2" + picomatch "^2.3.1" + +"@types/estree@*", "@types/estree@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + +"@types/resolve@1.20.2": + version "1.20.2" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" + integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== + +"@wasm-tool/rollup-plugin-rust@^2.4.5": + version "2.4.5" + resolved "https://registry.yarnpkg.com/@wasm-tool/rollup-plugin-rust/-/rollup-plugin-rust-2.4.5.tgz#203e0be9196ad278533b0996bf29c6dae5b468ec" + integrity sha512-rrgaHm/TmiOCKkt9mz8LMQMzigyn2xLHNZDtJAAv8HDrQt9QbksUkf+mYmsnQDF7gFmWtOEEJE5/7lfYuL0fEQ== + dependencies: + "@iarna/toml" "^2.2.5" + "@rollup/pluginutils" "^5.0.2" + binaryen "^111.0.0" + chalk "^4.0.0" + glob "^10.2.2" + node-fetch "^2.0.0" + rimraf "^5.0.0" + tar "^6.1.11" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +binaryen@^111.0.0: + version "111.0.0" + resolved "https://registry.yarnpkg.com/binaryen/-/binaryen-111.0.0.tgz#dd970a11d8fe61959f77d609dfee3c19ad80b80a" + integrity sha512-PEXOSHFO85aj1aP4t+KGzvxQ00qXbjCysWlsDjlGkP1e9owNiYdpEkLej21Ax8LDD7xJ01rEmJDqZ/JPoW2GXw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +builtin-modules@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" + integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +cpr@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/cpr/-/cpr-3.0.1.tgz#b9a55038b7cd81a35c17b9761895bd8496aef1e5" + integrity sha512-Xch4PXQ/KC8lJ+KfJ9JI6eG/nmppLrPPWg5Q+vh65Qr9EjuJEubxh/H/Le1TmCZ7+Xv7iJuNRqapyOFZB+wsxA== + dependencies: + graceful-fs "^4.1.5" + minimist "^1.2.0" + mkdirp "~0.5.1" + rimraf "^2.5.4" + +cross-spawn@^7.0.0: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +dexie@^4.0.1: + version "4.0.7" + resolved "https://registry.yarnpkg.com/dexie/-/dexie-4.0.7.tgz#c92e5032245fc075de58c636238a82ee3ff9fedb" + integrity sha512-M+Lo6rk4pekIfrc2T0o2tvVJwL6EAAM/B78DNfb8aaxFVoI1f8/rz5KTxuAnApkwqTSuxx7T5t0RKH7qprapGg== + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +foreground-child@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.2.1.tgz#767004ccf3a5b30df39bed90718bab43fe0a59f7" + integrity sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +glob@^10.2.2, glob@^10.3.7: + version "10.4.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.3.tgz#e0ba2253dd21b3d0acdfb5d507c59a29f513fc7a" + integrity sha512-Q38SGlYRpVtDBPSWEylRyctn7uDeTp4NQERTLiCT1FqA9JXPYWqAVmQU6qh4r/zMM5ehxTcbaO8EjhWnvEhmyg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^8.0.3: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + +graceful-fs@^4.1.5: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-builtin-module@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" + integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== + dependencies: + builtin-modules "^3.3.0" + +is-core-module@^2.13.0: + version "2.14.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.14.0.tgz#43b8ef9f46a6a08888db67b1ffd4ec9e3dfd59d1" + integrity sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A== + dependencies: + hasown "^2.0.2" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== + +is-reference@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" + integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== + dependencies: + "@types/estree" "*" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +jackspeak@^3.1.2: + version "3.4.1" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.1.tgz#145422416740568e9fc357bf60c844b3c1585f09" + integrity sha512-U23pQPDnmYybVkYjObcuYMk43VRlMLLqLI+RdZy8s8WV8WsxO9SnqSroKaluuvcNOdCAlauKszDwd+umbot5Mg== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +lru-cache@^10.2.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.0.tgz#cb29b4b2dd55b22e4a729cdb096093d7f85df02d" + integrity sha512-bfJaPTuEiTYBu+ulDaeQ0F+uLmlfFkMgXj4cbwfuMSjgObGMzb55FMMbDvbRU0fAHZ4sLGkz2mKwcMg8Dvm8Ww== + +magic-string@^0.30.3: + version "0.30.10" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e" + integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + +minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass@^3.0.0: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +mkdirp@~0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +node-fetch@^2.0.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +package-json-from-dist@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" + integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +resolve@^1.22.1: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rimraf@^2.5.4: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +rimraf@^5.0.0, rimraf@^5.0.1: + version "5.0.8" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.8.tgz#9d4d0ef5106817102b14fdbbf01cf29545e99a6c" + integrity sha512-XSh0V2/yNhDEi8HwdIefD8MLgs4LQXPag/nEJWs3YUc3Upn+UHa1GyIkEg9xSSNt7HnkO5FjTvmcRzgf+8UZuw== + dependencies: + glob "^10.3.7" + +rollup@^3.27.2: + version "3.29.4" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981" + integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw== + optionalDependencies: + fsevents "~2.3.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tar@^6.1.11: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==