use std::env;
use std::ffi::OsString;
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use clap::{Parser, Subcommand};
use comfy_table::{Attribute, Cell, ContentArrangement, Table, presets};
use errors::CliError;
use miden_client::account::AccountHeader;
use miden_client::builder::ClientBuilder;
use miden_client::keystore::{FilesystemKeyStore, Keystore};
use miden_client::note_transport::grpc::GrpcNoteTransportClient;
use miden_client::store::{NoteFilter as ClientNoteFilter, OutputNoteRecord};
use miden_client_sqlite_store::ClientBuilderSqliteExt;
mod commands;
use commands::account::AccountCmd;
use commands::clear_config::ClearConfigCmd;
use commands::exec::ExecCmd;
use commands::export::ExportCmd;
use commands::import::ImportCmd;
use commands::info::InfoCmd;
use commands::init::InitCmd;
use commands::new_account::{NewAccountCmd, NewWalletCmd};
use commands::new_transactions::{ConsumeNotesCmd, MintCmd, SendCmd, SwapCmd};
use commands::notes::NotesCmd;
use commands::sync::SyncCmd;
use commands::tags::TagsCmd;
use commands::transactions::TransactionCmd;
use self::utils::config_file_exists;
use crate::commands::address::AddressCmd;
pub type CliKeyStore = FilesystemKeyStore;
pub struct CliClient(miden_client::Client<CliKeyStore>);
impl CliClient {
pub async fn from_config(
config: CliConfig,
debug_mode: miden_client::DebugMode,
) -> Result<Self, CliError> {
let keystore =
CliKeyStore::new(config.secret_keys_directory.clone()).map_err(CliError::KeyStore)?;
let mut builder = ClientBuilder::new()
.sqlite_store(config.store_filepath.clone())
.grpc_client(&config.rpc.endpoint.clone().into(), Some(config.rpc.timeout_ms))
.authenticator(Arc::new(keystore))
.in_debug_mode(debug_mode)
.tx_discard_delta(Some(TX_DISCARD_DELTA));
if let Some(delta) = config.max_block_number_delta {
builder = builder.max_block_number_delta(delta);
}
if let Some(tl_config) = config.note_transport {
let note_transport_client =
GrpcNoteTransportClient::new(tl_config.endpoint.clone(), tl_config.timeout_ms);
builder = builder.note_transport(Arc::new(note_transport_client));
}
let client = builder.build().await.map_err(CliError::from)?;
Ok(CliClient(client))
}
pub async fn new(debug_mode: miden_client::DebugMode) -> Result<Self, CliError> {
if !config_file_exists()? {
let init_cmd = InitCmd::default();
init_cmd.execute()?;
}
let config = CliConfig::load()?;
Self::from_config(config, debug_mode).await
}
pub fn into_inner(self) -> miden_client::Client<CliKeyStore> {
self.0
}
}
impl Deref for CliClient {
type Target = miden_client::Client<CliKeyStore>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for CliClient {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
pub mod config;
#[allow(hidden_glob_reexports)]
mod errors;
mod faucet_details_map;
mod info;
#[allow(hidden_glob_reexports)]
mod utils;
pub use config::MIDEN_DIR;
pub use config::{CLIENT_CONFIG_FILE_NAME, CliConfig};
pub use errors::CliError as Error;
pub use miden_client::*;
pub fn client_binary_name() -> OsString {
std::env::current_exe()
.inspect_err(|e| {
eprintln!(
"WARNING: Couldn't obtain the path of the current executable because of {e}.\
Defaulting to miden-client."
);
})
.and_then(|executable_path| {
executable_path.file_name().map(std::ffi::OsStr::to_os_string).ok_or(
std::io::Error::other("Couldn't obtain the file name of the current executable"),
)
})
.unwrap_or(OsString::from("miden-client"))
}
const TX_DISCARD_DELTA: u32 = 20;
#[derive(Parser, Debug)]
#[command(
name = "miden-client",
about = "The Miden client",
version,
propagate_version = true,
rename_all = "kebab-case"
)]
#[command(multicall(true))]
pub struct MidenClientCli {
#[command(subcommand)]
behavior: Behavior,
}
impl From<MidenClientCli> for Cli {
fn from(value: MidenClientCli) -> Self {
match value.behavior {
Behavior::MidenClient { cli } => cli,
Behavior::External(args) => Cli::parse_from(args).set_external(),
}
}
}
#[derive(Debug, Subcommand)]
#[command(rename_all = "kebab-case")]
enum Behavior {
MidenClient {
#[command(flatten)]
cli: Cli,
},
#[command(external_subcommand)]
External(Vec<OsString>),
}
#[derive(Parser, Debug)]
#[command(name = "miden-client")]
pub struct Cli {
#[arg(short, long, default_value_t = false)]
debug: bool,
#[command(subcommand)]
action: Command,
#[arg(skip)]
#[allow(unused)]
external: bool,
}
#[derive(Debug, Parser)]
pub enum Command {
Account(AccountCmd),
NewAccount(NewAccountCmd),
NewWallet(NewWalletCmd),
Import(ImportCmd),
Export(ExportCmd),
Init(InitCmd),
ClearConfig(ClearConfigCmd),
Notes(NotesCmd),
Sync(SyncCmd),
Info(InfoCmd),
Tags(TagsCmd),
Address(AddressCmd),
#[command(name = "tx")]
Transaction(TransactionCmd),
Mint(MintCmd),
Send(SendCmd),
Swap(SwapCmd),
ConsumeNotes(ConsumeNotesCmd),
Exec(ExecCmd),
}
impl Cli {
pub async fn execute(&self) -> Result<(), CliError> {
match &self.action {
Command::Init(init_cmd) => {
init_cmd.execute()?;
return Ok(());
},
Command::ClearConfig(clear_config_cmd) => {
clear_config_cmd.execute()?;
return Ok(());
},
_ => {},
}
if !config_file_exists()? {
let init_cmd = InitCmd::default();
init_cmd.execute()?;
}
let in_debug_mode = match env::var("MIDEN_DEBUG") {
Ok(value) if value.to_lowercase() == "true" => miden_client::DebugMode::Enabled,
_ => miden_client::DebugMode::Disabled,
};
let cli_config = CliConfig::load()?;
let keystore = CliKeyStore::new(cli_config.secret_keys_directory.clone())
.map_err(CliError::KeyStore)?;
let cli_client = CliClient::from_config(cli_config, in_debug_mode).await?;
let client = cli_client.into_inner();
match &self.action {
Command::Account(account) => account.execute(client).await,
Command::NewWallet(new_wallet) => Box::pin(new_wallet.execute(client, keystore)).await,
Command::NewAccount(new_account) => {
Box::pin(new_account.execute(client, keystore)).await
},
Command::Import(import) => import.execute(client, keystore).await,
Command::Init(_) | Command::ClearConfig(_) => Ok(()), Command::Info(info_cmd) => info::print_client_info(&client, info_cmd.rpc_status).await,
Command::Notes(notes) => Box::pin(notes.execute(client)).await,
Command::Sync(sync) => sync.execute(client).await,
Command::Tags(tags) => tags.execute(client).await,
Command::Address(addresses) => addresses.execute(client).await,
Command::Transaction(transaction) => transaction.execute(client).await,
Command::Exec(execute_program) => Box::pin(execute_program.execute(client)).await,
Command::Export(cmd) => cmd.execute(client, keystore).await,
Command::Mint(mint) => Box::pin(mint.execute(client)).await,
Command::Send(send) => Box::pin(send.execute(client)).await,
Command::Swap(swap) => Box::pin(swap.execute(client)).await,
Command::ConsumeNotes(consume_notes) => Box::pin(consume_notes.execute(client)).await,
}
}
fn set_external(mut self) -> Self {
self.external = true;
self
}
}
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<AUTH: Keystore + Sync>(
client: &miden_client::Client<AUTH>,
note_id_prefix: &str,
) -> Result<OutputNoteRecord, miden_client::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}");
miden_client::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(miden_client::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(miden_client::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<AUTH>(
client: &miden_client::Client<AUTH>,
account_id_prefix: &str,
) -> Result<AccountHeader, miden_client::IdPrefixFetchError> {
let mut accounts = client
.get_account_headers()
.await
.map_err(|err| {
tracing::error!("Error when fetching all accounts from the store: {err}");
miden_client::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(miden_client::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(miden_client::IdPrefixFetchError::MultipleMatches(
format!("account ID prefix {account_id_prefix}").to_string(),
));
}
Ok(accounts.pop().expect("account_ids should always have one element"))
}