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
40const CLIENT_CONFIG_FILE_NAME: &str = "miden-client.toml";
42
43pub const CLIENT_BINARY_NAME: &str = "miden";
45
46const TX_GRACEFUL_BLOCK_DELTA: u32 = 20;
49
50#[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 #[clap(short, long, default_value_t = false)]
60 debug: bool,
61}
62
63#[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 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
86impl 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 if let Command::Init(init_cmd) = &self.action {
96 init_cmd.execute(¤t_dir)?;
97 return Ok(());
98 }
99
100 let in_debug_mode = match env::var("MIDEN_DEBUG") {
103 Ok(value) if value.to_lowercase() == "true" => true,
104 _ => self.debug,
105 };
106
107 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 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
171pub(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
217async 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}