miden_cli/
lib.rs

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