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