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
39const CLIENT_CONFIG_FILE_NAME: &str = "miden-client.toml";
41
42pub const CLIENT_BINARY_NAME: &str = "miden";
44
45#[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 #[clap(short, long, default_value_t = false)]
55 debug: bool,
56}
57
58#[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 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
80impl 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 if let Command::Init(init_cmd) = &self.action {
90 init_cmd.execute(current_dir.clone())?;
91 return Ok(());
92 }
93
94 let in_debug_mode = match env::var("MIDEN_DEBUG") {
97 Ok(value) if value.to_lowercase() == "true" => true,
98 _ => self.debug,
99 };
100
101 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 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
161pub(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
209async 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}