use std::env::{self, temp_dir};
use std::fs::{self, File};
use std::io::Read;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::Result;
use assert_cmd::Command;
use assert_cmd::cargo::cargo_bin_cmd;
use miden_client::account::component::{
AccountComponentMetadata,
FeltSchema,
FungibleFaucet,
StorageSchema,
StorageSlotSchema,
ValueSlotSchema,
WordSchema,
};
use miden_client::account::{AccountId, AccountType, FaucetMetadata, StorageSlotName};
use miden_client::address::{Address, NetworkId};
use miden_client::assembly::CodeBuilder;
use miden_client::auth::TransactionAuthenticator;
use miden_client::builder::ClientBuilder;
use miden_client::crypto::RandomCoin;
use miden_client::keystore::Keystore;
use miden_client::note::NoteId;
use miden_client::note_transport::NOTE_TRANSPORT_TESTNET_ENDPOINT;
use miden_client::rpc::Endpoint;
use miden_client::testing::account_id::ACCOUNT_ID_PRIVATE_SENDER;
use miden_client::testing::common::{
ACCOUNT_ID_REGULAR,
FilesystemKeyStore,
TestClient,
create_test_store_path,
};
use miden_client::utils::Serializable;
use miden_client::vm::{
Package,
PackageExport,
ProcedureExport,
QualifiedProcedureName,
Section,
SectionId,
TargetType,
};
use miden_client::{self, Deserializable, Felt};
use miden_client_cli::MIDEN_DIR;
use miden_client_cli::config::{KEYSTORE_DIRECTORY, Network};
use miden_client_integration_tests::{ClientConfig, fee_funding};
use miden_client_sqlite_store::SqliteStore;
use midenc_hir_type::{CallConv, FunctionType, StructType, Type};
use predicates::prelude::PredicateBooleanExt;
use predicates::str::contains;
use rand::RngExt;
#[test]
fn init_without_params() {
let temp_dir = init_cli().1;
let mut init_cmd = cargo_bin_cmd!("miden-client");
init_cmd.args(["init", "--local"]);
init_cmd.current_dir(&temp_dir).assert().failure();
}
#[test]
fn init_with_params() {
let store_path = create_test_store_path();
let endpoint = Endpoint::devnet();
let temp_dir = init_cli_with_store_path(&store_path, &endpoint);
let mut config_path = temp_dir.clone();
config_path.push(MIDEN_DIR);
config_path.push("miden-client.toml");
let mut config_file = File::open(config_path).unwrap();
let mut config_file_str = String::new();
config_file.read_to_string(&mut config_file_str).unwrap();
assert!(config_file_str.contains(store_path.to_str().unwrap()));
assert!(config_file_str.contains("devnet"));
let mut init_cmd = cargo_bin_cmd!("miden-client");
init_cmd.args([
"init",
"--local",
"--network",
"devnet",
"--store-path",
store_path.to_str().unwrap(),
]);
init_cmd.current_dir(&temp_dir).assert().failure();
}
#[test]
fn init_rejects_invalid_remote_prover_endpoint() {
let temp_dir = temp_dir().join(format!("cli-test-{}", rand::rng().random::<u64>()));
std::fs::create_dir_all(&temp_dir).unwrap();
let mut init_cmd = cargo_bin_cmd!("miden-client");
init_cmd.args(["init", "--local", "--remote-prover-endpoint", "localhost:not-a-port"]);
init_cmd.current_dir(&temp_dir).assert().failure();
let config_path = temp_dir.join(MIDEN_DIR).join("miden-client.toml");
assert!(
!config_path.exists(),
"init should not write a config when the remote prover endpoint is invalid"
);
}
#[test]
#[serial_test::file_serial]
fn silent_initialization_uses_default_values() {
let miden_home = set_isolated_miden_home();
let temp_dir = temp_dir().join(format!("cli-test-{}", rand::rng().random::<u64>()));
std::fs::create_dir_all(&temp_dir).unwrap();
let mut account_cmd = cargo_bin_cmd!("miden-client");
account_cmd.args(["account"]);
account_cmd.current_dir(&temp_dir).assert().success();
let global_config_path = miden_home.join("miden-client.toml");
let config_content = std::fs::read_to_string(&global_config_path).unwrap();
assert!(config_content.contains("testnet"), "Should use testnet as default network");
assert!(
config_content.contains("store.sqlite3"),
"Should use default store path (relative to config file)"
);
assert!(
config_content.contains("keystore"),
"Should use default keystore directory (relative to config file)"
);
assert!(
config_content.contains("[note_transport]"),
"Silent init should write a [note_transport] section"
);
assert!(
config_content.contains(NOTE_TRANSPORT_TESTNET_ENDPOINT),
"Silent init should default note transport to the testnet endpoint"
);
assert!(
!config_content.contains(&format!("{MIDEN_DIR}/store.sqlite3")),
"Paths should be relative to config file, not include {MIDEN_DIR}/ prefix"
);
let local_config_path = temp_dir.join(MIDEN_DIR).join("miden-client.toml");
assert!(
!local_config_path.exists(),
"Should not create local config during silent initialization"
);
}
#[test]
fn miden_directory_structure_creation() {
let temp_dir = temp_dir().join(format!("cli-test-{}", rand::rng().random::<u64>()));
std::fs::create_dir_all(&temp_dir).unwrap();
let mut init_cmd = cargo_bin_cmd!("miden-client");
init_cmd.args(["init", "--local"]);
init_cmd.current_dir(&temp_dir).assert().success();
let miden_dir = temp_dir.join(MIDEN_DIR);
assert!(miden_dir.exists(), ".miden directory should be created");
assert!(miden_dir.is_dir(), ".miden should be a directory");
let config_file = miden_dir.join("miden-client.toml");
assert!(config_file.exists(), "config file should be created");
assert!(config_file.is_file(), "config should be a file");
let packages_dir = miden_dir.join("packages");
assert!(packages_dir.exists(), "packages directory should be created");
assert!(packages_dir.is_dir(), "packages should be a directory");
let basic_wallet_package = packages_dir.join("basic-wallet.masp");
assert!(basic_wallet_package.exists(), "basic-wallet package should be created");
let basic_auth_package = packages_dir.join("auth/basic-auth.masp");
assert!(basic_auth_package.exists(), "basic-auth package should be created");
let ecdsa_auth_package = packages_dir.join("auth/ecdsa-auth.masp");
assert!(ecdsa_auth_package.exists(), "ecdsa-auth package should be created");
let basic_faucet_package = packages_dir.join("basic-fungible-faucet.masp");
assert!(basic_faucet_package.exists(), "basic-fungible-faucet package should be created");
let non_fungible_faucet_package = packages_dir.join("basic-non-fungible-faucet.masp");
assert!(
non_fungible_faucet_package.exists(),
"basic-non-fungible-faucet package should be created"
);
let guarded_multisig_auth_package = packages_dir.join("auth/guarded-multisig-auth.masp");
assert!(
guarded_multisig_auth_package.exists(),
"guarded-multisig-auth package should be created"
);
let network_account_auth_package = packages_dir.join("auth/network-account-auth.masp");
assert!(
network_account_auth_package.exists(),
"network-account-auth package should be created"
);
let config_content = std::fs::read_to_string(&config_file).unwrap();
assert!(
config_content.contains("store.sqlite3"),
"Config should reference store path relative to config file"
);
assert!(
config_content.contains("keystore"),
"Config should reference keystore path relative to config file"
);
assert!(
config_content.contains("packages"),
"Config should reference packages path relative to config file"
);
assert!(
config_content.contains("token_symbol_map.toml"),
"Config should reference token symbol map path relative to config file"
);
assert!(
!config_content.contains(&format!("{MIDEN_DIR}/store.sqlite3")),
"Paths should be relative to config file, not include {MIDEN_DIR}/ prefix"
);
assert!(
config_content.contains("https://rpc.testnet.miden.io"),
"Config should have default testnet RPC endpoint"
);
let keystore_dir = miden_dir.join("keystore");
assert!(!keystore_dir.exists(), "keystore directory should not exist until first use");
let token_map_file = miden_dir.join("token_symbol_map.toml");
assert!(!token_map_file.exists(), "token symbol map should not exist until first use");
let mut account_cmd = cargo_bin_cmd!("miden-client");
account_cmd.args(["account"]);
account_cmd.current_dir(&temp_dir).assert().success();
let keystore_dir = miden_dir.join("keystore");
assert!(keystore_dir.exists(), "keystore directory should be created on first use");
assert!(keystore_dir.is_dir(), "keystore should be a directory");
}
#[test]
fn silent_initialization_does_not_override_existing_config() {
let temp_dir = temp_dir().join(format!("cli-test-{}", rand::rng().random::<u64>()));
std::fs::create_dir_all(&temp_dir).unwrap();
let miden_dir = temp_dir.join(MIDEN_DIR);
std::fs::create_dir_all(&miden_dir).unwrap();
let config_path = miden_dir.join("miden-client.toml");
let custom_config = format!(
r#"
store_filepath = "{MIDEN_DIR}/custom-store.sqlite3"
secret_keys_directory = "{MIDEN_DIR}/custom-keystore"
token_symbol_map_filepath = "{MIDEN_DIR}/custom-tokens.toml"
package_directory = "{MIDEN_DIR}/custom-templates"
[rpc]
endpoint = "https://custom-endpoint.com"
timeout_ms = 5000
[remote_prover_timeout]
secs = 20
nanos = 0
"#
);
std::fs::write(&config_path, custom_config).unwrap();
let mut account_cmd = cargo_bin_cmd!("miden-client");
account_cmd.args(["account"]);
account_cmd.current_dir(&temp_dir).assert().success();
let config_content = std::fs::read_to_string(&config_path).unwrap();
assert!(
config_content.contains("custom-endpoint.com"),
"Config should not be overwritten"
);
assert!(
config_content.contains("custom-store.sqlite3"),
"Config should not be overwritten"
);
}
#[tokio::test]
async fn mint_with_untracked_account() -> Result<()> {
let (store_path, temp_dir, endpoint) = init_cli();
let fungible_faucet_account_id = new_faucet_cli(&temp_dir, AccountType::Private);
fund_cli_account(&temp_dir, &store_path, &endpoint, &fungible_faucet_account_id).await?;
sync_cli(&temp_dir);
mint_cli(
&temp_dir,
&AccountId::try_from(ACCOUNT_ID_REGULAR).unwrap().to_hex(),
&fungible_faucet_account_id,
);
sync_until_committed_transaction(&temp_dir);
Ok(())
}
#[tokio::test]
async fn token_symbol_mapping() -> Result<()> {
let (store_path, temp_dir, endpoint) = init_cli();
let fungible_faucet_account_id = new_faucet_cli(&temp_dir, AccountType::Private);
fund_cli_account(&temp_dir, &store_path, &endpoint, &fungible_faucet_account_id).await?;
let faucet_id = AccountId::from_hex(&fungible_faucet_account_id).unwrap();
let bech32_address = Address::new(faucet_id).encode(endpoint.to_network_id());
let token_symbol_map_path = temp_dir.join(MIDEN_DIR).join("token_symbol_map.toml");
let token_symbol_map_content =
format!(r#"BTC = {{ address = "{bech32_address}", decimals = 10 }}"#);
fs::write(&token_symbol_map_path, token_symbol_map_content).unwrap();
sync_cli(&temp_dir);
let mut mint_cmd = cargo_bin_cmd!("miden-client");
mint_cmd.args([
"mint",
"--target",
AccountId::try_from(ACCOUNT_ID_REGULAR).unwrap().to_hex().as_str(),
"--asset",
"0.00001::BTC",
"-n",
"private",
"--force",
]);
let output = mint_cmd.current_dir(&temp_dir).output().unwrap();
assert!(
output.status.success(),
"token_symbol mint failed.\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let note_id = String::from_utf8(output.stdout)
.unwrap()
.split_whitespace()
.skip_while(|&word| word != "Output")
.find(|word| word.starts_with("0x"))
.unwrap()
.to_string();
let note = {
let (client, _) = create_rust_client_with_store_path(&store_path, endpoint).await?;
client.get_output_note(NoteId::try_from_hex(¬e_id)?).await?.unwrap()
};
assert_eq!(note.assets().num_assets(), 1);
assert_eq!(
note.assets().iter().next().unwrap().unwrap_fungible().amount().as_u64(),
100_000
);
Ok(())
}
#[tokio::test]
async fn public_faucet_metadata_is_fetched_and_persisted() -> Result<()> {
let (store_path, temp_dir, endpoint) = init_cli();
let wallet_account_id = new_wallet_cli(&temp_dir, AccountType::Public);
let fungible_faucet_account_id = new_faucet_cli(&temp_dir, AccountType::Public);
fund_cli_account(&temp_dir, &store_path, &endpoint, &fungible_faucet_account_id).await?;
sync_cli(&temp_dir);
let mut mint_cmd = cargo_bin_cmd!("miden-client");
mint_cmd.args([
"mint",
"--target",
wallet_account_id.as_str(),
"--asset",
format!("100::{fungible_faucet_account_id}").as_str(),
"-n",
"private",
"--force",
]);
let mint_output = mint_cmd.current_dir(&temp_dir).output().unwrap();
assert!(
mint_output.status.success(),
"mint failed.\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&mint_output.stdout),
String::from_utf8_lossy(&mint_output.stderr)
);
let note_id = String::from_utf8(mint_output.stdout)
.unwrap()
.split_whitespace()
.skip_while(|&word| word != "Output")
.find(|word| word.starts_with("0x"))
.unwrap()
.to_string();
sync_until_committed_transaction(&temp_dir);
let mut show_cmd = cargo_bin_cmd!("miden-client");
show_cmd.args(["notes", "-s", ¬e_id]);
let show_output = show_cmd.current_dir(&temp_dir).output().unwrap();
assert!(
show_output.status.success(),
"notes -s failed.\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&show_output.stdout),
String::from_utf8_lossy(&show_output.stderr)
);
let show_stdout = String::from_utf8(show_output.stdout).unwrap();
assert!(
show_stdout.contains("BTC"),
"expected `notes -s` stdout to contain `BTC` (faucet symbol fetched via RPC), got:\n{show_stdout}",
);
let faucet_id = AccountId::from_hex(&fungible_faucet_account_id).unwrap();
let (client, _) = create_rust_client_with_store_path(&store_path, endpoint).await?;
let setting_key = format!("faucet_metadata:{}", faucet_id.to_hex());
let stored: Option<FaucetMetadata> = client.get_setting(setting_key).await?;
assert!(
stored.is_some(),
"expected settings store to contain metadata for {fungible_faucet_account_id} after notes -s",
);
let stored = stored.unwrap();
assert_eq!(stored.symbol, "BTC");
assert_eq!(stored.decimals, 10);
Ok(())
}
#[tokio::test]
async fn show_untracked_public_account() -> Result<()> {
let (store_path_a, temp_dir_a, endpoint) = init_cli();
let fungible_faucet_account_id = new_faucet_cli(&temp_dir_a, AccountType::Public);
fund_cli_account(&temp_dir_a, &store_path_a, &endpoint, &fungible_faucet_account_id).await?;
sync_cli(&temp_dir_a);
mint_cli(
&temp_dir_a,
&AccountId::try_from(ACCOUNT_ID_REGULAR).unwrap().to_hex(),
&fungible_faucet_account_id,
);
sync_until_committed_transaction(&temp_dir_a);
let store_path_b = create_test_store_path();
let temp_dir_b = init_cli_with_store_path(&store_path_b, &endpoint);
let mut show_cmd = cargo_bin_cmd!("miden-client");
show_cmd.args(["account", "--show", &fungible_faucet_account_id]);
show_cmd
.current_dir(&temp_dir_b)
.assert()
.success()
.stdout(contains("Fetching from the network"))
.stdout(contains("Fungible faucet (token symbol: BTC)"));
Ok(())
}
#[test]
fn account_inspect_resolves_procedure_names() {
let temp_dir = init_cli().1;
let account_id = new_wallet_cli(&temp_dir, AccountType::Private);
let mut inspect_cmd = cargo_bin_cmd!("miden-client");
inspect_cmd.args(["account", "--inspect", &account_id]);
inspect_cmd
.current_dir(&temp_dir)
.assert()
.success()
.stdout(contains("MAST Root"))
.stdout(contains("Package"))
.stdout(contains("receive_asset"))
.stdout(contains("auth_tx"))
.stdout(contains("fn([felt; 4])"));
}
#[test]
fn account_inspect_single_procedure() {
let temp_dir = init_cli().1;
let account_id = new_wallet_cli(&temp_dir, AccountType::Private);
let mut existing_cmd = cargo_bin_cmd!("miden-client");
existing_cmd.args(["account", "--inspect", &format!("{account_id}:receive_asset")]);
existing_cmd
.current_dir(&temp_dir)
.assert()
.success()
.stdout(contains("receive_asset"))
.stdout(contains("move_asset_to_note").not());
let mut missing_cmd = cargo_bin_cmd!("miden-client");
missing_cmd.args(["account", "--inspect", &format!("{account_id}:does_not_exist")]);
missing_cmd
.current_dir(&temp_dir)
.assert()
.failure()
.stderr(contains("no procedure named `does_not_exist` could be resolved"));
}
#[test]
fn account_inspect_verbose_prints_disassembly() {
let temp_dir = init_cli().1;
let account_id = new_wallet_cli(&temp_dir, AccountType::Private);
let mut inspect_cmd = cargo_bin_cmd!("miden-client");
inspect_cmd.args(["account", "--inspect", &format!("{account_id}:receive_asset"), "--verbose"]);
let assert = inspect_cmd.current_dir(&temp_dir).assert().success();
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
assert_eq!(
stdout.matches("\nProcedure ").count(),
1,
"exactly one procedure should be disassembled, got:\n{stdout}"
);
let header = stdout.find("Procedure receive_asset").expect("procedure header is printed");
let body = &stdout[header..];
assert!(
body.contains("begin") && body.contains("end"),
"the disassembly should follow the receive_asset header, got:\n{stdout}"
);
}
#[test]
fn account_inspect_without_packages_prints_roots() {
let temp_dir = init_cli().1;
let account_id = new_wallet_cli(&temp_dir, AccountType::Private);
let packages_dir = temp_dir.join(MIDEN_DIR).join("packages");
fs::remove_dir_all(&packages_dir).unwrap();
let mut inspect_cmd = cargo_bin_cmd!("miden-client");
inspect_cmd.args(["account", "--inspect", &account_id]);
inspect_cmd
.current_dir(&temp_dir)
.assert()
.success()
.stdout(contains("Unresolved"))
.stdout(contains("0x"));
}
#[test]
fn account_inspect_resolves_from_explicit_package() {
let temp_dir = init_cli().1;
let account_id = new_wallet_cli(&temp_dir, AccountType::Private);
let packages_dir = temp_dir.join(MIDEN_DIR).join("packages");
let auth_package = temp_dir.join("basic-auth.masp");
fs::copy(packages_dir.join("auth/basic-auth.masp"), &auth_package).unwrap();
fs::remove_dir_all(&packages_dir).unwrap();
let mut inspect_cmd = cargo_bin_cmd!("miden-client");
inspect_cmd.args([
"account",
"--inspect",
&account_id,
"--package",
auth_package.to_str().unwrap(),
]);
inspect_cmd
.current_dir(&temp_dir)
.assert()
.success()
.stdout(contains("auth_tx"))
.stdout(contains("fn([felt; 4])"))
.stdout(contains("Unresolved"));
}
#[test]
fn account_inspect_flags_require_inspect() {
let temp_dir = init_cli().1;
let mut verbose_cmd = cargo_bin_cmd!("miden-client");
verbose_cmd.args(["account", "--verbose"]);
verbose_cmd.current_dir(&temp_dir).assert().failure();
let mut package_cmd = cargo_bin_cmd!("miden-client");
package_cmd.args(["account", "--package", "some.masp"]);
package_cmd.current_dir(&temp_dir).assert().failure();
}
const GENESIS_ACCOUNTS_FILENAMES: [&str; 1] = ["account.mac"];
#[tokio::test]
#[ignore = "import genesis test gets ignored by default so integration tests can be ran with dockerized and remote nodes where we might not have the genesis data"]
async fn import_genesis_accounts_can_be_used_for_transactions() -> Result<()> {
let (store_path, temp_dir, endpoint) = init_cli();
for genesis_account_filename in GENESIS_ACCOUNTS_FILENAMES {
let mut new_file_path = temp_dir.clone();
new_file_path.push(genesis_account_filename);
let cargo_workspace_dir =
env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is not set");
let source_path = format!("{cargo_workspace_dir}/../../data/{genesis_account_filename}");
std::fs::copy(source_path, new_file_path).unwrap();
}
let mut args = vec!["import"];
for filename in GENESIS_ACCOUNTS_FILENAMES {
args.push(filename);
}
let mut import_cmd = cargo_bin_cmd!("miden-client");
import_cmd.args(&args);
import_cmd.current_dir(&temp_dir).assert().success();
sync_cli(&temp_dir);
let fungible_faucet_account_id = {
let (client, _) = create_rust_client_with_store_path(&store_path, endpoint).await?;
let accounts = client.get_account_headers().await?;
let mut faucet_accounts = Vec::new();
for (account_header, _) in accounts {
if let Some(account) = client.get_account(account_header.id()).await?
&& FungibleFaucet::try_from(&account).is_ok()
{
faucet_accounts.push(account.id());
}
}
assert_eq!(faucet_accounts.len(), 1);
faucet_accounts[0].to_hex()
};
let args = vec!["account", "--show", &fungible_faucet_account_id];
let mut show_cmd = cargo_bin_cmd!("miden-client");
show_cmd.args(&args);
show_cmd.current_dir(&temp_dir).assert().success();
mint_cli(
&temp_dir,
&AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap().to_hex(),
&fungible_faucet_account_id,
);
sync_until_committed_transaction(&temp_dir);
Ok(())
}
#[tokio::test]
async fn cli_export_import_note() -> Result<()> {
const NOTE_FILENAME: &str = "test_note.mno";
let (store_path_1, temp_dir_1, endpoint_1) = init_cli();
let (store_path_2, temp_dir_2, endpoint_2) = init_cli();
let first_basic_account_id = new_wallet_cli(&temp_dir_2, AccountType::Private);
fund_cli_account(&temp_dir_2, &store_path_2, &endpoint_2, &first_basic_account_id).await?;
let fungible_faucet_account_id = new_faucet_cli(&temp_dir_1, AccountType::Private);
fund_cli_account(&temp_dir_1, &store_path_1, &endpoint_1, &fungible_faucet_account_id).await?;
sync_cli(&temp_dir_1);
let note_to_export_id =
mint_cli(&temp_dir_1, &first_basic_account_id, &fungible_faucet_account_id);
let mut export_cmd = cargo_bin_cmd!("miden-client");
export_cmd.args(["export", ¬e_to_export_id, "--filename", NOTE_FILENAME]);
export_cmd.current_dir(&temp_dir_1).assert().failure().code(1);
let mut export_cmd = cargo_bin_cmd!("miden-client");
export_cmd.args([
"export",
¬e_to_export_id,
"--filename",
NOTE_FILENAME,
"--export-type",
"partial",
]);
export_cmd.current_dir(&temp_dir_1).assert().success();
let mut client_1_note_file_path = temp_dir_1.clone();
client_1_note_file_path.push(NOTE_FILENAME);
let mut client_2_note_file_path = temp_dir_2.clone();
client_2_note_file_path.push(NOTE_FILENAME);
std::fs::copy(client_1_note_file_path, client_2_note_file_path).unwrap();
let mut import_cmd = cargo_bin_cmd!("miden-client");
import_cmd.args(["import", NOTE_FILENAME]);
import_cmd.current_dir(&temp_dir_2).assert().success();
sync_until_committed_note(&temp_dir_2);
show_note_cli(&temp_dir_2, ¬e_to_export_id, false);
consume_note_cli(&temp_dir_2, &first_basic_account_id, &[¬e_to_export_id]);
let mock_target_id: AccountId = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
transfer_cli(
&temp_dir_2,
&first_basic_account_id,
&mock_target_id.to_hex(),
&fungible_faucet_account_id,
);
Ok(())
}
#[tokio::test]
async fn cli_export_import_account() -> Result<()> {
const FAUCET_FILENAME: &str = "test_faucet.mac";
const WALLET_FILENAME: &str = "test_wallet.wal";
let (store_path_1, temp_dir_1, endpoint_1) = init_cli();
let (store_path_2, temp_dir_2, endpoint_2) = init_cli();
let faucet_id = new_faucet_cli(&temp_dir_1, AccountType::Private);
fund_cli_account(&temp_dir_1, &store_path_1, &endpoint_1, &faucet_id).await?;
let wallet_id = new_wallet_cli(&temp_dir_1, AccountType::Private);
fund_cli_account(&temp_dir_1, &store_path_1, &endpoint_1, &wallet_id).await?;
let mut export_cmd = cargo_bin_cmd!("miden-client");
export_cmd.args(["export", &faucet_id, "--account", "--filename", FAUCET_FILENAME]);
export_cmd.current_dir(&temp_dir_1).assert().success();
let mut export_cmd = cargo_bin_cmd!("miden-client");
export_cmd.args(["export", &wallet_id, "--account", "--filename", WALLET_FILENAME]);
export_cmd.current_dir(&temp_dir_1).assert().success();
for filename in &[FAUCET_FILENAME, WALLET_FILENAME] {
let mut client_1_file_path = temp_dir_1.clone();
client_1_file_path.push(filename);
let mut client_2_file_path = temp_dir_2.clone();
client_2_file_path.push(filename);
std::fs::copy(client_1_file_path, client_2_file_path).unwrap();
}
let mut import_cmd = cargo_bin_cmd!("miden-client");
import_cmd.args(["import", FAUCET_FILENAME]);
import_cmd.current_dir(&temp_dir_2).assert().success();
let mut import_cmd = cargo_bin_cmd!("miden-client");
import_cmd.args(["import", WALLET_FILENAME]);
import_cmd.current_dir(&temp_dir_2).assert().success();
let (client_2, _) = create_rust_client_with_store_path(&store_path_2, endpoint_2).await?;
let cli_keystore =
FilesystemKeyStore::new(temp_dir_2.clone().join(MIDEN_DIR).join("keystore"))?;
assert!(client_2.get_account(AccountId::from_hex(&faucet_id)?).await.is_ok());
assert!(client_2.get_account(AccountId::from_hex(&wallet_id)?).await.is_ok());
sync_cli(&temp_dir_2);
let note_id = mint_cli(&temp_dir_2, &wallet_id, &faucet_id);
sync_until_committed_note(&temp_dir_2);
consume_note_cli(&temp_dir_2, &wallet_id, &[¬e_id]);
let faucet_pks = cli_keystore
.get_account_key_commitments(&AccountId::from_hex(&faucet_id)?)
.await?;
for stored_pk_commitment in faucet_pks {
let matching_secret_key = cli_keystore.get_key_sync(stored_pk_commitment).unwrap();
assert!(matching_secret_key.is_some());
assert_eq!(matching_secret_key.unwrap().public_key().to_commitment(), stored_pk_commitment);
let public_key = cli_keystore.get_public_key(stored_pk_commitment).await;
assert!(public_key.is_some());
assert_eq!(public_key.unwrap().to_commitment(), stored_pk_commitment);
}
let wallet_pks = cli_keystore
.get_account_key_commitments(&AccountId::from_hex(&wallet_id)?)
.await?;
for stored_pk_commitment in wallet_pks {
let matching_secret_key = cli_keystore.get_key_sync(stored_pk_commitment).unwrap();
assert!(matching_secret_key.is_some());
assert_eq!(matching_secret_key.unwrap().public_key().to_commitment(), stored_pk_commitment);
let public_key = cli_keystore.get_public_key(stored_pk_commitment).await;
assert!(public_key.is_some());
assert_eq!(public_key.unwrap().to_commitment(), stored_pk_commitment);
}
Ok(())
}
#[test]
fn cli_empty_commands() {
let temp_dir = init_cli().1;
let mut create_faucet_cmd = cargo_bin_cmd!("miden-client");
assert_command_fails_but_does_not_panic(
create_faucet_cmd.args(["new-account"]).current_dir(&temp_dir),
);
let mut import_cmd = cargo_bin_cmd!("miden-client");
assert_command_fails_but_does_not_panic(import_cmd.args(["export"]).current_dir(&temp_dir));
let mut mint_cmd = cargo_bin_cmd!("miden-client");
assert_command_fails_but_does_not_panic(mint_cmd.args(["mint"]).current_dir(&temp_dir));
let mut transfer_cmd = cargo_bin_cmd!("miden-client");
assert_command_fails_but_does_not_panic(transfer_cmd.args(["transfer"]).current_dir(&temp_dir));
let mut swam_cmd = cargo_bin_cmd!("miden-client");
assert_command_fails_but_does_not_panic(swam_cmd.args(["swap"]).current_dir(&temp_dir));
let mut pswap_cmd = cargo_bin_cmd!("miden-client");
assert_command_fails_but_does_not_panic(pswap_cmd.args(["pswap"]).current_dir(&temp_dir));
let mut pswap_create_cmd = cargo_bin_cmd!("miden-client");
assert_command_fails_but_does_not_panic(
pswap_create_cmd.args(["pswap", "create"]).current_dir(&temp_dir),
);
let mut pswap_consume_cmd = cargo_bin_cmd!("miden-client");
assert_command_fails_but_does_not_panic(
pswap_consume_cmd.args(["pswap", "consume"]).current_dir(&temp_dir),
);
let mut pswap_cancel_cmd = cargo_bin_cmd!("miden-client");
assert_command_fails_but_does_not_panic(
pswap_cancel_cmd.args(["pswap", "cancel"]).current_dir(&temp_dir),
);
let mut cmd = cargo_bin_cmd!("miden-client");
assert_command_fails_but_does_not_panic(cmd.args(["pswap", "unknown"]).current_dir(&temp_dir));
}
#[test]
fn pswap_cli_help_output() {
let temp_dir = init_cli().1;
let mut cmd = cargo_bin_cmd!("miden-client");
let output = cmd.args(["pswap", "--help"]).current_dir(&temp_dir).output().unwrap();
assert!(output.status.success(), "pswap --help should succeed");
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("create"), "Help should list 'create' subcommand");
assert!(stdout.contains("consume"), "Help should list 'consume' subcommand");
assert!(stdout.contains("cancel"), "Help should list 'cancel' subcommand");
let mut cmd = cargo_bin_cmd!("miden-client");
let output = cmd.args(["pswap", "create", "--help"]).current_dir(&temp_dir).output().unwrap();
assert!(output.status.success(), "pswap create --help should succeed");
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("--sender"), "Help should show --sender flag");
assert!(stdout.contains("--offered-asset"), "Help should show --offered-asset flag");
assert!(stdout.contains("--requested-asset"), "Help should show --requested-asset flag");
assert!(stdout.contains("--note-type"), "Help should show --note-type flag");
let mut cmd = cargo_bin_cmd!("miden-client");
let output = cmd
.args(["pswap", "consume", "--help"])
.current_dir(&temp_dir)
.output()
.unwrap();
assert!(output.status.success(), "pswap consume --help should succeed");
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("--account"), "Help should show --account flag");
assert!(stdout.contains("--fill-amount"), "Help should show --fill-amount flag");
}
#[test]
fn pswap_cli_invalid_args() {
let temp_dir = init_cli().1;
let mut cmd = cargo_bin_cmd!("miden-client");
assert_command_fails_but_does_not_panic(
cmd.args([
"pswap",
"create",
"--sender",
"0xaabbccdd",
"--offered-asset",
"100::0x1111111111111111",
"--note-type",
"public",
])
.current_dir(&temp_dir),
);
let mut cmd = cargo_bin_cmd!("miden-client");
assert_command_fails_but_does_not_panic(
cmd.args([
"pswap",
"create",
"--sender",
"0xaabbccdd",
"--offered-asset",
"100::0x1111111111111111",
"--requested-asset",
"50::0x2222222222222222",
"--note-type",
"invalid",
])
.current_dir(&temp_dir),
);
let mut cmd = cargo_bin_cmd!("miden-client");
assert_command_fails_but_does_not_panic(
cmd.args([
"pswap",
"consume",
"--account",
"0xaabbccdd",
"--note",
"0xdeadbeef",
"--fill-amount",
"not_a_number",
])
.current_dir(&temp_dir),
);
}
#[tokio::test]
async fn consume_unauthenticated_note() -> Result<()> {
let (store_path, temp_dir, endpoint) = init_cli();
let wallet_account_id = new_wallet_cli(&temp_dir, AccountType::Public);
fund_cli_account(&temp_dir, &store_path, &endpoint, &wallet_account_id).await?;
let fungible_faucet_account_id = new_faucet_cli(&temp_dir, AccountType::Public);
fund_cli_account(&temp_dir, &store_path, &endpoint, &fungible_faucet_account_id).await?;
sync_cli(&temp_dir);
let note_id = mint_cli(&temp_dir, &wallet_account_id, &fungible_faucet_account_id);
sync_until_committed_transaction(&temp_dir);
consume_note_cli(&temp_dir, &wallet_account_id, &[¬e_id]);
Ok(())
}
#[tokio::test]
async fn init_with_devnet() -> Result<()> {
let store_path = create_test_store_path();
let endpoint = Endpoint::devnet();
let temp_dir = init_cli_with_store_path(&store_path, &endpoint);
let mut config_path = temp_dir.clone();
config_path.push(MIDEN_DIR);
config_path.push("miden-client.toml");
let mut config_file = File::open(config_path).unwrap();
let mut config_file_str = String::new();
config_file.read_to_string(&mut config_file_str).unwrap();
assert!(config_file_str.contains(&Endpoint::devnet().to_string()));
Ok(())
}
#[tokio::test]
async fn init_with_testnet() -> Result<()> {
let store_path = create_test_store_path();
let endpoint = Endpoint::testnet();
let temp_dir = init_cli_with_store_path(&store_path, &endpoint);
let mut config_path = temp_dir.clone();
config_path.push(MIDEN_DIR);
config_path.push("miden-client.toml");
let mut config_file = File::open(config_path).unwrap();
let mut config_file_str = String::new();
config_file.read_to_string(&mut config_file_str).unwrap();
assert!(config_file_str.contains(&Endpoint::testnet().to_string()));
Ok(())
}
#[tokio::test]
async fn list_addresses_add() -> Result<()> {
let temp_dir = init_cli().1;
let basic_account_id = new_wallet_cli(&temp_dir, AccountType::Private);
sync_cli(&temp_dir);
let mut list_addresses_cmd = cargo_bin_cmd!("miden-client");
list_addresses_cmd.args(["address", "list", &basic_account_id]);
let output = list_addresses_cmd.current_dir(temp_dir.clone()).output().unwrap();
assert!(output.status.success());
let formatted_output = String::from_utf8(output.stdout).unwrap();
assert!(formatted_output.contains(&basic_account_id));
assert!(formatted_output.contains("Unspecified"));
assert!(!formatted_output.contains("BasicWallet"));
let encoded_address =
encode_address_cli(&temp_dir, &basic_account_id, "basic-wallet", Some("10"));
let mut add_address_cmd = cargo_bin_cmd!("miden-client");
add_address_cmd.args(["address", "add", &basic_account_id, &encoded_address]);
let output = add_address_cmd.current_dir(temp_dir.clone()).output().unwrap();
assert!(output.status.success());
sync_cli(&temp_dir);
let output = list_addresses_cmd.current_dir(temp_dir.clone()).output().unwrap();
assert!(output.status.success());
let formatted_output = String::from_utf8(output.stdout).unwrap();
assert!(formatted_output.contains(&basic_account_id));
assert_eq!(formatted_output.matches("Unspecified").count(), 1);
assert_eq!(formatted_output.matches("BasicWallet").count(), 1);
let encoded_address =
encode_address_cli(&temp_dir, &basic_account_id, "basic-wallet", Some("5"));
let mut add_address_cmd = cargo_bin_cmd!("miden-client");
add_address_cmd.args(["address", "add", &basic_account_id, &encoded_address]);
let output = add_address_cmd.current_dir(temp_dir.clone()).output().unwrap();
assert!(output.status.success());
sync_cli(&temp_dir);
let output = list_addresses_cmd.current_dir(temp_dir.clone()).output().unwrap();
assert!(output.status.success());
let formatted_output = String::from_utf8(output.stdout).unwrap();
assert!(formatted_output.contains(&basic_account_id));
assert_eq!(formatted_output.matches("Unspecified").count(), 1);
assert_eq!(formatted_output.matches("BasicWallet").count(), 2);
Ok(())
}
#[tokio::test]
async fn address_add_rejects_mismatched_account() -> Result<()> {
let temp_dir = init_cli().1;
let account_a = new_wallet_cli(&temp_dir, AccountType::Private);
let account_b = new_wallet_cli(&temp_dir, AccountType::Private);
assert_ne!(account_a, account_b, "two new wallets should have distinct ids");
sync_cli(&temp_dir);
let encoded_for_a = encode_address_cli(&temp_dir, &account_a, "basic-wallet", None);
let mut add_cmd = cargo_bin_cmd!("miden-client");
add_cmd.args(["address", "add", &account_b, &encoded_for_a]);
let output = add_cmd.current_dir(temp_dir.clone()).output().unwrap();
assert!(!output.status.success(), "expected add to fail on account mismatch");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("does not match the provided account ID"),
"unexpected stderr: {stderr}"
);
Ok(())
}
#[tokio::test]
async fn address_add_rejects_mismatched_network() -> Result<()> {
let temp_dir = init_cli().1;
let account = new_wallet_cli(&temp_dir, AccountType::Private);
sync_cli(&temp_dir);
let encoded_local = encode_address_cli(&temp_dir, &account, "basic-wallet", None);
let (cli_network_id, address) = Address::decode(&encoded_local)?;
let other_network_id = if cli_network_id == NetworkId::Mainnet {
NetworkId::Testnet
} else {
NetworkId::Mainnet
};
let encoded_other = address.encode(other_network_id);
let mut add_cmd = cargo_bin_cmd!("miden-client");
add_cmd.args(["address", "add", &account, &encoded_other]);
let output = add_cmd.current_dir(temp_dir.clone()).output().unwrap();
assert!(!output.status.success(), "expected add to fail on network mismatch");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("does not match configured network"),
"unexpected stderr: {stderr}"
);
Ok(())
}
#[tokio::test]
async fn list_addresses_remove() -> Result<()> {
let temp_dir = init_cli().1;
let basic_account_id = new_wallet_cli(&temp_dir, AccountType::Private);
sync_cli(&temp_dir);
let mut list_addresses_cmd = cargo_bin_cmd!("miden-client");
list_addresses_cmd.args(["address", "list", &basic_account_id]);
let output = list_addresses_cmd.current_dir(temp_dir.clone()).output().unwrap();
assert!(output.status.success());
let formatted_output = String::from_utf8(output.stdout).unwrap();
assert!(formatted_output.contains(&basic_account_id));
assert_eq!(formatted_output.matches("Unspecified").count(), 1);
let mut remove_address_cmd = cargo_bin_cmd!("miden-client");
let unspecified_wallet_address = regex::Regex::new(r"m[a-z]{1,4}1[0-9a-z]+")
.unwrap()
.find(&formatted_output)
.unwrap()
.as_str();
remove_address_cmd.args(["address", "remove", &basic_account_id, unspecified_wallet_address]);
let output = remove_address_cmd.current_dir(temp_dir.clone()).output().unwrap();
assert!(output.status.success());
sync_cli(&temp_dir);
let output = list_addresses_cmd.current_dir(temp_dir.clone()).output().unwrap();
assert!(output.status.success());
let formatted_output = String::from_utf8(output.stdout).unwrap();
assert!(formatted_output.contains(&basic_account_id));
assert_eq!(formatted_output.matches("Unspecified").count(), 0);
Ok(())
}
fn init_cli() -> (PathBuf, PathBuf, Endpoint) {
let network: Network = std::env::var("TEST_MIDEN_NETWORK")
.unwrap_or_else(|_| "localhost".to_string())
.parse()
.unwrap();
let endpoint = Endpoint::try_from(network.to_rpc_endpoint().as_str()).unwrap();
let store_path = create_test_store_path();
let temp_dir = init_cli_with_store_path(&store_path, &endpoint);
(store_path, temp_dir, endpoint)
}
fn init_cli_with_store_path(store_path: &Path, endpoint: &Endpoint) -> PathBuf {
let temp_dir = temp_dir().join(format!("cli-test-{}", rand::rng().random::<u64>()));
std::fs::create_dir_all(&temp_dir).unwrap();
let mut init_cmd = cargo_bin_cmd!("miden-client");
init_cmd.args([
"init",
"--local", "--network",
endpoint.to_string().as_str(),
"--store-path",
store_path.to_str().unwrap(),
]);
init_cmd.current_dir(&temp_dir).assert().success();
temp_dir
}
fn set_isolated_miden_home() -> PathBuf {
let path = temp_dir().join(format!("miden-home-{}", rand::rng().random::<u64>()));
std::fs::create_dir_all(&path).unwrap();
unsafe {
env::set_var("MIDEN_CLIENT_HOME", &path);
}
path
}
struct SyncResult {
committed_notes: u64,
committed_transactions: u64,
}
fn sync_cli(cli_path: &Path) -> SyncResult {
loop {
let mut sync_cmd = cargo_bin_cmd!("miden-client");
sync_cmd.args(["sync"]);
let output = sync_cmd.current_dir(cli_path).output().unwrap();
if output.status.success() {
let stdout = String::from_utf8(output.stdout).unwrap();
let committed_notes = stdout
.lines()
.find_map(|line| {
line.strip_prefix("Committed notes: ")
.and_then(|rest| rest.trim().parse::<u64>().ok())
})
.unwrap();
let committed_transactions = stdout
.lines()
.find_map(|line| {
line.strip_prefix("Committed transactions: ")
.and_then(|rest| rest.trim().parse::<u64>().ok())
})
.unwrap();
return SyncResult { committed_notes, committed_transactions };
}
std::thread::sleep(std::time::Duration::from_secs(3));
}
}
fn mint_cli(cli_path: &Path, target_account_id: &str, faucet_id: &str) -> String {
let mut mint_cmd = cargo_bin_cmd!("miden-client");
mint_cmd.args([
"mint",
"--target",
target_account_id,
"--asset",
&format!("100::{faucet_id}"),
"-n",
"private",
"--force",
]);
let output = mint_cmd.current_dir(cli_path).output().unwrap();
assert!(
output.status.success(),
"mint_cli failed.\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout)
.unwrap()
.split_whitespace()
.skip_while(|&word| word != "Output")
.find(|word| word.starts_with("0x"))
.unwrap()
.to_string()
}
fn show_note_cli(cli_path: &Path, note_id: &str, should_fail: bool) {
let mut show_note_cmd = cargo_bin_cmd!("miden-client");
show_note_cmd.args(["notes", "--show", note_id]);
if should_fail {
show_note_cmd.current_dir(cli_path).assert().failure();
} else {
show_note_cmd.current_dir(cli_path).assert().success();
}
}
fn transfer_cli(cli_path: &Path, from_account_id: &str, to_account_id: &str, faucet_id: &str) {
let mut transfer_cmd = cargo_bin_cmd!("miden-client");
transfer_cmd.args([
"transfer",
"--sender",
from_account_id,
"--target",
to_account_id,
"--asset",
&format!("25::{faucet_id}"),
"-n",
"private",
"--force",
]);
transfer_cmd.current_dir(cli_path).assert().success();
}
fn sync_until_committed_note(cli_path: &Path) {
while sync_cli(cli_path).committed_notes == 0 {
std::thread::sleep(std::time::Duration::from_secs(1));
}
}
fn sync_until_committed_transaction(cli_path: &Path) {
while sync_cli(cli_path).committed_transactions == 0 {
std::thread::sleep(std::time::Duration::from_secs(1));
}
}
fn consume_note_cli(cli_path: &Path, account_id: &str, note_ids: &[&str]) {
let mut consume_note_cmd = cargo_bin_cmd!("miden-client");
let mut cli_args = vec!["consume-notes", "--account", &account_id, "--force"];
cli_args.extend_from_slice(note_ids);
consume_note_cmd.args(&cli_args);
consume_note_cmd.current_dir(cli_path).assert().success();
}
fn new_faucet_cli(cli_path: &Path, visibility: AccountType) -> String {
const INIT_DATA_FILENAME: &str = "init_data.toml";
let mut create_faucet_cmd = cargo_bin_cmd!("miden-client");
let init_storage_data_toml = r#"
[fungible-faucet-metadata]
symbol = "BTC"
decimals = 10
max_supply = 10000000
"#;
let file_path = cli_path.join(INIT_DATA_FILENAME);
fs::write(&file_path, init_storage_data_toml).unwrap();
create_faucet_cmd.args([
"new-account",
"-t",
visibility.to_string().as_str(),
"-p",
"basic-fungible-faucet",
"-p",
"basic-wallet",
"-i",
INIT_DATA_FILENAME,
]);
create_faucet_cmd.current_dir(cli_path).assert().success();
let output = create_faucet_cmd.current_dir(cli_path).output().unwrap();
assert!(output.status.success());
std::str::from_utf8(&output.stdout)
.unwrap()
.split_whitespace()
.skip_while(|&word| word != "-s")
.nth(1)
.unwrap()
.to_string()
}
fn new_wallet_cli(cli_path: &Path, visibility: AccountType) -> String {
let mut create_wallet_cmd = cargo_bin_cmd!("miden-client");
create_wallet_cmd.args(["new-wallet", "-t", visibility.to_string().as_str()]);
let output = create_wallet_cmd.current_dir(cli_path).output().unwrap();
assert!(
output.status.success(),
"Failed to create wallet {}",
String::from_utf8(output.stderr)
.map_or(". Also failed to access the Command's stderr".to_string(), |err_msg| format!(
"with error: {err_msg}"
))
);
std::str::from_utf8(&output.stdout)
.unwrap()
.split_whitespace()
.skip_while(|&word| word != "-s")
.nth(1)
.unwrap()
.to_string()
}
fn encode_address_cli(
cli_path: &Path,
account_id: &str,
interface: &str,
tag_len: Option<&str>,
) -> String {
let mut encode_cmd = cargo_bin_cmd!("miden-client");
let mut args = vec!["address", "encode", account_id, interface];
if let Some(tag_len) = tag_len {
args.push(tag_len);
}
encode_cmd.args(args);
let output = encode_cmd.current_dir(cli_path).output().unwrap();
assert!(
output.status.success(),
"address encode failed.\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
String::from_utf8(output.stdout).unwrap().trim().to_string()
}
async fn create_rust_client_with_store_path(
store_path: &Path,
endpoint: Endpoint,
) -> Result<(TestClient, FilesystemKeyStore)> {
create_rust_client(store_path, &temp_dir(), endpoint).await
}
async fn create_rust_client_with_cli_keystore(
store_path: &Path,
cli_path: &Path,
endpoint: Endpoint,
) -> Result<(TestClient, FilesystemKeyStore)> {
let keystore_dir = cli_path.join(MIDEN_DIR).join(KEYSTORE_DIRECTORY);
create_rust_client(store_path, &keystore_dir, endpoint).await
}
async fn create_rust_client(
store_path: &Path,
keystore_path: &Path,
endpoint: Endpoint,
) -> Result<(TestClient, FilesystemKeyStore)> {
let store = {
let sqlite_store = SqliteStore::new(PathBuf::from(store_path)).await?;
std::sync::Arc::new(sqlite_store)
};
let mut rng = rand::rng();
let coin_seed: [u64; 4] = rng.random();
let rng = Box::new(RandomCoin::new(coin_seed.map(Felt::new_unchecked).into()));
let keystore = FilesystemKeyStore::new(keystore_path.to_path_buf())?;
let client = ClientBuilder::new()
.grpc_client(&endpoint, Some(10_000))
.rng(rng)
.store(store)
.authenticator(Arc::new(keystore.clone()))
.build()
.await?;
Ok((TestClient::from(client), keystore))
}
fn block_on<F: std::future::Future>(future: F) -> F::Output {
tokio::runtime::Runtime::new()
.expect("failed to build a runtime")
.block_on(future)
}
async fn fund_cli_account(
cli_path: &Path,
store_path: &Path,
endpoint: &Endpoint,
account_id: &str,
) -> Result<()> {
let mut client = cli_funding_client(cli_path, store_path, endpoint).await?;
client.deploy_account(AccountId::from_hex(account_id)?).await
}
async fn cli_funding_client(
cli_path: &Path,
store_path: &Path,
endpoint: &Endpoint,
) -> Result<TestClient> {
let fee_funder = fee_funding::load(
&ClientConfig::new(endpoint.clone(), 10_000),
fee_funding::funders_path_from_env().as_deref(),
)?;
let (client, _) =
create_rust_client_with_cli_keystore(store_path, cli_path, endpoint.clone()).await?;
let mut client = client.with_fee_funder(fee_funder);
client.sync_state().await?;
Ok(client)
}
fn assert_command_fails_but_does_not_panic(command: &mut Command) {
let output_error = command.ok().unwrap_err();
let exit_code = output_error.as_output().unwrap().status.code().unwrap();
assert_ne!(exit_code, 0); assert_ne!(exit_code, 101); }
#[test]
fn exec_parse() {
let failure_script =
fs::canonicalize("tests/files/test_cli_advice_inputs_expect_failure.masm").unwrap();
let success_script =
fs::canonicalize("tests/files/test_cli_advice_inputs_expect_success.masm").unwrap();
let toml_path = fs::canonicalize("tests/files/test_cli_advice_inputs_input.toml").unwrap();
let temp_dir = init_cli().1;
let basic_account_id = new_wallet_cli(&temp_dir, AccountType::Private);
sync_cli(&temp_dir);
let mut success_cmd = cargo_bin_cmd!("miden-client");
success_cmd.args([
"exec",
"-s",
success_script.to_str().unwrap(),
"-a",
&basic_account_id,
"-i",
toml_path.to_str().unwrap(),
]);
success_cmd.current_dir(&temp_dir).assert().success();
let mut failure_cmd = cargo_bin_cmd!("miden-client");
failure_cmd.args([
"exec",
"-s",
failure_script.to_str().unwrap(),
"-a",
&basic_account_id,
"-i",
toml_path.to_str().unwrap(),
]);
failure_cmd.current_dir(&temp_dir).assert().failure();
}
#[test]
fn call_empty_command() {
let temp_dir = init_cli().1;
let mut cmd = cargo_bin_cmd!("miden-client");
assert_command_fails_but_does_not_panic(cmd.args(["call"]).current_dir(&temp_dir));
}
#[test]
fn call_nonexistent_package() {
let temp_dir = init_cli().1;
let basic_account_id = new_wallet_cli(&temp_dir, AccountType::Private);
let mut cmd = cargo_bin_cmd!("miden-client");
cmd.args([
"call",
&format!("{basic_account_id}:some_procedure"),
"--package",
"nonexistent/path/package.masp",
]);
cmd.current_dir(&temp_dir).assert().failure();
}
#[test]
fn call_nonexistent_procedure() {
let temp_dir = init_cli().1;
let basic_account_id = new_wallet_cli(&temp_dir, AccountType::Private);
let package_path = temp_dir.join(MIDEN_DIR).join("packages/basic-wallet.masp");
sync_cli(&temp_dir);
let mut cmd = cargo_bin_cmd!("miden-client");
cmd.args([
"call",
&format!("{basic_account_id}:nonexistent_procedure"),
"--package",
package_path.to_str().unwrap(),
]);
cmd.current_dir(&temp_dir).assert().failure();
}
fn call_test_exports(package: &Package) -> Vec<PackageExport> {
let account_id = Type::Struct(Arc::new(StructType::named(
Arc::from("miden:base/core-types@1.0.0/account-id"),
[
(Arc::<str>::from("prefix"), Type::Felt),
(Arc::<str>::from("suffix"), Type::Felt),
],
)));
let signature_overrides: [(&str, FunctionType); 6] = [
(
"add",
FunctionType::new(CallConv::ComponentModel, [Type::Felt, Type::Felt], [Type::Felt]),
),
(
"set_value",
FunctionType::new(
CallConv::ComponentModel,
[Type::Felt, Type::Felt, Type::Felt, Type::Felt],
[],
),
),
("read_advice", FunctionType::new(CallConv::ComponentModel, [], [Type::Felt])),
(
"wide_result",
FunctionType::new(CallConv::ComponentModel, [], vec![Type::Felt; 17]),
),
(
"take_account_id",
FunctionType::new(CallConv::ComponentModel, [account_id.clone()], [account_id.clone()]),
),
(
"account_id_suffix",
FunctionType::new(CallConv::ComponentModel, [account_id.clone()], [Type::Felt]),
),
];
let mut exports = Vec::new();
for module_descriptor in package.module_descriptors() {
for (_, proc_info) in module_descriptor.procedures() {
let name =
QualifiedProcedureName::new(module_descriptor.path(), proc_info.name.clone());
let override_sig = signature_overrides
.iter()
.find(|(n, _)| *n == proc_info.name.as_str())
.map(|(_, sig)| sig.clone());
exports.push(PackageExport::Procedure(ProcedureExport {
path: name.into_inner(),
node: None,
source_node: None,
digest: proc_info.digest,
signature: override_sig.or_else(|| proc_info.signature.as_deref().cloned()),
attributes: proc_info.attributes.clone(),
}));
}
}
exports
}
fn build_call_test_masp(out_path: &Path) {
let call_test_code = r#"
use miden::protocol::native_account
use miden::core::word
use miden::core::sys
const STORED_VALUE = word("miden::testing::call_test::stored_value")
@account_procedure
pub proc add
add
end
@account_procedure
pub proc set_value
push.STORED_VALUE[0..2]
exec.native_account::set_item
dropw
exec.sys::truncate_stack
end
@account_procedure
pub proc wide_result
exec.sys::truncate_stack
end
@account_procedure
pub proc read_advice
# Look up a fixed key in the advice map and return the sum of its two values.
push.268435456.0.0.0
adv.push_mapval
dropw
adv_push adv_push
add
exec.sys::truncate_stack
end
@account_procedure
pub proc take_account_id
# Identity over the two felts of an account id, so the typed decoder can be checked
# against the value that was encoded.
nop
end
@account_procedure
pub proc account_id_suffix
# Drops the prefix and returns the suffix, so a swapped field order cannot pass
# unnoticed the way it does through the identity above.
drop
end
@account_procedure
pub proc raw_add
# Left out of `signature_overrides`, so the package describes no WIT types for it and
# `call` has to fall back to raw field elements.
add
end
"#;
let component_package: Package = CodeBuilder::default()
.compile_component_code("miden::testing::call_test", call_test_code)
.expect("failed to compile call-test component")
.into();
let slot_name =
StorageSlotName::new("miden::testing::call_test::stored_value").expect("valid slot name");
let word_schema = WordSchema::new_value([
FeltSchema::new_void(),
FeltSchema::new_void(),
FeltSchema::new_void(),
FeltSchema::new_void(),
]);
let storage_schema = StorageSchema::new([(
slot_name,
StorageSlotSchema::Value(ValueSlotSchema::new(None, word_schema)),
)])
.expect("valid storage schema");
let metadata = AccountComponentMetadata::new("call-test").with_storage_schema(storage_schema);
let exports = call_test_exports(&component_package);
let modules = component_package.module_descriptors().map(|module_info| {
miden_mast_package::PackageModule::new(
std::sync::Arc::from(module_info.path().to_path_buf().into_boxed_path()),
module_info
.submodules()
.iter()
.map(|submodule| miden_mast_package::PackageSubmodule::new(submodule.name.clone())),
)
});
let section = Section::new(SectionId::ACCOUNT_COMPONENT_METADATA, metadata.to_bytes());
let mut package = Package::create_with_modules(
metadata.name().to_string().into(),
metadata.version().clone(),
TargetType::AccountComponent,
component_package.mast_forest().clone(),
exports,
modules,
[],
)
.expect("failed to create call-test package");
package.description = Some(metadata.description().to_string());
package.sections = vec![section];
fs::write(out_path, package.to_bytes()).expect("failed to write call-test .masp");
}
fn setup_call_test_account() -> (PathBuf, String, PathBuf) {
let (store_path, temp_dir, endpoint) = init_cli();
let masp_dst = temp_dir.join("call_test.masp");
build_call_test_masp(&masp_dst);
let init_toml = r#"
"miden::testing::call_test::stored_value" = "0x0000000000000000000000000000000000000000000000000000000000000000"
"#;
let init_path = temp_dir.join("call_test_init.toml");
fs::write(&init_path, init_toml).unwrap();
let mut create_cmd = cargo_bin_cmd!("miden-client");
create_cmd.args([
"new-account",
"-t",
"public",
"-p",
"auth/no-auth",
"-p",
"basic-wallet",
"-p",
masp_dst.to_str().unwrap(),
"-i",
init_path.to_str().unwrap(),
]);
let output = create_cmd.current_dir(&temp_dir).output().unwrap();
assert!(
output.status.success(),
"Failed to create account: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let account_id = stdout
.split_whitespace()
.skip_while(|&w| w != "-s")
.nth(1)
.expect("Could not parse account ID from new-account output")
.to_string();
sync_cli(&temp_dir);
block_on(fund_cli_account(&temp_dir, &store_path, &endpoint, &account_id))
.expect("failed to fund the call-test account");
(temp_dir, account_id, masp_dst)
}
fn procedure_digest_hex(masp_path: &Path, procedure: &str) -> String {
let bytes = fs::read(masp_path).expect("failed to read call-test package");
let package = Package::read_from_bytes(&bytes).expect("failed to parse call-test package");
package
.manifest
.exports()
.find_map(|export| match export {
PackageExport::Procedure(proc)
if export.name() == procedure
&& proc
.signature
.as_ref()
.is_some_and(|sig| sig.abi.is_wasm_canonical_abi()) =>
{
Some(proc.digest.to_hex())
},
_ => None,
})
.unwrap_or_else(|| panic!("no ComponentModel export named '{procedure}'"))
}
#[test]
fn call_by_digest_without_package() {
let (temp_dir, account_id, masp_path) = setup_call_test_account();
let digest = procedure_digest_hex(&masp_path, "add");
let mut cmd = cargo_bin_cmd!("miden-client");
cmd.args(["call", &format!("{account_id}:{digest}"), "3", "7"]);
let output = cmd.current_dir(&temp_dir).output().unwrap();
assert!(
output.status.success(),
"Call by digest failed.\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("No `--package` provided; output will be raw felts."),
"Expected the raw-felts notice in output:\n{stdout}"
);
assert!(
stdout.contains("\nResult: 10\n"),
"Expected `add(3, 7)` to leave 10 on top of the stack:\n{stdout}"
);
}
#[test]
fn call_without_package_rejects_procedure_name() {
let (temp_dir, account_id, _masp_path) = setup_call_test_account();
let mut cmd = cargo_bin_cmd!("miden-client");
cmd.args(["call", &format!("{account_id}:add"), "3", "7"]);
cmd.current_dir(&temp_dir)
.assert()
.failure()
.stderr(contains("'add' is not a hex digest"));
}
#[test]
fn call_procedure_by_name() {
let (temp_dir, account_id, masp_path) = setup_call_test_account();
let mut cmd = cargo_bin_cmd!("miden-client");
cmd.args([
"call",
&format!("{account_id}:add"),
"3",
"7",
"--package",
masp_path.to_str().unwrap(),
]);
cmd.current_dir(&temp_dir).assert().success();
}
#[test]
fn call_shows_nonce_delta() {
let (temp_dir, account_id, masp_path) = setup_call_test_account();
let mut cmd = cargo_bin_cmd!("miden-client");
cmd.args([
"call",
&format!("{account_id}:add"),
"1",
"2",
"--package",
masp_path.to_str().unwrap(),
]);
let output = cmd.current_dir(&temp_dir).output().unwrap();
assert!(
output.status.success(),
"Call failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("New account nonce:"),
"Expected the new account nonce in output:\n{stdout}"
);
}
#[test]
fn call_set_value_shows_storage_delta() {
let (temp_dir, account_id, masp_path) = setup_call_test_account();
let mut cmd = cargo_bin_cmd!("miden-client");
cmd.args([
"call",
&format!("{account_id}:set_value"),
"42",
"0",
"0",
"0",
"--package",
masp_path.to_str().unwrap(),
]);
let output = cmd.current_dir(&temp_dir).output().unwrap();
assert!(
output.status.success(),
"Call failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Storage Slot"), "Expected storage delta in output:\n{stdout}");
}
#[test]
fn call_with_advice_inputs() {
let (temp_dir, account_id, masp_path) = setup_call_test_account();
let advice_path = fs::canonicalize("tests/files/test_cli_advice_inputs_input.toml").unwrap();
let mut cmd = cargo_bin_cmd!("miden-client");
cmd.args([
"call",
&format!("{account_id}:read_advice"),
"--package",
masp_path.to_str().unwrap(),
"-i",
advice_path.to_str().unwrap(),
]);
let output = cmd.current_dir(&temp_dir).output().unwrap();
assert!(
output.status.success(),
"Call with advice inputs failed.\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(output_line(&stdout, "Result:"), "Result: 22felt");
}
fn output_line<'a>(stdout: &'a str, prefix: &str) -> &'a str {
let mut matching = stdout.lines().filter(|line| line.starts_with(prefix));
let line = matching
.next()
.unwrap_or_else(|| panic!("no line starts with `{prefix}`:\n{stdout}"));
assert!(
matching.next().is_none(),
"more than one line starts with `{prefix}`:\n{stdout}"
);
line
}
#[test]
fn call_typed_account_id_roundtrip() {
let (temp_dir, account_id, masp_path) = setup_call_test_account();
let acct_hex = "0xaa0000000000bb110000cc000000dd";
let mut cmd = cargo_bin_cmd!("miden-client");
cmd.args([
"call",
&format!("{account_id}:take_account_id"),
acct_hex,
"--package",
masp_path.to_str().unwrap(),
]);
let output = cmd.current_dir(&temp_dir).output().unwrap();
assert!(
output.status.success(),
"Call failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(
output_line(&stdout, "Signature:"),
"Signature: take_account_id(account-id) -> account-id"
);
assert_eq!(output_line(&stdout, "Result:"), format!("Result: account-id({acct_hex})"));
}
#[test]
fn call_untyped_procedure_falls_back_to_raw_felts() {
let (temp_dir, account_id, masp_path) = setup_call_test_account();
let mut cmd = cargo_bin_cmd!("miden-client");
cmd.args([
"call",
&format!("{account_id}:raw_add"),
"3",
"7",
"--package",
masp_path.to_str().unwrap(),
]);
let output = cmd.current_dir(&temp_dir).output().unwrap();
assert!(
output.status.success(),
"Call failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(output_line(&stdout, "Signature:"), "Signature: raw_add(...) [no type info]");
assert_eq!(output_line(&stdout, "Result:"), "Result: 10");
}
#[test]
fn call_untyped_procedure_rejects_a_hex_argument() {
let (temp_dir, account_id, masp_path) = setup_call_test_account();
let mut cmd = cargo_bin_cmd!("miden-client");
cmd.args([
"call",
&format!("{account_id}:raw_add"),
"0xff",
"7",
"--package",
masp_path.to_str().unwrap(),
]);
let output = cmd.current_dir(&temp_dir).output().unwrap();
assert!(!output.status.success(), "Expected failure for a hex argument");
assert!(
String::from_utf8_lossy(&output.stderr)
.contains("Invalid argument '0xff'. Expected a felt."),
"Unexpected stderr:\n{}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn call_typed_account_id_field_order() {
let (temp_dir, account_id, masp_path) = setup_call_test_account();
let acct_hex = "0xaa0000000000bb110000cc000000dd";
let suffix = AccountId::from_hex(acct_hex).unwrap().suffix();
let mut cmd = cargo_bin_cmd!("miden-client");
cmd.args([
"call",
&format!("{account_id}:account_id_suffix"),
acct_hex,
"--package",
masp_path.to_str().unwrap(),
]);
let output = cmd.current_dir(&temp_dir).output().unwrap();
assert!(
output.status.success(),
"Call failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(
output_line(&stdout, "Signature:"),
"Signature: account_id_suffix(account-id) -> felt"
);
assert_eq!(output_line(&stdout, "Result:"), format!("Result: {suffix}felt"));
}
#[test]
fn call_rejects_wrong_arg_count() {
let (temp_dir, account_id, masp_path) = setup_call_test_account();
let mut too_few = cargo_bin_cmd!("miden-client");
too_few.args([
"call",
&format!("{account_id}:add"),
"3",
"--package",
masp_path.to_str().unwrap(),
]);
let out = too_few.current_dir(&temp_dir).output().unwrap();
assert!(!out.status.success(), "Expected failure for too-few args");
let stderr = String::from_utf8_lossy(&out.stderr);
assert_eq!(output_line(&stderr, " ×"), " × procedure 'add' expects 2 argument(s), got 1");
let mut too_many = cargo_bin_cmd!("miden-client");
too_many.args([
"call",
&format!("{account_id}:add"),
"3",
"7",
"11",
"--package",
masp_path.to_str().unwrap(),
]);
let out = too_many.current_dir(&temp_dir).output().unwrap();
assert!(!out.status.success(), "Expected failure for too-many args");
let stderr = String::from_utf8_lossy(&out.stderr);
assert_eq!(output_line(&stderr, " ×"), " × procedure 'add' expects 2 argument(s), got 3");
}
#[test]
fn call_rejects_more_args_than_stack_window() {
let (temp_dir, account_id, masp_path) = setup_call_test_account();
let digest = procedure_digest_hex(&masp_path, "add");
let mut args = vec!["call".to_string(), format!("{account_id}:{digest}")];
args.extend((0..17).map(|value| value.to_string()));
let mut cmd = cargo_bin_cmd!("miden-client");
cmd.args(&args);
cmd.current_dir(&temp_dir)
.assert()
.failure()
.stderr(contains("takes at most 16 input values; got 17"));
}
#[test]
fn call_rejects_results_wider_than_stack_window() {
let (temp_dir, account_id, masp_path) = setup_call_test_account();
let mut cmd = cargo_bin_cmd!("miden-client");
cmd.args([
"call",
&format!("{account_id}:wide_result"),
"--package",
masp_path.to_str().unwrap(),
]);
cmd.current_dir(&temp_dir)
.assert()
.failure()
.stderr(contains("returns 17 values"));
}
fn setup_remote_call_test() -> (PathBuf, String, PathBuf) {
let (target_store_path, target_dir, endpoint) = init_cli();
let masp_path = target_dir.join("call_test.masp");
build_call_test_masp(&masp_path);
let init_toml = r#"
"miden::testing::call_test::stored_value" = "0x0000000000000000000000000000000000000000000000000000000000000000"
"#;
let init_path = target_dir.join("call_test_init.toml");
fs::write(&init_path, init_toml).unwrap();
sync_cli(&target_dir);
let mut create_cmd = cargo_bin_cmd!("miden-client");
create_cmd.args([
"new-account",
"-t",
"public",
"-p",
"auth/no-auth",
"-p",
"basic-wallet",
"-p",
masp_path.to_str().unwrap(),
"-i",
init_path.to_str().unwrap(),
]);
let output = create_cmd.current_dir(&target_dir).output().unwrap();
assert!(
output.status.success(),
"Failed to create account: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let account_id = stdout
.split_whitespace()
.skip_while(|&w| w != "-s")
.nth(1)
.expect("Could not parse account ID from new-account output")
.to_string();
block_on(async {
let mut client = cli_funding_client(&target_dir, &target_store_path, &endpoint).await?;
client.deploy_account(AccountId::from_hex(&account_id)?).await
})
.expect("failed to deploy the call-test account");
sync_cli(&target_dir);
let caller_dir = init_cli().1;
new_wallet_cli(&caller_dir, AccountType::Private);
sync_cli(&caller_dir);
(caller_dir, account_id, masp_path)
}
#[test]
fn call_remote_account_via_fpi() {
let (caller_dir, account_id, masp_path) = setup_remote_call_test();
let mut cmd = cargo_bin_cmd!("miden-client");
cmd.args([
"call",
&format!("{account_id}:add"),
"3",
"7",
"--package",
masp_path.to_str().unwrap(),
]);
let output = cmd.current_dir(&caller_dir).output().unwrap();
assert!(
output.status.success(),
"Remote call failed.\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("reading its state from the network"),
"Expected the network-read message in output:\n{stdout}"
);
assert!(
stdout.contains("Result: 10"),
"Expected `add(3, 7)` result in output:\n{stdout}"
);
}
#[test]
fn call_remote_account_rejects_state_change() {
let (caller_dir, account_id, masp_path) = setup_remote_call_test();
let mut cmd = cargo_bin_cmd!("miden-client");
cmd.args([
"call",
&format!("{account_id}:set_value"),
"42",
"0",
"0",
"0",
"--package",
masp_path.to_str().unwrap(),
]);
cmd.current_dir(&caller_dir)
.assert()
.failure()
.stderr(contains("the active account is not"));
}
#[test]
fn call_rejects_untracked_private_account() {
let owner_dir = init_cli().1;
let target_id = new_wallet_cli(&owner_dir, AccountType::Private);
let caller_dir = init_cli().1;
new_wallet_cli(&caller_dir, AccountType::Private);
sync_cli(&caller_dir);
let digest = format!("0x{}", "0".repeat(64));
let mut cmd = cargo_bin_cmd!("miden-client");
cmd.args(["call", &format!("{target_id}:{digest}")]);
cmd.current_dir(&caller_dir)
.assert()
.failure()
.stderr(contains("its state isn't public"));
}
#[test]
fn call_remote_account_requires_local_executor() {
let (_caller_dir, account_id, masp_path) = setup_remote_call_test();
let empty_dir = init_cli().1;
sync_cli(&empty_dir);
let mut cmd = cargo_bin_cmd!("miden-client");
cmd.args([
"call",
&format!("{account_id}:add"),
"3",
"7",
"--package",
masp_path.to_str().unwrap(),
]);
cmd.current_dir(&empty_dir)
.assert()
.failure()
.stderr(contains("of your own accounts to run the call from"));
}
#[test]
fn create_account_with_no_auth() {
let temp_dir = init_cli().1;
let mut create_account_cmd = cargo_bin_cmd!("miden-client");
create_account_cmd.args([
"new-account",
"-t",
"private",
"-p",
"basic-wallet",
"-p",
"auth/no-auth",
]);
create_account_cmd.current_dir(&temp_dir).assert().success();
}
#[test]
fn create_account_with_multisig_auth() {
let temp_dir = init_cli().1;
let init_storage_data_toml = r#"
"miden::standards::auth::multisig::threshold_config.threshold" = "2"
"miden::standards::auth::multisig::threshold_config.num_approvers" = "3"
"miden::standards::auth::multisig::approver_public_keys" = [
{ key = ["0", "0", "0", "0"], value = "0x0000000000000000000000000000000000000000000000000000000000000001" },
{ key = ["1", "0", "0", "0"], value = "0x0000000000000000000000000000000000000000000000000000000000000002" },
{ key = ["2", "0", "0", "0"], value = "0x0000000000000000000000000000000000000000000000000000000000000003" }
]
"miden::standards::auth::multisig::approver_schemes" = [
{ key = ["0", "0", "0", "0"], value = ["2", "0", "0", "0"] },
{ key = ["1", "0", "0", "0"], value = ["2", "0", "0", "0"] },
{ key = ["2", "0", "0", "0"], value = ["2", "0", "0", "0"] }
]
"miden::standards::auth::multisig::procedure_thresholds" = [
{ key = "0xd2d1b6229d7cfb9f2ada31c5cb61453cf464f91828e124437c708eec55b9cd07", value = "1" }
]
"#;
let file_path = temp_dir.join("multisig_init_data.toml");
fs::write(&file_path, init_storage_data_toml).unwrap();
let mut create_account_cmd = cargo_bin_cmd!("miden-client");
create_account_cmd.args([
"new-account",
"-t",
"private",
"-p",
"basic-wallet",
"-p",
"auth/multisig-auth",
"-i",
"multisig_init_data.toml",
]);
create_account_cmd.current_dir(&temp_dir).assert().success();
}
#[test]
fn create_account_with_ecdsa_auth() {
let temp_dir = init_cli().1;
let init_storage_data_toml = r#"
"miden::standards::auth::singlesig::pub_key" = "0x0000000000000000000000000000000000000000000000000000000000000001"
"miden::standards::auth::singlesig::scheme" = "EcdsaK256Keccak"
"#;
let file_path = temp_dir.join("ecdsa_init_data.toml");
fs::write(&file_path, init_storage_data_toml).unwrap();
let mut create_account_cmd = cargo_bin_cmd!("miden-client");
create_account_cmd.args([
"new-account",
"-t",
"private",
"-p",
"basic-wallet",
"-p",
"auth/ecdsa-auth",
"-i",
"ecdsa_init_data.toml",
]);
create_account_cmd.current_dir(&temp_dir).assert().success();
}
#[tokio::test]
#[serial_test::file_serial]
async fn test_new_with_local_config() -> Result<()> {
let (store_path, temp_dir, _endpoint) = init_cli();
let _miden_home = set_isolated_miden_home();
let original_dir = env::current_dir().unwrap();
env::set_current_dir(&temp_dir)?;
let client_result = miden_client_cli::CliClient::new().await;
env::set_current_dir(original_dir)?;
assert!(
client_result.is_ok(),
"Failed to create client from local config: {:?}",
client_result.err()
);
assert!(
store_path.exists(),
"Local store file should exist at {store_path:?}, indicating local config was used"
);
Ok(())
}
#[tokio::test]
#[serial_test::file_serial]
async fn test_new_silent_init() -> Result<()> {
let temp_dir = temp_dir().join(format!("cli-test-silent-init-{}", rand::rng().random::<u64>()));
std::fs::create_dir_all(&temp_dir)?;
let miden_home = set_isolated_miden_home();
let global_config_path = miden_home.join("miden-client.toml");
assert!(!global_config_path.exists(), "Global config should not exist before test");
let original_dir = env::current_dir().unwrap();
env::set_current_dir(&temp_dir)?;
let client_result = miden_client_cli::CliClient::new().await;
env::set_current_dir(original_dir)?;
assert!(
client_result.is_ok(),
"Expected client to be created via silent initialization, but got error: {:?}",
client_result.err()
);
assert!(
global_config_path.exists(),
"Expected global config to be created at {global_config_path:?} by silent initialization"
);
Ok(())
}
#[tokio::test]
#[serial_test::file_serial]
async fn test_load_local_priority() -> Result<()> {
let _miden_home = set_isolated_miden_home();
let global_store_path = create_test_store_path();
let global_endpoint = Endpoint::testnet();
let temp_dir_for_global =
temp_dir().join(format!("cli-test-global-init-{}", rand::rng().random::<u64>()));
std::fs::create_dir_all(&temp_dir_for_global)?;
let mut init_global_cmd = cargo_bin_cmd!("miden-client");
init_global_cmd.args([
"init",
"--network",
global_endpoint.to_string().as_str(),
"--store-path",
global_store_path.to_str().unwrap(),
]);
init_global_cmd.current_dir(&temp_dir_for_global).assert().success();
let local_store_path = create_test_store_path();
let local_endpoint = Endpoint::localhost();
let local_temp_dir = init_cli_with_store_path(&local_store_path, &local_endpoint);
let local_miden_dir = local_temp_dir.join(MIDEN_DIR);
let config = miden_client_cli::CliConfig::from_dir(&local_miden_dir)?;
let client = miden_client_cli::CliClient::from_config(config).await;
assert!(client.is_ok(), "Failed to create client with local config: {:?}", client.err());
assert!(
local_store_path.exists(),
"Local store file should exist at {local_store_path:?}, indicating local config was used"
);
assert!(
!global_store_path.exists(),
"Global store file should NOT exist at {global_store_path:?}, as global config should not have been used"
);
Ok(())
}