use std::{env, sync::Arc};
use clap::Parser;
use comfy_table::{Attribute, Cell, ContentArrangement, Table, presets};
use errors::CliError;
use miden_client::{
Client, ClientError, Felt, IdPrefixFetchError,
account::AccountHeader,
crypto::RpoRandomCoin,
keystore::FilesystemKeyStore,
rpc::TonicRpcClient,
store::{NoteFilter as ClientNoteFilter, OutputNoteRecord, Store, sqlite_store::SqliteStore},
};
use rand::{Rng, rngs::StdRng};
mod commands;
use commands::{
account::AccountCmd,
exec::ExecCmd,
export::ExportCmd,
import::ImportCmd,
init::InitCmd,
new_account::{NewAccountCmd, NewWalletCmd},
new_transactions::{ConsumeNotesCmd, MintCmd, SendCmd, SwapCmd},
notes::NotesCmd,
sync::SyncCmd,
tags::TagsCmd,
transactions::TransactionCmd,
};
use self::utils::load_config_file;
pub type CliKeyStore = FilesystemKeyStore<StdRng>;
mod config;
mod errors;
mod faucet_details_map;
mod info;
mod utils;
const CLIENT_CONFIG_FILE_NAME: &str = "miden-client.toml";
pub const CLIENT_BINARY_NAME: &str = "miden";
const TX_GRACEFUL_BLOCK_DELTA: u32 = 20;
#[derive(Parser, Debug)]
#[clap(name = "Miden", about = "Miden client", version, rename_all = "kebab-case")]
pub struct Cli {
#[clap(subcommand)]
action: Command,
#[clap(short, long, default_value_t = false)]
debug: bool,
}
#[derive(Debug, Parser)]
pub enum Command {
Account(AccountCmd),
NewAccount(NewAccountCmd),
NewWallet(NewWalletCmd),
Import(ImportCmd),
Export(ExportCmd),
Init(InitCmd),
Notes(NotesCmd),
Sync(SyncCmd),
Info,
Tags(TagsCmd),
#[clap(name = "tx")]
Transaction(TransactionCmd),
Mint(MintCmd),
Send(SendCmd),
Swap(SwapCmd),
ConsumeNotes(ConsumeNotesCmd),
Exec(ExecCmd),
}
impl Cli {
pub async fn execute(&self) -> Result<(), CliError> {
let mut current_dir = std::env::current_dir()?;
current_dir.push(CLIENT_CONFIG_FILE_NAME);
if let Command::Init(init_cmd) = &self.action {
init_cmd.execute(¤t_dir)?;
return Ok(());
}
let in_debug_mode = match env::var("MIDEN_DEBUG") {
Ok(value) if value.to_lowercase() == "true" => true,
_ => self.debug,
};
let (cli_config, _config_path) = load_config_file()?;
let store = SqliteStore::new(cli_config.store_filepath.clone())
.await
.map_err(ClientError::StoreError)?;
let store = Arc::new(store);
let mut rng = rand::rng();
let coin_seed: [u64; 4] = rng.random();
let rng = RpoRandomCoin::new(coin_seed.map(Felt::new));
let keystore = CliKeyStore::new(cli_config.secret_keys_directory.clone())
.map_err(CliError::KeyStore)?;
let client = Client::new(
Arc::new(TonicRpcClient::new(
&cli_config.rpc.endpoint.clone().into(),
cli_config.rpc.timeout_ms,
)),
Box::new(rng),
store as Arc<dyn Store>,
Arc::new(keystore.clone()),
in_debug_mode,
Some(TX_GRACEFUL_BLOCK_DELTA),
cli_config.max_block_number_delta,
);
match &self.action {
Command::Account(account) => account.execute(client).await,
Command::NewWallet(new_wallet) => new_wallet.execute(client, keystore).await,
Command::NewAccount(new_account) => new_account.execute(client, keystore).await,
Command::Import(import) => import.execute(client, keystore).await,
Command::Init(_) => Ok(()),
Command::Info => info::print_client_info(&client).await,
Command::Notes(notes) => notes.execute(client).await,
Command::Sync(sync) => sync.execute(client).await,
Command::Tags(tags) => tags.execute(client).await,
Command::Transaction(transaction) => transaction.execute(client).await,
Command::Exec(execute_program) => execute_program.execute(client).await,
Command::Export(cmd) => cmd.execute(client, keystore).await,
Command::Mint(mint) => mint.execute(client).await,
Command::Send(send) => send.execute(client).await,
Command::Swap(swap) => swap.execute(client).await,
Command::ConsumeNotes(consume_notes) => consume_notes.execute(client).await,
}
}
}
pub fn create_dynamic_table(headers: &[&str]) -> Table {
let header_cells = headers
.iter()
.map(|header| Cell::new(header).add_attribute(Attribute::Bold))
.collect::<Vec<_>>();
let mut table = Table::new();
table
.load_preset(presets::UTF8_FULL)
.set_content_arrangement(ContentArrangement::DynamicFullWidth)
.set_header(header_cells);
table
}
pub(crate) async fn get_output_note_with_id_prefix(
client: &Client,
note_id_prefix: &str,
) -> Result<OutputNoteRecord, IdPrefixFetchError> {
let mut output_note_records = client
.get_output_notes(ClientNoteFilter::All)
.await
.map_err(|err| {
tracing::error!("Error when fetching all notes from the store: {err}");
IdPrefixFetchError::NoMatch(format!("note ID prefix {note_id_prefix}").to_string())
})?
.into_iter()
.filter(|note_record| note_record.id().to_hex().starts_with(note_id_prefix))
.collect::<Vec<_>>();
if output_note_records.is_empty() {
return Err(IdPrefixFetchError::NoMatch(
format!("note ID prefix {note_id_prefix}").to_string(),
));
}
if output_note_records.len() > 1 {
let output_note_record_ids =
output_note_records.iter().map(OutputNoteRecord::id).collect::<Vec<_>>();
tracing::error!(
"Multiple notes found for the prefix {}: {:?}",
note_id_prefix,
output_note_record_ids
);
return Err(IdPrefixFetchError::MultipleMatches(
format!("note ID prefix {note_id_prefix}").to_string(),
));
}
Ok(output_note_records
.pop()
.expect("input_note_records should always have one element"))
}
async fn get_account_with_id_prefix(
client: &Client,
account_id_prefix: &str,
) -> Result<AccountHeader, IdPrefixFetchError> {
let mut accounts = client
.get_account_headers()
.await
.map_err(|err| {
tracing::error!("Error when fetching all accounts from the store: {err}");
IdPrefixFetchError::NoMatch(
format!("account ID prefix {account_id_prefix}").to_string(),
)
})?
.into_iter()
.filter(|(account_header, _)| account_header.id().to_hex().starts_with(account_id_prefix))
.map(|(acc, _)| acc)
.collect::<Vec<_>>();
if accounts.is_empty() {
return Err(IdPrefixFetchError::NoMatch(
format!("account ID prefix {account_id_prefix}").to_string(),
));
}
if accounts.len() > 1 {
let account_ids = accounts.iter().map(AccountHeader::id).collect::<Vec<_>>();
tracing::error!(
"Multiple accounts found for the prefix {}: {:?}",
account_id_prefix,
account_ids
);
return Err(IdPrefixFetchError::MultipleMatches(
format!("account ID prefix {account_id_prefix}").to_string(),
));
}
Ok(accounts.pop().expect("account_ids should always have one element"))
}