Skip to main content

miden_client_cli/
lib.rs

1use std::env;
2use std::ffi::OsString;
3use std::ops::{Deref, DerefMut};
4use std::sync::Arc;
5
6use clap::{Parser, Subcommand};
7use comfy_table::{Attribute, Cell, ContentArrangement, Table, presets};
8use errors::CliError;
9use miden_client::account::AccountHeader;
10use miden_client::builder::ClientBuilder;
11use miden_client::keystore::{FilesystemKeyStore, Keystore};
12use miden_client::note_transport::grpc::GrpcNoteTransportClient;
13use miden_client::store::{NoteFilter as ClientNoteFilter, OutputNoteRecord};
14use miden_client_sqlite_store::ClientBuilderSqliteExt;
15
16mod commands;
17use commands::account::AccountCmd;
18use commands::clear_config::ClearConfigCmd;
19use commands::exec::ExecCmd;
20use commands::export::ExportCmd;
21use commands::import::ImportCmd;
22use commands::info::InfoCmd;
23use commands::init::InitCmd;
24use commands::new_account::{NewAccountCmd, NewWalletCmd};
25use commands::new_transactions::{ConsumeNotesCmd, MintCmd, SendCmd, SwapCmd};
26use commands::notes::NotesCmd;
27use commands::sync::SyncCmd;
28use commands::tags::TagsCmd;
29use commands::transactions::TransactionCmd;
30
31use self::utils::config_file_exists;
32use crate::commands::address::AddressCmd;
33
34pub type CliKeyStore = FilesystemKeyStore;
35
36/// A Client configured using the CLI's system user configuration.
37///
38/// This is a wrapper around `Client<CliKeyStore>` that provides convenient
39/// initialization methods while maintaining full compatibility with the
40/// underlying Client API through `Deref`.
41///
42/// # Examples
43///
44/// ```no_run
45/// use miden_client_cli::transaction::TransactionRequestBuilder;
46/// use miden_client_cli::{CliClient, DebugMode};
47///
48/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
49/// // Create a CLI-configured client
50/// let mut client = CliClient::new(DebugMode::Disabled).await?;
51///
52/// // All Client methods work automatically via Deref
53/// client.sync_state().await?;
54///
55/// // Build and submit transactions
56/// let req = TransactionRequestBuilder::new()
57///     // ... configure transaction
58///     .build()?;
59///
60/// // client.submit_new_transaction(req, target_account_id)?;
61/// # Ok(())
62/// # }
63/// ```
64pub struct CliClient(miden_client::Client<CliKeyStore>);
65
66impl CliClient {
67    /// Creates a new `CliClient` instance from an existing `CliConfig`.
68    ///
69    ///
70    /// **⚠️ WARNING: This method bypasses the standard CLI configuration discovery logic and should
71    /// only be used in specific scenarios such as testing or when you have explicit control
72    /// requirements.**
73    ///
74    /// ## When NOT to use this method
75    ///
76    /// - **DO NOT** use this method if you want your application to behave like the CLI tool
77    /// - **DO NOT** use this for general-purpose client initialization
78    /// - **DO NOT** use this if you expect automatic local/global config resolution
79    ///
80    /// ## When to use this method
81    ///
82    /// - **Testing**: When you need to test with a specific configuration
83    /// - **Explicit Control**: When you must load config from a non-standard location
84    /// - **Programmatic Config**: When you're constructing configuration programmatically
85    ///
86    /// ## Recommended Alternative
87    ///
88    /// For standard client initialization that matches CLI behavior, use:
89    /// ```ignore
90    /// CliClient::new(debug_mode).await?
91    /// ```
92    ///
93    /// This method **does not** follow the CLI's configuration priority logic (local → global).
94    /// Instead, it uses exactly the configuration provided, which may not be what you expect.
95    ///
96    /// # Arguments
97    ///
98    /// * `config` - The CLI configuration to use (bypasses standard config discovery)
99    /// * `debug_mode` - The debug mode setting ([`DebugMode::Enabled`] or [`DebugMode::Disabled`])
100    ///
101    /// # Returns
102    ///
103    /// A configured [`CliClient`] instance.
104    ///
105    /// # Errors
106    ///
107    /// Returns a [`CliError`] if:
108    /// - Keystore initialization fails
109    /// - Client builder fails to construct the client
110    /// - Note transport connection fails (if configured)
111    ///
112    /// # Examples
113    ///
114    /// ```no_run
115    /// use std::path::PathBuf;
116    ///
117    /// use miden_client_cli::{CliClient, CliConfig, DebugMode};
118    ///
119    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
120    /// // BEWARE: This bypasses standard config discovery!
121    /// // Only use if you know what you're doing.
122    /// let config = CliConfig::from_dir(&PathBuf::from("/path/to/.miden"))?;
123    /// let client = CliClient::from_config(config, DebugMode::Disabled).await?;
124    ///
125    /// // Prefer this for standard CLI-like behavior:
126    /// let client = CliClient::new(DebugMode::Disabled).await?;
127    /// # Ok(())
128    /// # }
129    /// ```
130    pub async fn from_config(
131        config: CliConfig,
132        debug_mode: miden_client::DebugMode,
133    ) -> Result<Self, CliError> {
134        // Create keystore
135        let keystore =
136            CliKeyStore::new(config.secret_keys_directory.clone()).map_err(CliError::KeyStore)?;
137
138        // Build client with the provided configuration
139        let mut builder = ClientBuilder::new()
140            .sqlite_store(config.store_filepath.clone())
141            .grpc_client(&config.rpc.endpoint.clone().into(), Some(config.rpc.timeout_ms))
142            .authenticator(Arc::new(keystore))
143            .in_debug_mode(debug_mode)
144            .tx_discard_delta(Some(TX_DISCARD_DELTA));
145
146        // Add optional max_block_number_delta
147        if let Some(delta) = config.max_block_number_delta {
148            builder = builder.max_block_number_delta(delta);
149        }
150
151        // Add optional note transport client
152        if let Some(tl_config) = config.note_transport {
153            let note_transport_client =
154                GrpcNoteTransportClient::new(tl_config.endpoint.clone(), tl_config.timeout_ms);
155            builder = builder.note_transport(Arc::new(note_transport_client));
156        }
157
158        // Build and return the wrapped client
159        let client = builder.build().await.map_err(CliError::from)?;
160        Ok(CliClient(client))
161    }
162
163    /// Creates a new `CliClient` instance configured using the system user configuration.
164    ///
165    /// # ✅ Recommended Constructor
166    ///
167    /// **This is the recommended way to create a `CliClient` instance.**
168    ///
169    /// This method implements the configuration logic used by the CLI tool, allowing external
170    /// projects to create a Client instance with the same configuration. It searches for
171    /// configuration files in the following order:
172    ///
173    /// 1. Local `.miden/miden-client.toml` in the current working directory
174    /// 2. Global `.miden/miden-client.toml` in the home directory
175    ///
176    /// If no configuration file is found, it silently initializes a default configuration.
177    ///
178    /// The client is initialized with:
179    /// - `SQLite` store from the configured path
180    /// - `gRPC` client connection to the configured RPC endpoint
181    /// - Filesystem-based keystore authenticator
182    /// - Optional note transport client (if configured)
183    /// - Transaction graceful blocks delta
184    /// - Optional max block number delta
185    ///
186    /// # Arguments
187    ///
188    /// * `debug_mode` - The debug mode setting ([`DebugMode::Enabled`] or [`DebugMode::Disabled`]).
189    ///
190    /// # Returns
191    ///
192    /// A configured [`CliClient`] instance.
193    ///
194    /// # Errors
195    ///
196    /// Returns a [`CliError`] if:
197    /// - No configuration file is found (local or global)
198    /// - Configuration file parsing fails
199    /// - Keystore initialization fails
200    /// - Client builder fails to construct the client
201    /// - Note transport connection fails (if configured)
202    ///
203    /// # Examples
204    ///
205    /// ```no_run
206    /// use miden_client_cli::transaction::TransactionRequestBuilder;
207    /// use miden_client_cli::{CliClient, DebugMode};
208    ///
209    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
210    /// // Create a client with default settings (debug disabled)
211    /// let mut client = CliClient::new(DebugMode::Disabled).await?;
212    ///
213    /// // Or with debug mode enabled
214    /// let mut client = CliClient::new(DebugMode::Enabled).await?;
215    ///
216    /// // Use it like a regular Client
217    /// client.sync_state().await?;
218    ///
219    /// // Build and submit transactions
220    /// let req = TransactionRequestBuilder::new()
221    ///     // ... configure transaction
222    ///     .build()?;
223    ///
224    /// // client.submit_new_transaction(req, target_account_id)?;
225    /// # Ok(())
226    /// # }
227    /// ```
228    pub async fn new(debug_mode: miden_client::DebugMode) -> Result<Self, CliError> {
229        // Check if client is not yet initialized => silently initialize the client
230        if !config_file_exists()? {
231            let init_cmd = InitCmd::default();
232            init_cmd.execute()?;
233        }
234
235        // Load configuration from system
236        let config = CliConfig::load()?;
237
238        // Create client using the loaded configuration
239        Self::from_config(config, debug_mode).await
240    }
241
242    /// Unwraps the `CliClient` to get the inner `Client<CliKeyStore>`.
243    ///
244    /// This consumes the `CliClient` and returns the underlying client.
245    ///
246    /// # Examples
247    ///
248    /// ```no_run
249    /// use miden_client_cli::{CliClient, DebugMode};
250    ///
251    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
252    /// let cli_client = CliClient::new(DebugMode::Disabled).await?;
253    /// let inner_client = cli_client.into_inner();
254    /// # Ok(())
255    /// # }
256    /// ```
257    pub fn into_inner(self) -> miden_client::Client<CliKeyStore> {
258        self.0
259    }
260}
261
262/// Allows using `CliClient` like `Client<CliKeyStore>` through deref coercion.
263///
264/// This enables calling all `Client` methods on `CliClient` directly.
265impl Deref for CliClient {
266    type Target = miden_client::Client<CliKeyStore>;
267
268    fn deref(&self) -> &Self::Target {
269        &self.0
270    }
271}
272
273/// Allows mutable access to `Client<CliKeyStore>` methods.
274impl DerefMut for CliClient {
275    fn deref_mut(&mut self) -> &mut Self::Target {
276        &mut self.0
277    }
278}
279
280pub mod config;
281// These modules intentionally shadow the miden_client re-exports - CLI has its own errors/utils
282#[allow(hidden_glob_reexports)]
283mod errors;
284mod faucet_details_map;
285mod info;
286#[allow(hidden_glob_reexports)]
287mod utils;
288
289/// Re-export `MIDEN_DIR` for use in tests
290pub use config::MIDEN_DIR;
291/// Re-export common types for external projects
292pub use config::{CLIENT_CONFIG_FILE_NAME, CliConfig};
293pub use errors::CliError as Error;
294/// Re-export the entire `miden_client` crate so external projects can use a single dependency.
295pub use miden_client::*;
296
297/// Client binary name.
298///
299/// If, for whatever reason, we fail to obtain the client's executable name,
300/// then we simply display the standard "miden-client".
301pub fn client_binary_name() -> OsString {
302    std::env::current_exe()
303        .inspect_err(|e| {
304            eprintln!(
305                "WARNING: Couldn't obtain the path of the current executable because of {e}.\
306             Defaulting to miden-client."
307            );
308        })
309        .and_then(|executable_path| {
310            executable_path.file_name().map(std::ffi::OsStr::to_os_string).ok_or(
311                std::io::Error::other("Couldn't obtain the file name of the current executable"),
312            )
313        })
314        .unwrap_or(OsString::from("miden-client"))
315}
316
317/// Number of blocks that must elapse after a transaction’s reference block before it is marked
318/// stale and discarded.
319const TX_DISCARD_DELTA: u32 = 20;
320
321/// Root CLI struct.
322#[derive(Parser, Debug)]
323#[command(
324    name = "miden-client",
325    about = "The Miden client",
326    version,
327    propagate_version = true,
328    rename_all = "kebab-case"
329)]
330#[command(multicall(true))]
331pub struct MidenClientCli {
332    #[command(subcommand)]
333    behavior: Behavior,
334}
335
336impl From<MidenClientCli> for Cli {
337    fn from(value: MidenClientCli) -> Self {
338        match value.behavior {
339            Behavior::MidenClient { cli } => cli,
340            Behavior::External(args) => Cli::parse_from(args).set_external(),
341        }
342    }
343}
344
345#[derive(Debug, Subcommand)]
346#[command(rename_all = "kebab-case")]
347enum Behavior {
348    /// The Miden Client CLI.
349    MidenClient {
350        #[command(flatten)]
351        cli: Cli,
352    },
353
354    /// Used when the Miden Client CLI is called under a different name, like
355    /// when it is called from [Midenup](https://github.com/0xMiden/midenup).
356    /// Vec<OsString> holds the "raw" arguments passed to the command line,
357    /// analogous to `argv`.
358    #[command(external_subcommand)]
359    External(Vec<OsString>),
360}
361
362#[derive(Parser, Debug)]
363#[command(name = "miden-client")]
364pub struct Cli {
365    /// Activates the executor's debug mode, which enables debug output for scripts
366    /// that were compiled and executed with this mode.
367    #[arg(short, long, default_value_t = false)]
368    debug: bool,
369
370    #[command(subcommand)]
371    action: Command,
372
373    /// Indicates whether the client's CLI is being called directly, or
374    /// externally under an alias (like in the case of
375    /// [Midenup](https://github.com/0xMiden/midenup).
376    #[arg(skip)]
377    #[allow(unused)]
378    external: bool,
379}
380
381/// CLI actions.
382#[derive(Debug, Parser)]
383pub enum Command {
384    Account(AccountCmd),
385    NewAccount(NewAccountCmd),
386    NewWallet(NewWalletCmd),
387    Import(ImportCmd),
388    Export(ExportCmd),
389    Init(InitCmd),
390    ClearConfig(ClearConfigCmd),
391    Notes(NotesCmd),
392    Sync(SyncCmd),
393    /// View a summary of the current client state.
394    Info(InfoCmd),
395    Tags(TagsCmd),
396    Address(AddressCmd),
397    #[command(name = "tx")]
398    Transaction(TransactionCmd),
399    Mint(MintCmd),
400    Send(SendCmd),
401    Swap(SwapCmd),
402    ConsumeNotes(ConsumeNotesCmd),
403    Exec(ExecCmd),
404}
405
406/// CLI entry point.
407impl Cli {
408    pub async fn execute(&self) -> Result<(), CliError> {
409        // Handle commands that don't require client initialization
410        match &self.action {
411            Command::Init(init_cmd) => {
412                init_cmd.execute()?;
413                return Ok(());
414            },
415            Command::ClearConfig(clear_config_cmd) => {
416                clear_config_cmd.execute()?;
417                return Ok(());
418            },
419            _ => {},
420        }
421
422        // Check if Client is not yet initialized => silently initialize the client
423        if !config_file_exists()? {
424            let init_cmd = InitCmd::default();
425            init_cmd.execute()?;
426        }
427
428        // Define whether we want to use the executor's debug mode based on the env var and
429        // the flag override
430        let in_debug_mode = match env::var("MIDEN_DEBUG") {
431            Ok(value) if value.to_lowercase() == "true" => miden_client::DebugMode::Enabled,
432            _ => miden_client::DebugMode::Disabled,
433        };
434
435        // Load configuration
436        let cli_config = CliConfig::load()?;
437
438        // Create keystore for commands that need it
439        let keystore = CliKeyStore::new(cli_config.secret_keys_directory.clone())
440            .map_err(CliError::KeyStore)?;
441
442        // Create the client
443        let cli_client = CliClient::from_config(cli_config, in_debug_mode).await?;
444
445        // Extract the inner client for command execution
446        let client = cli_client.into_inner();
447
448        // Execute CLI command
449        match &self.action {
450            Command::Account(account) => account.execute(client).await,
451            Command::NewWallet(new_wallet) => Box::pin(new_wallet.execute(client, keystore)).await,
452            Command::NewAccount(new_account) => {
453                Box::pin(new_account.execute(client, keystore)).await
454            },
455            Command::Import(import) => import.execute(client, keystore).await,
456            Command::Init(_) | Command::ClearConfig(_) => Ok(()), // Already handled earlier
457            Command::Info(info_cmd) => info::print_client_info(&client, info_cmd.rpc_status).await,
458            Command::Notes(notes) => Box::pin(notes.execute(client)).await,
459            Command::Sync(sync) => sync.execute(client).await,
460            Command::Tags(tags) => tags.execute(client).await,
461            Command::Address(addresses) => addresses.execute(client).await,
462            Command::Transaction(transaction) => transaction.execute(client).await,
463            Command::Exec(execute_program) => Box::pin(execute_program.execute(client)).await,
464            Command::Export(cmd) => cmd.execute(client, keystore).await,
465            Command::Mint(mint) => Box::pin(mint.execute(client)).await,
466            Command::Send(send) => Box::pin(send.execute(client)).await,
467            Command::Swap(swap) => Box::pin(swap.execute(client)).await,
468            Command::ConsumeNotes(consume_notes) => Box::pin(consume_notes.execute(client)).await,
469        }
470    }
471
472    fn set_external(mut self) -> Self {
473        self.external = true;
474        self
475    }
476}
477
478pub fn create_dynamic_table(headers: &[&str]) -> Table {
479    let header_cells = headers
480        .iter()
481        .map(|header| Cell::new(header).add_attribute(Attribute::Bold))
482        .collect::<Vec<_>>();
483
484    let mut table = Table::new();
485    table
486        .load_preset(presets::UTF8_FULL)
487        .set_content_arrangement(ContentArrangement::DynamicFullWidth)
488        .set_header(header_cells);
489
490    table
491}
492
493/// Returns the client output note whose ID starts with `note_id_prefix`.
494///
495/// # Errors
496///
497/// - Returns [`IdPrefixFetchError::NoMatch`](miden_client::IdPrefixFetchError::NoMatch) if we were
498///   unable to find any note where `note_id_prefix` is a prefix of its ID.
499/// - Returns [`IdPrefixFetchError::MultipleMatches`](miden_client::IdPrefixFetchError::MultipleMatches)
500///   if there were more than one note found where `note_id_prefix` is a prefix of its ID.
501pub(crate) async fn get_output_note_with_id_prefix<AUTH: Keystore + Sync>(
502    client: &miden_client::Client<AUTH>,
503    note_id_prefix: &str,
504) -> Result<OutputNoteRecord, miden_client::IdPrefixFetchError> {
505    let mut output_note_records = client
506        .get_output_notes(ClientNoteFilter::All)
507        .await
508        .map_err(|err| {
509            tracing::error!("Error when fetching all notes from the store: {err}");
510            miden_client::IdPrefixFetchError::NoMatch(
511                format!("note ID prefix {note_id_prefix}").to_string(),
512            )
513        })?
514        .into_iter()
515        .filter(|note_record| note_record.id().to_hex().starts_with(note_id_prefix))
516        .collect::<Vec<_>>();
517
518    if output_note_records.is_empty() {
519        return Err(miden_client::IdPrefixFetchError::NoMatch(
520            format!("note ID prefix {note_id_prefix}").to_string(),
521        ));
522    }
523    if output_note_records.len() > 1 {
524        let output_note_record_ids =
525            output_note_records.iter().map(OutputNoteRecord::id).collect::<Vec<_>>();
526        tracing::error!(
527            "Multiple notes found for the prefix {}: {:?}",
528            note_id_prefix,
529            output_note_record_ids
530        );
531        return Err(miden_client::IdPrefixFetchError::MultipleMatches(
532            format!("note ID prefix {note_id_prefix}").to_string(),
533        ));
534    }
535
536    Ok(output_note_records
537        .pop()
538        .expect("input_note_records should always have one element"))
539}
540
541/// Returns the client account whose ID starts with `account_id_prefix`.
542///
543/// # Errors
544///
545/// - Returns [`IdPrefixFetchError::NoMatch`](miden_client::IdPrefixFetchError::NoMatch) if we were
546///   unable to find any account where `account_id_prefix` is a prefix of its ID.
547/// - Returns [`IdPrefixFetchError::MultipleMatches`](miden_client::IdPrefixFetchError::MultipleMatches)
548///   if there were more than one account found where `account_id_prefix` is a prefix of its ID.
549async fn get_account_with_id_prefix<AUTH>(
550    client: &miden_client::Client<AUTH>,
551    account_id_prefix: &str,
552) -> Result<AccountHeader, miden_client::IdPrefixFetchError> {
553    let mut accounts = client
554        .get_account_headers()
555        .await
556        .map_err(|err| {
557            tracing::error!("Error when fetching all accounts from the store: {err}");
558            miden_client::IdPrefixFetchError::NoMatch(
559                format!("account ID prefix {account_id_prefix}").to_string(),
560            )
561        })?
562        .into_iter()
563        .filter(|(account_header, _)| account_header.id().to_hex().starts_with(account_id_prefix))
564        .map(|(acc, _)| acc)
565        .collect::<Vec<_>>();
566
567    if accounts.is_empty() {
568        return Err(miden_client::IdPrefixFetchError::NoMatch(
569            format!("account ID prefix {account_id_prefix}").to_string(),
570        ));
571    }
572    if accounts.len() > 1 {
573        let account_ids = accounts.iter().map(AccountHeader::id).collect::<Vec<_>>();
574        tracing::error!(
575            "Multiple accounts found for the prefix {}: {:?}",
576            account_id_prefix,
577            account_ids
578        );
579        return Err(miden_client::IdPrefixFetchError::MultipleMatches(
580            format!("account ID prefix {account_id_prefix}").to_string(),
581        ));
582    }
583
584    Ok(accounts.pop().expect("account_ids should always have one element"))
585}