miden_client_cli/
lib.rs

1use std::env;
2use std::ffi::OsString;
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;
9use miden_client::auth::TransactionAuthenticator;
10use miden_client::builder::ClientBuilder;
11use miden_client::keystore::FilesystemKeyStore;
12use miden_client::store::{NoteFilter as ClientNoteFilter, OutputNoteRecord};
13use miden_client::{Client, DebugMode, IdPrefixFetchError};
14use rand::rngs::StdRng;
15mod commands;
16use commands::account::AccountCmd;
17use commands::exec::ExecCmd;
18use commands::export::ExportCmd;
19use commands::import::ImportCmd;
20use commands::init::InitCmd;
21use commands::new_account::{NewAccountCmd, NewWalletCmd};
22use commands::new_transactions::{ConsumeNotesCmd, MintCmd, SendCmd, SwapCmd};
23use commands::notes::NotesCmd;
24use commands::sync::SyncCmd;
25use commands::tags::TagsCmd;
26use commands::transactions::TransactionCmd;
27
28use self::utils::load_config_file;
29
30pub type CliKeyStore = FilesystemKeyStore<StdRng>;
31
32mod config;
33mod errors;
34mod faucet_details_map;
35mod info;
36mod utils;
37
38/// Config file name.
39const CLIENT_CONFIG_FILE_NAME: &str = "miden-client.toml";
40
41/// Client binary name.
42///
43/// If, for whatever reason, we fail to obtain the client's executable name,
44/// then we simply display the standard "miden-client".
45pub fn client_binary_name() -> OsString {
46    std::env::current_exe()
47        .inspect_err(|e| {
48            eprintln!(
49                "WARNING: Couldn't obtain the path of the current executable because of {e}.\
50             Defaulting to miden-client."
51            );
52        })
53        .and_then(|executable_path| {
54            executable_path.file_name().map(std::ffi::OsStr::to_os_string).ok_or(
55                std::io::Error::other("Couldn't obtain the file name of the current executable"),
56            )
57        })
58        .unwrap_or(OsString::from("miden-client"))
59}
60
61/// Number of blocks that must elapse after a transaction’s reference block before it is marked
62/// stale and discarded.
63const TX_GRACEFUL_BLOCK_DELTA: u32 = 20;
64
65/// Root CLI struct.
66#[derive(Parser, Debug)]
67#[command(
68    name = "miden-client",
69    about = "The Miden client",
70    version,
71    rename_all = "kebab-case"
72)]
73#[command(multicall(true))]
74pub struct MidenClientCli {
75    #[command(subcommand)]
76    behavior: Behavior,
77}
78
79impl From<MidenClientCli> for Cli {
80    fn from(value: MidenClientCli) -> Self {
81        match value.behavior {
82            Behavior::MidenClient { cli } => cli,
83            Behavior::External(args) => Cli::parse_from(args).set_external(),
84        }
85    }
86}
87
88#[derive(Debug, Subcommand)]
89#[command(rename_all = "kebab-case")]
90enum Behavior {
91    /// The Miden Client CLI.
92    MidenClient {
93        #[command(flatten)]
94        cli: Cli,
95    },
96
97    /// Used when the Miden Client CLI is called under a different name, like
98    /// when it is called from [Midenup](https://github.com/0xMiden/midenup).
99    /// Vec<OsString> holds the "raw" arguments passed to the command line,
100    /// analogous to `argv`.
101    #[command(external_subcommand)]
102    External(Vec<OsString>),
103}
104
105#[derive(Parser, Debug)]
106#[command(name = "miden-client")]
107pub struct Cli {
108    /// Activates the executor's debug mode, which enables debug output for scripts
109    /// that were compiled and executed with this mode.
110    #[arg(short, long, default_value_t = false)]
111    debug: bool,
112
113    #[command(subcommand)]
114    action: Command,
115
116    /// Indicates whether the client's CLI is being called directly, or
117    /// externally under an alias (like in the case of
118    /// [Midenup](https://github.com/0xMiden/midenup).
119    #[arg(skip)]
120    #[allow(unused)]
121    external: bool,
122}
123
124/// CLI actions.
125#[derive(Debug, Parser)]
126pub enum Command {
127    Account(AccountCmd),
128    NewAccount(NewAccountCmd),
129    NewWallet(NewWalletCmd),
130    Import(ImportCmd),
131    Export(ExportCmd),
132    Init(InitCmd),
133    Notes(NotesCmd),
134    Sync(SyncCmd),
135    /// View a summary of the current client state.
136    Info,
137    Tags(TagsCmd),
138    #[command(name = "tx")]
139    Transaction(TransactionCmd),
140    Mint(MintCmd),
141    Send(SendCmd),
142    Swap(SwapCmd),
143    ConsumeNotes(ConsumeNotesCmd),
144    Exec(ExecCmd),
145}
146
147/// CLI entry point.
148impl Cli {
149    pub async fn execute(&self) -> Result<(), CliError> {
150        let mut current_dir = std::env::current_dir()?;
151        current_dir.push(CLIENT_CONFIG_FILE_NAME);
152
153        // Check if it's an init command before anything else. When we run the init command for
154        // the first time we won't have a config file and thus creating the store would not be
155        // possible.
156        if let Command::Init(init_cmd) = &self.action {
157            init_cmd.execute(&current_dir)?;
158            return Ok(());
159        }
160
161        // Define whether we want to use the executor's debug mode based on the env var and
162        // the flag override
163        let in_debug_mode = match env::var("MIDEN_DEBUG") {
164            Ok(value) if value.to_lowercase() == "true" => DebugMode::Enabled,
165            _ => DebugMode::Disabled,
166        };
167
168        // Create the client
169        let (cli_config, _config_path) = load_config_file()?;
170
171        let keystore = CliKeyStore::new(cli_config.secret_keys_directory.clone())
172            .map_err(CliError::KeyStore)?;
173
174        let mut builder = ClientBuilder::new()
175            .sqlite_store(cli_config.store_filepath.to_str().expect("Store path should be valid"))
176            .tonic_rpc_client(
177                &cli_config.rpc.endpoint.clone().into(),
178                Some(cli_config.rpc.timeout_ms),
179            )
180            .authenticator(Arc::new(keystore.clone()))
181            .in_debug_mode(in_debug_mode)
182            .tx_graceful_blocks(Some(TX_GRACEFUL_BLOCK_DELTA));
183
184        if let Some(delta) = cli_config.max_block_number_delta {
185            builder = builder.max_block_number_delta(delta);
186        }
187
188        let mut client = builder.build().await?;
189
190        client.ensure_genesis_in_place().await?;
191
192        // Execute CLI command
193        match &self.action {
194            Command::Account(account) => account.execute(client).await,
195            Command::NewWallet(new_wallet) => Box::pin(new_wallet.execute(client, keystore)).await,
196            Command::NewAccount(new_account) => {
197                Box::pin(new_account.execute(client, keystore)).await
198            },
199            Command::Import(import) => import.execute(client, keystore).await,
200            Command::Init(_) => Ok(()),
201            Command::Info => info::print_client_info(&client).await,
202            Command::Notes(notes) => Box::pin(notes.execute(client)).await,
203            Command::Sync(sync) => sync.execute(client).await,
204            Command::Tags(tags) => tags.execute(client).await,
205            Command::Transaction(transaction) => transaction.execute(client).await,
206            Command::Exec(execute_program) => Box::pin(execute_program.execute(client)).await,
207            Command::Export(cmd) => cmd.execute(client, keystore).await,
208            Command::Mint(mint) => Box::pin(mint.execute(client)).await,
209            Command::Send(send) => Box::pin(send.execute(client)).await,
210            Command::Swap(swap) => Box::pin(swap.execute(client)).await,
211            Command::ConsumeNotes(consume_notes) => Box::pin(consume_notes.execute(client)).await,
212        }
213    }
214
215    fn set_external(mut self) -> Self {
216        self.external = true;
217        self
218    }
219}
220
221pub fn create_dynamic_table(headers: &[&str]) -> Table {
222    let header_cells = headers
223        .iter()
224        .map(|header| Cell::new(header).add_attribute(Attribute::Bold))
225        .collect::<Vec<_>>();
226
227    let mut table = Table::new();
228    table
229        .load_preset(presets::UTF8_FULL)
230        .set_content_arrangement(ContentArrangement::DynamicFullWidth)
231        .set_header(header_cells);
232
233    table
234}
235
236/// Returns the client output note whose ID starts with `note_id_prefix`.
237///
238/// # Errors
239///
240/// - Returns [`IdPrefixFetchError::NoMatch`] if we were unable to find any note where
241///   `note_id_prefix` is a prefix of its ID.
242/// - Returns [`IdPrefixFetchError::MultipleMatches`] if there were more than one note found where
243///   `note_id_prefix` is a prefix of its ID.
244pub(crate) async fn get_output_note_with_id_prefix<AUTH: TransactionAuthenticator + Sync>(
245    client: &Client<AUTH>,
246    note_id_prefix: &str,
247) -> Result<OutputNoteRecord, IdPrefixFetchError> {
248    let mut output_note_records = client
249        .get_output_notes(ClientNoteFilter::All)
250        .await
251        .map_err(|err| {
252            tracing::error!("Error when fetching all notes from the store: {err}");
253            IdPrefixFetchError::NoMatch(format!("note ID prefix {note_id_prefix}").to_string())
254        })?
255        .into_iter()
256        .filter(|note_record| note_record.id().to_hex().starts_with(note_id_prefix))
257        .collect::<Vec<_>>();
258
259    if output_note_records.is_empty() {
260        return Err(IdPrefixFetchError::NoMatch(
261            format!("note ID prefix {note_id_prefix}").to_string(),
262        ));
263    }
264    if output_note_records.len() > 1 {
265        let output_note_record_ids =
266            output_note_records.iter().map(OutputNoteRecord::id).collect::<Vec<_>>();
267        tracing::error!(
268            "Multiple notes found for the prefix {}: {:?}",
269            note_id_prefix,
270            output_note_record_ids
271        );
272        return Err(IdPrefixFetchError::MultipleMatches(
273            format!("note ID prefix {note_id_prefix}").to_string(),
274        ));
275    }
276
277    Ok(output_note_records
278        .pop()
279        .expect("input_note_records should always have one element"))
280}
281
282/// Returns the client account whose ID starts with `account_id_prefix`.
283///
284/// # Errors
285///
286/// - Returns [`IdPrefixFetchError::NoMatch`] if we were unable to find any account where
287///   `account_id_prefix` is a prefix of its ID.
288/// - Returns [`IdPrefixFetchError::MultipleMatches`] if there were more than one account found
289///   where `account_id_prefix` is a prefix of its ID.
290async fn get_account_with_id_prefix<AUTH>(
291    client: &Client<AUTH>,
292    account_id_prefix: &str,
293) -> Result<AccountHeader, IdPrefixFetchError> {
294    let mut accounts = client
295        .get_account_headers()
296        .await
297        .map_err(|err| {
298            tracing::error!("Error when fetching all accounts from the store: {err}");
299            IdPrefixFetchError::NoMatch(
300                format!("account ID prefix {account_id_prefix}").to_string(),
301            )
302        })?
303        .into_iter()
304        .filter(|(account_header, _)| account_header.id().to_hex().starts_with(account_id_prefix))
305        .map(|(acc, _)| acc)
306        .collect::<Vec<_>>();
307
308    if accounts.is_empty() {
309        return Err(IdPrefixFetchError::NoMatch(
310            format!("account ID prefix {account_id_prefix}").to_string(),
311        ));
312    }
313    if accounts.len() > 1 {
314        let account_ids = accounts.iter().map(AccountHeader::id).collect::<Vec<_>>();
315        tracing::error!(
316            "Multiple accounts found for the prefix {}: {:?}",
317            account_id_prefix,
318            account_ids
319        );
320        return Err(IdPrefixFetchError::MultipleMatches(
321            format!("account ID prefix {account_id_prefix}").to_string(),
322        ));
323    }
324
325    Ok(accounts.pop().expect("account_ids should always have one element"))
326}