miden_client_cli/
lib.rs

1use std::{env, sync::Arc};
2
3use clap::Parser;
4use comfy_table::{Attribute, Cell, ContentArrangement, Table, presets};
5use errors::CliError;
6use miden_client::{
7    Client, IdPrefixFetchError,
8    account::AccountHeader,
9    builder::ClientBuilder,
10    keystore::FilesystemKeyStore,
11    store::{NoteFilter as ClientNoteFilter, OutputNoteRecord},
12};
13use rand::rngs::StdRng;
14mod commands;
15use commands::{
16    account::AccountCmd,
17    exec::ExecCmd,
18    export::ExportCmd,
19    import::ImportCmd,
20    init::InitCmd,
21    new_account::{NewAccountCmd, NewWalletCmd},
22    new_transactions::{ConsumeNotesCmd, MintCmd, SendCmd, SwapCmd},
23    notes::NotesCmd,
24    sync::SyncCmd,
25    tags::TagsCmd,
26    transactions::TransactionCmd,
27};
28
29use self::utils::load_config_file;
30
31pub type CliKeyStore = FilesystemKeyStore<StdRng>;
32
33mod config;
34mod errors;
35mod faucet_details_map;
36mod info;
37mod utils;
38
39/// Config file name.
40const CLIENT_CONFIG_FILE_NAME: &str = "miden-client.toml";
41
42/// Client binary name.
43pub const CLIENT_BINARY_NAME: &str = "miden-client";
44
45/// Number of blocks that must elapse after a transaction’s reference block before it is marked
46/// stale and discarded.
47const TX_GRACEFUL_BLOCK_DELTA: u32 = 20;
48
49/// Root CLI struct.
50#[derive(Parser, Debug)]
51#[command(
52    name = "miden-client",
53    about = "The Miden client",
54    version,
55    rename_all = "kebab-case"
56)]
57pub struct Cli {
58    #[command(subcommand)]
59    action: Command,
60
61    /// Activates the executor's debug mode, which enables debug output for scripts
62    /// that were compiled and executed with this mode.
63    #[arg(short, long, default_value_t = false)]
64    debug: bool,
65}
66
67/// CLI actions.
68#[derive(Debug, Parser)]
69pub enum Command {
70    Account(AccountCmd),
71    NewAccount(NewAccountCmd),
72    NewWallet(NewWalletCmd),
73    Import(ImportCmd),
74    Export(ExportCmd),
75    Init(InitCmd),
76    Notes(NotesCmd),
77    Sync(SyncCmd),
78    /// View a summary of the current client state.
79    Info,
80    Tags(TagsCmd),
81    #[command(name = "tx")]
82    Transaction(TransactionCmd),
83    Mint(MintCmd),
84    Send(SendCmd),
85    Swap(SwapCmd),
86    ConsumeNotes(ConsumeNotesCmd),
87    Exec(ExecCmd),
88}
89
90/// CLI entry point.
91impl Cli {
92    pub async fn execute(&self) -> Result<(), CliError> {
93        let mut current_dir = std::env::current_dir()?;
94        current_dir.push(CLIENT_CONFIG_FILE_NAME);
95
96        // Check if it's an init command before anything else. When we run the init command for
97        // the first time we won't have a config file and thus creating the store would not be
98        // possible.
99        if let Command::Init(init_cmd) = &self.action {
100            init_cmd.execute(&current_dir)?;
101            return Ok(());
102        }
103
104        // Define whether we want to use the executor's debug mode based on the env var and
105        // the flag override
106        let in_debug_mode = match env::var("MIDEN_DEBUG") {
107            Ok(value) if value.to_lowercase() == "true" => true,
108            _ => self.debug,
109        };
110
111        // Create the client
112        let (cli_config, _config_path) = load_config_file()?;
113
114        let keystore = CliKeyStore::new(cli_config.secret_keys_directory.clone())
115            .map_err(CliError::KeyStore)?;
116
117        let mut builder = ClientBuilder::new()
118            .sqlite_store(cli_config.store_filepath.to_str().expect("Store path should be valid"))
119            .tonic_rpc_client(
120                &cli_config.rpc.endpoint.clone().into(),
121                Some(cli_config.rpc.timeout_ms),
122            )
123            .authenticator(Arc::new(keystore.clone()))
124            .in_debug_mode(in_debug_mode)
125            .tx_graceful_blocks(Some(TX_GRACEFUL_BLOCK_DELTA));
126
127        if let Some(delta) = cli_config.max_block_number_delta {
128            builder = builder.max_block_number_delta(delta);
129        }
130
131        let mut client = builder.build().await?;
132
133        client.ensure_genesis_in_place().await?;
134
135        // Execute CLI command
136        match &self.action {
137            Command::Account(account) => account.execute(client).await,
138            Command::NewWallet(new_wallet) => new_wallet.execute(client, keystore).await,
139            Command::NewAccount(new_account) => new_account.execute(client, keystore).await,
140            Command::Import(import) => import.execute(client, keystore).await,
141            Command::Init(_) => Ok(()),
142            Command::Info => info::print_client_info(&client).await,
143            Command::Notes(notes) => notes.execute(client).await,
144            Command::Sync(sync) => sync.execute(client).await,
145            Command::Tags(tags) => tags.execute(client).await,
146            Command::Transaction(transaction) => transaction.execute(client).await,
147            Command::Exec(execute_program) => execute_program.execute(client).await,
148            Command::Export(cmd) => cmd.execute(client, keystore).await,
149            Command::Mint(mint) => mint.execute(client).await,
150            Command::Send(send) => send.execute(client).await,
151            Command::Swap(swap) => swap.execute(client).await,
152            Command::ConsumeNotes(consume_notes) => consume_notes.execute(client).await,
153        }
154    }
155}
156
157pub fn create_dynamic_table(headers: &[&str]) -> Table {
158    let header_cells = headers
159        .iter()
160        .map(|header| Cell::new(header).add_attribute(Attribute::Bold))
161        .collect::<Vec<_>>();
162
163    let mut table = Table::new();
164    table
165        .load_preset(presets::UTF8_FULL)
166        .set_content_arrangement(ContentArrangement::DynamicFullWidth)
167        .set_header(header_cells);
168
169    table
170}
171
172/// Returns the client output note whose ID starts with `note_id_prefix`.
173///
174/// # Errors
175///
176/// - Returns [`IdPrefixFetchError::NoMatch`] if we were unable to find any note where
177///   `note_id_prefix` is a prefix of its ID.
178/// - Returns [`IdPrefixFetchError::MultipleMatches`] if there were more than one note found where
179///   `note_id_prefix` is a prefix of its ID.
180pub(crate) async fn get_output_note_with_id_prefix(
181    client: &Client,
182    note_id_prefix: &str,
183) -> Result<OutputNoteRecord, IdPrefixFetchError> {
184    let mut output_note_records = client
185        .get_output_notes(ClientNoteFilter::All)
186        .await
187        .map_err(|err| {
188            tracing::error!("Error when fetching all notes from the store: {err}");
189            IdPrefixFetchError::NoMatch(format!("note ID prefix {note_id_prefix}").to_string())
190        })?
191        .into_iter()
192        .filter(|note_record| note_record.id().to_hex().starts_with(note_id_prefix))
193        .collect::<Vec<_>>();
194
195    if output_note_records.is_empty() {
196        return Err(IdPrefixFetchError::NoMatch(
197            format!("note ID prefix {note_id_prefix}").to_string(),
198        ));
199    }
200    if output_note_records.len() > 1 {
201        let output_note_record_ids =
202            output_note_records.iter().map(OutputNoteRecord::id).collect::<Vec<_>>();
203        tracing::error!(
204            "Multiple notes found for the prefix {}: {:?}",
205            note_id_prefix,
206            output_note_record_ids
207        );
208        return Err(IdPrefixFetchError::MultipleMatches(
209            format!("note ID prefix {note_id_prefix}").to_string(),
210        ));
211    }
212
213    Ok(output_note_records
214        .pop()
215        .expect("input_note_records should always have one element"))
216}
217
218/// Returns the client account whose ID starts with `account_id_prefix`.
219///
220/// # Errors
221///
222/// - Returns [`IdPrefixFetchError::NoMatch`] if we were unable to find any account where
223///   `account_id_prefix` is a prefix of its ID.
224/// - Returns [`IdPrefixFetchError::MultipleMatches`] if there were more than one account found
225///   where `account_id_prefix` is a prefix of its ID.
226async fn get_account_with_id_prefix(
227    client: &Client,
228    account_id_prefix: &str,
229) -> Result<AccountHeader, IdPrefixFetchError> {
230    let mut accounts = client
231        .get_account_headers()
232        .await
233        .map_err(|err| {
234            tracing::error!("Error when fetching all accounts from the store: {err}");
235            IdPrefixFetchError::NoMatch(
236                format!("account ID prefix {account_id_prefix}").to_string(),
237            )
238        })?
239        .into_iter()
240        .filter(|(account_header, _)| account_header.id().to_hex().starts_with(account_id_prefix))
241        .map(|(acc, _)| acc)
242        .collect::<Vec<_>>();
243
244    if accounts.is_empty() {
245        return Err(IdPrefixFetchError::NoMatch(
246            format!("account ID prefix {account_id_prefix}").to_string(),
247        ));
248    }
249    if accounts.len() > 1 {
250        let account_ids = accounts.iter().map(AccountHeader::id).collect::<Vec<_>>();
251        tracing::error!(
252            "Multiple accounts found for the prefix {}: {:?}",
253            account_id_prefix,
254            account_ids
255        );
256        return Err(IdPrefixFetchError::MultipleMatches(
257            format!("account ID prefix {account_id_prefix}").to_string(),
258        ));
259    }
260
261    Ok(accounts.pop().expect("account_ids should always have one element"))
262}