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
46#[derive(Parser, Debug)]
48#[clap(name = "Miden", about = "Miden client", version, rename_all = "kebab-case")]
49pub struct Cli {
50 #[clap(subcommand)]
51 action: Command,
52
53 #[clap(short, long, default_value_t = false)]
56 debug: bool,
57}
58
59#[derive(Debug, Parser)]
61pub enum Command {
62 Account(AccountCmd),
63 NewAccount(NewAccountCmd),
64 NewWallet(NewWalletCmd),
65 Import(ImportCmd),
66 Export(ExportCmd),
67 Init(InitCmd),
68 Notes(NotesCmd),
69 Sync(SyncCmd),
70 Info,
72 Tags(TagsCmd),
73 #[clap(name = "tx")]
74 Transaction(TransactionCmd),
75 Mint(MintCmd),
76 Send(SendCmd),
77 Swap(SwapCmd),
78 ConsumeNotes(ConsumeNotesCmd),
79 Exec(ExecCmd),
80}
81
82impl Cli {
84 pub async fn execute(&self) -> Result<(), CliError> {
85 let mut current_dir = std::env::current_dir()?;
86 current_dir.push(CLIENT_CONFIG_FILE_NAME);
87
88 if let Command::Init(init_cmd) = &self.action {
92 init_cmd.execute(¤t_dir)?;
93 return Ok(());
94 }
95
96 let in_debug_mode = match env::var("MIDEN_DEBUG") {
99 Ok(value) if value.to_lowercase() == "true" => true,
100 _ => self.debug,
101 };
102
103 let (cli_config, _config_path) = load_config_file()?;
105 let store = SqliteStore::new(cli_config.store_filepath.clone())
106 .await
107 .map_err(ClientError::StoreError)?;
108 let store = Arc::new(store);
109
110 let mut rng = rand::rng();
111 let coin_seed: [u64; 4] = rng.random();
112
113 let rng = RpoRandomCoin::new(coin_seed.map(Felt::new));
114 let keystore = CliKeyStore::new(cli_config.secret_keys_directory.clone())
115 .map_err(CliError::KeyStore)?;
116
117 let client = Client::new(
118 Arc::new(TonicRpcClient::new(
119 &cli_config.rpc.endpoint.clone().into(),
120 cli_config.rpc.timeout_ms,
121 )),
122 Box::new(rng),
123 store as Arc<dyn Store>,
124 Arc::new(keystore.clone()),
125 in_debug_mode,
126 );
127
128 match &self.action {
130 Command::Account(account) => account.execute(client).await,
131 Command::NewWallet(new_wallet) => new_wallet.execute(client, keystore).await,
132 Command::NewAccount(new_account) => new_account.execute(client, keystore).await,
133 Command::Import(import) => import.execute(client, keystore).await,
134 Command::Init(_) => Ok(()),
135 Command::Info => info::print_client_info(&client, &cli_config).await,
136 Command::Notes(notes) => notes.execute(client).await,
137 Command::Sync(sync) => sync.execute(client).await,
138 Command::Tags(tags) => tags.execute(client).await,
139 Command::Transaction(transaction) => transaction.execute(client).await,
140 Command::Exec(execute_program) => execute_program.execute(client).await,
141 Command::Export(cmd) => cmd.execute(client, keystore).await,
142 Command::Mint(mint) => mint.execute(client).await,
143 Command::Send(send) => send.execute(client).await,
144 Command::Swap(swap) => swap.execute(client).await,
145 Command::ConsumeNotes(consume_notes) => consume_notes.execute(client).await,
146 }
147 }
148}
149
150pub fn create_dynamic_table(headers: &[&str]) -> Table {
151 let header_cells = headers
152 .iter()
153 .map(|header| Cell::new(header).add_attribute(Attribute::Bold))
154 .collect::<Vec<_>>();
155
156 let mut table = Table::new();
157 table
158 .load_preset(presets::UTF8_FULL)
159 .set_content_arrangement(ContentArrangement::DynamicFullWidth)
160 .set_header(header_cells);
161
162 table
163}
164
165pub(crate) async fn get_output_note_with_id_prefix(
174 client: &Client,
175 note_id_prefix: &str,
176) -> Result<OutputNoteRecord, IdPrefixFetchError> {
177 let mut output_note_records = client
178 .get_output_notes(ClientNoteFilter::All)
179 .await
180 .map_err(|err| {
181 tracing::error!("Error when fetching all notes from the store: {err}");
182 IdPrefixFetchError::NoMatch(format!("note ID prefix {note_id_prefix}").to_string())
183 })?
184 .into_iter()
185 .filter(|note_record| note_record.id().to_hex().starts_with(note_id_prefix))
186 .collect::<Vec<_>>();
187
188 if output_note_records.is_empty() {
189 return Err(IdPrefixFetchError::NoMatch(
190 format!("note ID prefix {note_id_prefix}").to_string(),
191 ));
192 }
193 if output_note_records.len() > 1 {
194 let output_note_record_ids =
195 output_note_records.iter().map(OutputNoteRecord::id).collect::<Vec<_>>();
196 tracing::error!(
197 "Multiple notes found for the prefix {}: {:?}",
198 note_id_prefix,
199 output_note_record_ids
200 );
201 return Err(IdPrefixFetchError::MultipleMatches(
202 format!("note ID prefix {note_id_prefix}").to_string(),
203 ));
204 }
205
206 Ok(output_note_records
207 .pop()
208 .expect("input_note_records should always have one element"))
209}
210
211async fn get_account_with_id_prefix(
220 client: &Client,
221 account_id_prefix: &str,
222) -> Result<AccountHeader, IdPrefixFetchError> {
223 let mut accounts = client
224 .get_account_headers()
225 .await
226 .map_err(|err| {
227 tracing::error!("Error when fetching all accounts from the store: {err}");
228 IdPrefixFetchError::NoMatch(
229 format!("account ID prefix {account_id_prefix}").to_string(),
230 )
231 })?
232 .into_iter()
233 .filter(|(account_header, _)| account_header.id().to_hex().starts_with(account_id_prefix))
234 .map(|(acc, _)| acc)
235 .collect::<Vec<_>>();
236
237 if accounts.is_empty() {
238 return Err(IdPrefixFetchError::NoMatch(
239 format!("account ID prefix {account_id_prefix}").to_string(),
240 ));
241 }
242 if accounts.len() > 1 {
243 let account_ids = accounts.iter().map(AccountHeader::id).collect::<Vec<_>>();
244 tracing::error!(
245 "Multiple accounts found for the prefix {}: {:?}",
246 account_id_prefix,
247 account_ids
248 );
249 return Err(IdPrefixFetchError::MultipleMatches(
250 format!("account ID prefix {account_id_prefix}").to_string(),
251 ));
252 }
253
254 Ok(accounts.pop().expect("account_ids should always have one element"))
255}