use std::env::{self, temp_dir};
use std::fs::{self, File};
use std::io::{Read, Write};
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::{AccountId, AccountStorageMode};
use miden_client::address::AddressInterface;
use miden_client::auth::{RPO_FALCON_SCHEME_ID, TransactionAuthenticator};
use miden_client::builder::ClientBuilder;
use miden_client::crypto::{FeltRng, RandomCoin};
use miden_client::keystore::Keystore;
use miden_client::note::{
Note,
NoteAssets,
NoteFile,
NoteId,
NoteMetadata,
NoteRecipient,
NoteStorage,
NoteTag,
NoteType,
};
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,
create_test_store_path,
execute_tx_and_sync,
insert_new_wallet,
};
use miden_client::transaction::TransactionRequestBuilder;
use miden_client::utils::Serializable;
use miden_client::{self, Client, DebugMode, Felt};
use miden_client_cli::MIDEN_DIR;
use miden_client_cli::config::Network;
use miden_client_sqlite_store::SqliteStore;
use predicates::str::contains;
use rand::Rng;
#[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]
#[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 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 temp_dir = init_cli().1;
let fungible_faucet_account_id = new_faucet_cli(&temp_dir, AccountStorageMode::Private);
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, AccountStorageMode::Private);
let token_symbol_map_path = temp_dir.join(MIDEN_DIR).join("token_symbol_map.toml");
let token_symbol_map_content =
format!(r#"BTC = {{ id = "{fungible_faucet_account_id}", 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(), 100_000);
Ok(())
}
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 account_ids = accounts.iter().map(|(acc, _seed)| acc.id()).collect::<Vec<_>>();
let faucet_accounts = account_ids.iter().filter(|id| id.is_faucet()).collect::<Vec<_>>();
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 temp_dir_1 = init_cli().1;
let temp_dir_2 = init_cli().1;
let first_basic_account_id = new_wallet_cli(&temp_dir_2, AccountStorageMode::Private);
let fungible_faucet_account_id = new_faucet_cli(&temp_dir_1, AccountStorageMode::Private);
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();
send_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 (_, temp_dir_1, _) = init_cli();
let (store_path_2, temp_dir_2, endpoint_2) = init_cli();
let faucet_id = new_faucet_cli(&temp_dir_1, AccountStorageMode::Private);
let wallet_id = new_wallet_cli(&temp_dir_1, AccountStorageMode::Private);
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 send_cmd = cargo_bin_cmd!("miden-client");
assert_command_fails_but_does_not_panic(send_cmd.args(["send"]).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));
}
#[tokio::test]
async fn consume_unauthenticated_note() -> Result<()> {
let temp_dir = init_cli().1;
let wallet_account_id = new_wallet_cli(&temp_dir, AccountStorageMode::Public);
let fungible_faucet_account_id = new_faucet_cli(&temp_dir, AccountStorageMode::Public);
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]
#[serial_test::file_serial]
async fn debug_mode_outputs_logs() -> Result<()> {
const NOTE_FILENAME: &str = "test_note.mno";
unsafe {
env::set_var("MIDEN_DEBUG", "true");
}
let (store_path, _, endpoint) = init_cli();
let (mut client, authenticator) =
create_rust_client_with_store_path(&store_path, endpoint).await?;
let (account, ..) = insert_new_wallet(
&mut client,
AccountStorageMode::Private,
&authenticator,
RPO_FALCON_SCHEME_ID,
)
.await?;
let note_script = "
@note_script
pub proc main
debug.stack
assert_eq
end
";
let note_script = client.code_builder().compile_note_script(note_script).unwrap();
let inputs = NoteStorage::new(vec![]).unwrap();
let serial_num = client.rng().draw_word();
let note_metadata = NoteMetadata::new(account.id(), NoteType::Private)
.with_tag(NoteTag::with_account_target(account.id()));
let note_assets = NoteAssets::new(vec![]).unwrap();
let note_recipient = NoteRecipient::new(serial_num, note_script, inputs);
let note = Note::new(note_assets, note_metadata, note_recipient);
client.sync_state().await?;
let transaction_request =
TransactionRequestBuilder::new().own_output_notes(vec![note.clone()]).build()?;
execute_tx_and_sync(&mut client, account.id(), transaction_request).await?;
let note_file: NoteFile = NoteFile::NoteDetails {
details: note.clone().into(),
after_block_num: 0.into(),
tag: Some(note.metadata().tag()),
};
let (_, temp_dir, _) = init_cli();
let note_path = temp_dir.join(NOTE_FILENAME);
let mut file = File::create(note_path.clone()).unwrap();
file.write_all(¬e_file.to_bytes()).unwrap();
let mut import_cmd = cargo_bin_cmd!("miden-client");
import_cmd.args(["import", note_path.to_str().unwrap()]);
import_cmd.current_dir(&temp_dir).assert().success();
sync_cli(&temp_dir);
let wallet_account_id = new_wallet_cli(&temp_dir, AccountStorageMode::Private);
let mut consume_note_cmd = cargo_bin_cmd!("miden-client");
let note_id = note.id().to_hex();
let mut cli_args = vec!["consume-notes", "--account", &wallet_account_id, "--force"];
cli_args.extend_from_slice(vec![note_id.as_str()].as_slice());
consume_note_cmd.args(&cli_args);
consume_note_cmd
.current_dir(&temp_dir)
.assert()
.success()
.stdout(contains("Stack state"));
unsafe {
env::remove_var("MIDEN_DEBUG");
}
Ok(())
}
#[tokio::test]
async fn list_addresses_add() -> Result<()> {
let temp_dir = init_cli().1;
let basic_account_id = new_wallet_cli(&temp_dir, AccountStorageMode::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 mut add_address_cmd = cargo_bin_cmd!("miden-client");
let custom_note_tag_len = "10";
add_address_cmd.args([
"address",
"add",
&basic_account_id,
&AddressInterface::BasicWallet.to_string(),
custom_note_tag_len,
]);
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 mut add_address_cmd = cargo_bin_cmd!("miden-client");
let custom_note_tag_len = "5";
add_address_cmd.args([
"address",
"add",
&basic_account_id,
&AddressInterface::BasicWallet.to_string(),
custom_note_tag_len,
]);
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 list_addresses_remove() -> Result<()> {
let temp_dir = init_cli().1;
let basic_account_id = new_wallet_cli(&temp_dir, AccountStorageMode::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(())
}
#[tokio::test]
async fn new_wallet_with_deploy_flag() -> Result<()> {
let (store_path, temp_dir, endpoint) = init_cli();
sync_cli(&temp_dir);
let mut create_wallet_cmd = cargo_bin_cmd!("miden-client");
create_wallet_cmd.args(["new-wallet", "-s", "public", "--deploy"]);
let output = create_wallet_cmd.current_dir(&temp_dir).output().unwrap();
assert!(
output.status.success(),
"Failed to create and deploy wallet: {}",
String::from_utf8(output.stderr).unwrap()
);
let output_str = std::str::from_utf8(&output.stdout).unwrap();
let account_id_str = output_str
.split_whitespace()
.skip_while(|&word| word != "-s")
.nth(1)
.expect("Failed to extract account ID from output");
sync_cli(&temp_dir);
let (client, _) = create_rust_client_with_store_path(&store_path, endpoint).await?;
let account_id = AccountId::from_hex(account_id_str)?;
let nonce = client.account_reader(account_id).nonce().await?;
assert!(
nonce.as_canonical_u64() > 0,
"Account nonce should be non-zero after deployment, but got: {nonce}"
);
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.env("MIDEN_DEBUG", "true");
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 send_cli(cli_path: &Path, from_account_id: &str, to_account_id: &str, faucet_id: &str) {
let mut send_cmd = cargo_bin_cmd!("miden-client");
send_cmd.args([
"send",
"--sender",
from_account_id,
"--target",
to_account_id,
"--asset",
&format!("25::{faucet_id}"),
"-n",
"private",
"--force",
]);
send_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, storage_mode: AccountStorageMode) -> 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#"
["miden::standards::fungible_faucets::metadata"]
decimals="10"
max_supply="10000000"
symbol="BTC"
"#;
let file_path = cli_path.join(INIT_DATA_FILENAME);
fs::write(&file_path, init_storage_data_toml).unwrap();
create_faucet_cmd.args([
"new-account",
"-s",
storage_mode.to_string().as_str(),
"--account-type",
"fungible-faucet",
"-p",
"basic-fungible-faucet",
"-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, storage_mode: AccountStorageMode) -> String {
let mut create_wallet_cmd = cargo_bin_cmd!("miden-client");
create_wallet_cmd.args(["new-wallet", "-s", storage_mode.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()
}
pub type TestClient = Client<FilesystemKeyStore>;
async fn create_rust_client_with_store_path(
store_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).into()));
let keystore = FilesystemKeyStore::new(temp_dir())?;
let client = ClientBuilder::new()
.grpc_client(&endpoint, Some(10_000))
.rng(rng)
.store(store)
.authenticator(Arc::new(keystore.clone()))
.in_debug_mode(DebugMode::Enabled)
.build()
.await?;
Ok((client, keystore))
}
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, AccountStorageMode::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 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",
"-s",
"private",
"--account-type",
"regular-account-updatable-code",
"-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",
"-s",
"private",
"--account-type",
"regular-account-updatable-code",
"-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_acl_auth() {
let temp_dir = init_cli().1;
let init_storage_data_toml = r#"
"miden::standards::auth::singlesig_acl::pub_key" = "0x0000000000000000000000000000000000000000000000000000000000000001"
"miden::standards::auth::singlesig_acl::scheme" = "Falcon512Poseidon2"
"miden::standards::auth::singlesig_acl::config.num_trigger_procs" = "1"
"miden::standards::auth::singlesig_acl::config.allow_unauthorized_output_notes" = "0"
"miden::standards::auth::singlesig_acl::config.allow_unauthorized_input_notes" = "0"
"miden::standards::auth::singlesig_acl::trigger_procedure_roots" = [
{ key = ["0", "0", "0", "0"], value = "0xd2d1b6229d7cfb9f2ada31c5cb61453cf464f91828e124437c708eec55b9cd07" }
]
"#;
let file_path = temp_dir.join("acl_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",
"-s",
"private",
"--account-type",
"regular-account-updatable-code",
"-p",
"basic-wallet",
"-p",
"auth/acl-auth",
"-i",
"acl_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",
"-s",
"private",
"--account-type",
"regular-account-updatable-code",
"-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(DebugMode::Disabled).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(DebugMode::Disabled).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, DebugMode::Disabled).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(())
}