1use std::ffi::OsString;
2use std::ops::{Deref, DerefMut};
3use std::sync::Arc;
4
5use clap::{Parser, Subcommand};
6use comfy_table::{Attribute, Cell, ContentArrangement, Table, presets};
7use errors::CliError;
8use miden_client::account::{AccountHeader, AccountId};
9use miden_client::asset::AssetId;
10use miden_client::builder::ClientBuilder;
11use miden_client::keystore::{FilesystemKeyStore, Keystore};
12use miden_client::note_transport::grpc::GrpcNoteTransportClient;
13use miden_client::protocol_config::ProtocolConfig;
14use miden_client::rpc::{GrpcClient, VerifyingRpcClient};
15use miden_client::store::{NoteFilter as ClientNoteFilter, OutputNoteRecord};
16use miden_client_sqlite_store::ClientBuilderSqliteExt;
17
18mod commands;
19use commands::account::AccountCmd;
20use commands::call::CallCmd;
21use commands::clear_config::ClearConfigCmd;
22use commands::exec::ExecCmd;
23use commands::export::ExportCmd;
24use commands::import::ImportCmd;
25use commands::info::InfoCmd;
26use commands::init::InitCmd;
27use commands::keys::KeysCmd;
28use commands::network_note_status::NetworkNoteStatusCmd;
29use commands::new_account::{NewAccountCmd, NewWalletCmd};
30use commands::new_transactions::{ConsumeNotesCmd, MintCmd, PswapCmd, SwapCmd, TransferCmd};
31use commands::notes::NotesCmd;
32use commands::sync::SyncCmd;
33use commands::tags::TagsCmd;
34use commands::transactions::TransactionCmd;
35
36use self::utils::config_file_exists;
37use crate::commands::address::AddressCmd;
38
39pub type CliKeyStore = FilesystemKeyStore;
40
41pub struct CliClient(miden_client::Client<CliKeyStore>);
69
70impl CliClient {
71 pub async fn from_config(config: CliConfig) -> Result<Self, CliError> {
134 let keystore =
135 CliKeyStore::new(config.secret_keys_directory.clone()).map_err(CliError::KeyStore)?;
136
137 let rpc_client = Arc::new(VerifyingRpcClient::new(
138 GrpcClient::new(&config.rpc.endpoint.clone().into(), config.rpc.timeout_ms)
139 .with_max_decoding_message_size(CLI_MAX_RESPONSE_SIZE_BYTES),
140 ));
141
142 let mut builder = ClientBuilder::new()
143 .sqlite_store(config.store_filepath.clone())
144 .rpc(rpc_client)
145 .authenticator(Arc::new(keystore))
146 .tx_discard_delta(Some(TX_DISCARD_DELTA));
147
148 if let Some(faucet) = config.fee_faucet_id.as_deref() {
149 let faucet_id = AccountId::from_hex(faucet).map_err(|err| {
150 CliError::Config(Box::new(err), "invalid `fee_faucet_id`".to_string())
151 })?;
152 let protocol_config = ProtocolConfig::current(AssetId::new_fungible(faucet_id))
153 .map_err(|err| {
154 CliError::Config(
155 Box::new(err),
156 "failed to derive the protocol configuration from `fee_faucet_id`"
157 .to_string(),
158 )
159 })?;
160 builder = builder.protocol_config(protocol_config);
161 }
162
163 if let Some(delta) = config.max_block_number_delta {
164 builder = builder.max_block_number_delta(delta);
165 }
166
167 if let Some(tl_config) = config.note_transport {
168 let note_transport_client =
169 GrpcNoteTransportClient::new(tl_config.endpoint.clone(), tl_config.timeout_ms);
170 builder = builder.note_transport(Arc::new(note_transport_client));
171 }
172
173 let client = builder.build().await.map_err(CliError::from)?;
174 if let Some(path) = std::env::var_os("MIDEN_PROTOCOL_CONFIG") {
175 let path = std::path::PathBuf::from(path);
176 let bytes = std::fs::read(&path).map_err(|err| {
177 CliError::Config(Box::new(err), format!("failed to read {}", path.display()))
178 })?;
179 let protocol_config = ProtocolConfig::read_from_bytes(&bytes).map_err(|err| {
180 CliError::Config(Box::new(err), format!("failed to decode {}", path.display()))
181 })?;
182 client.add_protocol_config(protocol_config).await.map_err(CliError::from)?;
183 }
184 Ok(CliClient(client))
185 }
186
187 pub async fn new() -> Result<Self, CliError> {
249 if !config_file_exists()? {
251 let init_cmd = InitCmd::default();
252 init_cmd.execute()?;
253 }
254
255 let config = CliConfig::load()?;
256
257 Self::from_config(config).await
258 }
259
260 pub fn into_inner(self) -> miden_client::Client<CliKeyStore> {
276 self.0
277 }
278}
279
280impl Deref for CliClient {
284 type Target = miden_client::Client<CliKeyStore>;
285
286 fn deref(&self) -> &Self::Target {
287 &self.0
288 }
289}
290
291impl DerefMut for CliClient {
293 fn deref_mut(&mut self) -> &mut Self::Target {
294 &mut self.0
295 }
296}
297
298mod advice_inputs;
299mod codecs;
300pub mod config;
301#[allow(hidden_glob_reexports)]
303mod errors;
304mod info;
305#[allow(hidden_glob_reexports)]
306mod utils;
307
308pub use config::MIDEN_DIR;
310pub use config::{CLIENT_CONFIG_FILE_NAME, CliConfig};
312pub use errors::CliError as Error;
313pub use miden_client::*;
315
316pub fn client_binary_name() -> OsString {
321 std::env::current_exe()
322 .inspect_err(|e| {
323 eprintln!(
324 "WARNING: Couldn't obtain the path of the current executable because of {e}.\
325 Defaulting to miden-client."
326 );
327 })
328 .and_then(|executable_path| {
329 executable_path.file_name().map(std::ffi::OsStr::to_os_string).ok_or(
330 std::io::Error::other("Couldn't obtain the file name of the current executable"),
331 )
332 })
333 .unwrap_or(OsString::from("miden-client"))
334}
335
336const TX_DISCARD_DELTA: u32 = 20;
339
340const CLI_MAX_RESPONSE_SIZE_BYTES: usize = 6 * 1024 * 1024;
343
344#[derive(Parser, Debug)]
346#[command(
347 name = "miden-client",
348 about = "The Miden client",
349 version,
350 propagate_version = true,
351 rename_all = "kebab-case"
352)]
353#[command(multicall(true))]
354pub struct MidenClientCli {
355 #[command(subcommand)]
356 behavior: Behavior,
357}
358
359impl From<MidenClientCli> for Cli {
360 fn from(value: MidenClientCli) -> Self {
361 match value.behavior {
362 Behavior::MidenClient { cli } => cli,
363 Behavior::External(args) => Cli::parse_from(args).set_external(),
364 }
365 }
366}
367
368#[derive(Debug, Subcommand)]
369#[command(rename_all = "kebab-case")]
370enum Behavior {
371 MidenClient {
373 #[command(flatten)]
374 cli: Cli,
375 },
376
377 #[command(external_subcommand)]
381 External(Vec<OsString>),
382}
383
384#[derive(Parser, Debug)]
385#[command(name = "miden-client", version)]
386pub struct Cli {
387 #[command(subcommand)]
388 action: Command,
389
390 #[arg(skip)]
393 #[allow(unused)]
394 external: bool,
395}
396
397#[derive(Debug, Parser)]
399pub enum Command {
400 Account(AccountCmd),
401 NewAccount(NewAccountCmd),
402 NewWallet(NewWalletCmd),
403 Import(ImportCmd),
404 Export(ExportCmd),
405 Keys(KeysCmd),
406 Init(InitCmd),
407 ClearConfig(ClearConfigCmd),
408 Notes(NotesCmd),
409 Sync(SyncCmd),
410 Info(InfoCmd),
412 Tags(TagsCmd),
413 Address(AddressCmd),
414 #[command(name = "tx")]
415 Transaction(TransactionCmd),
416 Mint(MintCmd),
417 Transfer(TransferCmd),
418 Pswap(PswapCmd),
419 Swap(SwapCmd),
420 ConsumeNotes(ConsumeNotesCmd),
421 Exec(ExecCmd),
422 NetworkNoteStatus(NetworkNoteStatusCmd),
423 Call(CallCmd),
424}
425
426impl Cli {
428 pub async fn execute(&self) -> Result<(), CliError> {
429 match &self.action {
431 Command::Init(init_cmd) => {
432 init_cmd.execute()?;
433 return Ok(());
434 },
435 Command::ClearConfig(clear_config_cmd) => {
436 clear_config_cmd.execute()?;
437 return Ok(());
438 },
439 Command::NetworkNoteStatus(cmd) => {
440 return cmd.execute().await;
441 },
442 _ => {},
443 }
444
445 if !config_file_exists()? {
447 let init_cmd = InitCmd::default();
448 init_cmd.execute()?;
449 }
450
451 let cli_config = CliConfig::load()?;
452
453 let keystore = CliKeyStore::new(cli_config.secret_keys_directory.clone())
454 .map_err(CliError::KeyStore)?;
455
456 if let Command::Keys(keys) = &self.action {
457 return keys.execute(&keystore);
458 }
459
460 let cli_client = CliClient::from_config(cli_config).await?;
461
462 let client = cli_client.into_inner();
463
464 match &self.action {
465 Command::Account(account) => account.execute(client).await,
466 Command::NewWallet(new_wallet) => Box::pin(new_wallet.execute(client, keystore)).await,
467 Command::NewAccount(new_account) => {
468 Box::pin(new_account.execute(client, keystore)).await
469 },
470 Command::Import(import) => import.execute(client, keystore).await,
471 Command::Init(_)
472 | Command::ClearConfig(_)
473 | Command::NetworkNoteStatus(_)
474 | Command::Keys(_) => Ok(()), Command::Info(info_cmd) => info::print_client_info(&client, info_cmd.rpc_status).await,
476 Command::Notes(notes) => Box::pin(notes.execute(client)).await,
477 Command::Sync(sync) => sync.execute(client).await,
478 Command::Tags(tags) => tags.execute(client).await,
479 Command::Address(addresses) => addresses.execute(client).await,
480 Command::Transaction(transaction) => transaction.execute(client).await,
481 Command::Exec(execute_program) => Box::pin(execute_program.execute(client)).await,
482 Command::Call(call) => Box::pin(call.execute(client)).await,
483 Command::Export(cmd) => cmd.execute(client, keystore).await,
484 Command::Mint(mint) => Box::pin(mint.execute(client)).await,
485 Command::Transfer(transfer) => Box::pin(transfer.execute(client)).await,
486 Command::Pswap(pswap) => Box::pin(pswap.execute(client)).await,
487 Command::Swap(swap) => Box::pin(swap.execute(client)).await,
488 Command::ConsumeNotes(consume_notes) => Box::pin(consume_notes.execute(client)).await,
489 }
490 }
491
492 fn set_external(mut self) -> Self {
493 self.external = true;
494 self
495 }
496}
497
498pub fn create_dynamic_table(headers: &[&str]) -> Table {
499 let header_cells = headers
500 .iter()
501 .map(|header| Cell::new(header).add_attribute(Attribute::Bold))
502 .collect::<Vec<_>>();
503
504 let mut table = Table::new();
505 table
506 .load_preset(presets::UTF8_FULL)
507 .set_content_arrangement(ContentArrangement::DynamicFullWidth)
508 .set_header(header_cells);
509
510 table
511}
512
513pub(crate) async fn get_output_note_with_id_prefix<AUTH: Keystore + Sync>(
522 client: &miden_client::Client<AUTH>,
523 note_id_prefix: &str,
524) -> Result<OutputNoteRecord, miden_client::IdPrefixFetchError> {
525 let mut output_note_records = client
526 .get_output_notes(ClientNoteFilter::All)
527 .await
528 .map_err(|err| {
529 tracing::error!("Error when fetching all notes from the store: {err}");
530 miden_client::IdPrefixFetchError::NoMatch(
531 format!("note ID prefix {note_id_prefix}").to_string(),
532 )
533 })?
534 .into_iter()
535 .filter(|note_record| note_record.id().to_hex().starts_with(note_id_prefix))
536 .collect::<Vec<_>>();
537
538 if output_note_records.is_empty() {
539 return Err(miden_client::IdPrefixFetchError::NoMatch(
540 format!("note ID prefix {note_id_prefix}").to_string(),
541 ));
542 }
543 if output_note_records.len() > 1 {
544 let output_note_record_ids =
545 output_note_records.iter().map(OutputNoteRecord::id).collect::<Vec<_>>();
546 tracing::error!(
547 "Multiple notes found for the prefix {}: {:?}",
548 note_id_prefix,
549 output_note_record_ids
550 );
551 return Err(miden_client::IdPrefixFetchError::MultipleMatches(
552 format!("note ID prefix {note_id_prefix}").to_string(),
553 ));
554 }
555
556 Ok(output_note_records
557 .pop()
558 .expect("input_note_records should always have one element"))
559}
560
561async fn get_account_with_id_prefix<AUTH>(
570 client: &miden_client::Client<AUTH>,
571 account_id_prefix: &str,
572) -> Result<AccountHeader, miden_client::IdPrefixFetchError> {
573 let mut accounts = client
574 .get_account_headers()
575 .await
576 .map_err(|err| {
577 tracing::error!("Error when fetching all accounts from the store: {err}");
578 miden_client::IdPrefixFetchError::NoMatch(
579 format!("account ID prefix {account_id_prefix}").to_string(),
580 )
581 })?
582 .into_iter()
583 .filter(|(account_header, _)| account_header.id().to_hex().starts_with(account_id_prefix))
584 .map(|(acc, _)| acc)
585 .collect::<Vec<_>>();
586
587 if accounts.is_empty() {
588 return Err(miden_client::IdPrefixFetchError::NoMatch(
589 format!("account ID prefix {account_id_prefix}").to_string(),
590 ));
591 }
592 if accounts.len() > 1 {
593 let account_ids = accounts.iter().map(AccountHeader::id).collect::<Vec<_>>();
594 tracing::error!(
595 "Multiple accounts found for the prefix {}: {:?}",
596 account_id_prefix,
597 account_ids
598 );
599 return Err(miden_client::IdPrefixFetchError::MultipleMatches(
600 format!("account ID prefix {account_id_prefix}").to_string(),
601 ));
602 }
603
604 Ok(accounts.pop().expect("account_ids should always have one element"))
605}