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
39const CLIENT_CONFIG_FILE_NAME: &str = "miden-client.toml";
41
42pub const CLIENT_BINARY_NAME: &str = "miden-client";
44
45const TX_GRACEFUL_BLOCK_DELTA: u32 = 20;
48
49#[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 #[arg(short, long, default_value_t = false)]
64 debug: bool,
65}
66
67#[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 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
90impl 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 if let Command::Init(init_cmd) = &self.action {
100 init_cmd.execute(¤t_dir)?;
101 return Ok(());
102 }
103
104 let in_debug_mode = match env::var("MIDEN_DEBUG") {
107 Ok(value) if value.to_lowercase() == "true" => true,
108 _ => self.debug,
109 };
110
111 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 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
172pub(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
218async 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}