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