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::clear_config::ClearConfigCmd;
20use commands::exec::ExecCmd;
21use commands::export::ExportCmd;
22use commands::import::ImportCmd;
23use commands::init::InitCmd;
24use commands::new_account::{NewAccountCmd, NewWalletCmd};
25use commands::new_transactions::{ConsumeNotesCmd, MintCmd, SendCmd, SwapCmd};
26use commands::notes::NotesCmd;
27use commands::sync::SyncCmd;
28use commands::tags::TagsCmd;
29use commands::transactions::TransactionCmd;
30
31use self::utils::{config_file_exists, load_config_file};
32use crate::commands::address::AddressCmd;
33
34pub type CliKeyStore = FilesystemKeyStore<StdRng>;
35
36mod config;
37mod errors;
38mod faucet_details_map;
39mod info;
40mod utils;
41
42pub use config::MIDEN_DIR;
44
45const CLIENT_CONFIG_FILE_NAME: &str = "miden-client.toml";
47
48pub fn client_binary_name() -> OsString {
53 std::env::current_exe()
54 .inspect_err(|e| {
55 eprintln!(
56 "WARNING: Couldn't obtain the path of the current executable because of {e}.\
57 Defaulting to miden-client."
58 );
59 })
60 .and_then(|executable_path| {
61 executable_path.file_name().map(std::ffi::OsStr::to_os_string).ok_or(
62 std::io::Error::other("Couldn't obtain the file name of the current executable"),
63 )
64 })
65 .unwrap_or(OsString::from("miden-client"))
66}
67
68const TX_GRACEFUL_BLOCK_DELTA: u32 = 20;
71
72#[derive(Parser, Debug)]
74#[command(
75 name = "miden-client",
76 about = "The Miden client",
77 version,
78 rename_all = "kebab-case"
79)]
80#[command(multicall(true))]
81pub struct MidenClientCli {
82 #[command(subcommand)]
83 behavior: Behavior,
84}
85
86impl From<MidenClientCli> for Cli {
87 fn from(value: MidenClientCli) -> Self {
88 match value.behavior {
89 Behavior::MidenClient { cli } => cli,
90 Behavior::External(args) => Cli::parse_from(args).set_external(),
91 }
92 }
93}
94
95#[derive(Debug, Subcommand)]
96#[command(rename_all = "kebab-case")]
97enum Behavior {
98 MidenClient {
100 #[command(flatten)]
101 cli: Cli,
102 },
103
104 #[command(external_subcommand)]
109 External(Vec<OsString>),
110}
111
112#[derive(Parser, Debug)]
113#[command(name = "miden-client")]
114pub struct Cli {
115 #[arg(short, long, default_value_t = false)]
118 debug: bool,
119
120 #[command(subcommand)]
121 action: Command,
122
123 #[arg(skip)]
127 #[allow(unused)]
128 external: bool,
129}
130
131#[derive(Debug, Parser)]
133pub enum Command {
134 Account(AccountCmd),
135 NewAccount(NewAccountCmd),
136 NewWallet(NewWalletCmd),
137 Import(ImportCmd),
138 Export(ExportCmd),
139 Init(InitCmd),
140 ClearConfig(ClearConfigCmd),
141 Notes(NotesCmd),
142 Sync(SyncCmd),
143 Info,
145 Tags(TagsCmd),
146 Address(AddressCmd),
147 #[command(name = "tx")]
148 Transaction(TransactionCmd),
149 Mint(MintCmd),
150 Send(SendCmd),
151 Swap(SwapCmd),
152 ConsumeNotes(ConsumeNotesCmd),
153 Exec(ExecCmd),
154}
155
156impl Cli {
158 pub async fn execute(&self) -> Result<(), CliError> {
159 match &self.action {
161 Command::Init(init_cmd) => {
162 init_cmd.execute()?;
163 return Ok(());
164 },
165 Command::ClearConfig(clear_config_cmd) => {
166 clear_config_cmd.execute()?;
167 return Ok(());
168 },
169 _ => {},
170 }
171
172 if !config_file_exists()? {
174 let init_cmd = InitCmd::default();
175 init_cmd.execute()?;
176 }
177
178 let in_debug_mode = match env::var("MIDEN_DEBUG") {
181 Ok(value) if value.to_lowercase() == "true" => DebugMode::Enabled,
182 _ => DebugMode::Disabled,
183 };
184
185 let (cli_config, _config_path) = load_config_file()?;
187
188 let keystore = CliKeyStore::new(cli_config.secret_keys_directory.clone())
189 .map_err(CliError::KeyStore)?;
190
191 let mut builder = ClientBuilder::new()
192 .sqlite_store(cli_config.store_filepath.clone())
193 .grpc_client(&cli_config.rpc.endpoint.clone().into(), Some(cli_config.rpc.timeout_ms))
194 .authenticator(Arc::new(keystore.clone()))
195 .in_debug_mode(in_debug_mode)
196 .tx_graceful_blocks(Some(TX_GRACEFUL_BLOCK_DELTA));
197
198 if let Some(delta) = cli_config.max_block_number_delta {
199 builder = builder.max_block_number_delta(delta);
200 }
201
202 if let Some(tl_config) = cli_config.note_transport {
203 let client =
204 GrpcNoteTransportClient::connect(tl_config.endpoint.clone(), tl_config.timeout_ms)
205 .await
206 .map_err(|e| CliError::from(ClientError::from(e)))?;
207 builder = builder.note_transport(Arc::new(client));
208 }
209
210 let client = builder.build().await?;
211
212 match &self.action {
214 Command::Account(account) => account.execute(client).await,
215 Command::NewWallet(new_wallet) => Box::pin(new_wallet.execute(client, keystore)).await,
216 Command::NewAccount(new_account) => {
217 Box::pin(new_account.execute(client, keystore)).await
218 },
219 Command::Import(import) => import.execute(client, keystore).await,
220 Command::Init(_) | Command::ClearConfig(_) => Ok(()), Command::Info => info::print_client_info(&client).await,
222 Command::Notes(notes) => Box::pin(notes.execute(client)).await,
223 Command::Sync(sync) => sync.execute(client).await,
224 Command::Tags(tags) => tags.execute(client).await,
225 Command::Address(addresses) => addresses.execute(client).await,
226 Command::Transaction(transaction) => transaction.execute(client).await,
227 Command::Exec(execute_program) => Box::pin(execute_program.execute(client)).await,
228 Command::Export(cmd) => cmd.execute(client, keystore).await,
229 Command::Mint(mint) => Box::pin(mint.execute(client)).await,
230 Command::Send(send) => Box::pin(send.execute(client)).await,
231 Command::Swap(swap) => Box::pin(swap.execute(client)).await,
232 Command::ConsumeNotes(consume_notes) => Box::pin(consume_notes.execute(client)).await,
233 }
234 }
235
236 fn set_external(mut self) -> Self {
237 self.external = true;
238 self
239 }
240}
241
242pub fn create_dynamic_table(headers: &[&str]) -> Table {
243 let header_cells = headers
244 .iter()
245 .map(|header| Cell::new(header).add_attribute(Attribute::Bold))
246 .collect::<Vec<_>>();
247
248 let mut table = Table::new();
249 table
250 .load_preset(presets::UTF8_FULL)
251 .set_content_arrangement(ContentArrangement::DynamicFullWidth)
252 .set_header(header_cells);
253
254 table
255}
256
257pub(crate) async fn get_output_note_with_id_prefix<AUTH: TransactionAuthenticator + Sync>(
266 client: &Client<AUTH>,
267 note_id_prefix: &str,
268) -> Result<OutputNoteRecord, IdPrefixFetchError> {
269 let mut output_note_records = client
270 .get_output_notes(ClientNoteFilter::All)
271 .await
272 .map_err(|err| {
273 tracing::error!("Error when fetching all notes from the store: {err}");
274 IdPrefixFetchError::NoMatch(format!("note ID prefix {note_id_prefix}").to_string())
275 })?
276 .into_iter()
277 .filter(|note_record| note_record.id().to_hex().starts_with(note_id_prefix))
278 .collect::<Vec<_>>();
279
280 if output_note_records.is_empty() {
281 return Err(IdPrefixFetchError::NoMatch(
282 format!("note ID prefix {note_id_prefix}").to_string(),
283 ));
284 }
285 if output_note_records.len() > 1 {
286 let output_note_record_ids =
287 output_note_records.iter().map(OutputNoteRecord::id).collect::<Vec<_>>();
288 tracing::error!(
289 "Multiple notes found for the prefix {}: {:?}",
290 note_id_prefix,
291 output_note_record_ids
292 );
293 return Err(IdPrefixFetchError::MultipleMatches(
294 format!("note ID prefix {note_id_prefix}").to_string(),
295 ));
296 }
297
298 Ok(output_note_records
299 .pop()
300 .expect("input_note_records should always have one element"))
301}
302
303async fn get_account_with_id_prefix<AUTH>(
312 client: &Client<AUTH>,
313 account_id_prefix: &str,
314) -> Result<AccountHeader, IdPrefixFetchError> {
315 let mut accounts = client
316 .get_account_headers()
317 .await
318 .map_err(|err| {
319 tracing::error!("Error when fetching all accounts from the store: {err}");
320 IdPrefixFetchError::NoMatch(
321 format!("account ID prefix {account_id_prefix}").to_string(),
322 )
323 })?
324 .into_iter()
325 .filter(|(account_header, _)| account_header.id().to_hex().starts_with(account_id_prefix))
326 .map(|(acc, _)| acc)
327 .collect::<Vec<_>>();
328
329 if accounts.is_empty() {
330 return Err(IdPrefixFetchError::NoMatch(
331 format!("account ID prefix {account_id_prefix}").to_string(),
332 ));
333 }
334 if accounts.len() > 1 {
335 let account_ids = accounts.iter().map(AccountHeader::id).collect::<Vec<_>>();
336 tracing::error!(
337 "Multiple accounts found for the prefix {}: {:?}",
338 account_id_prefix,
339 account_ids
340 );
341 return Err(IdPrefixFetchError::MultipleMatches(
342 format!("account ID prefix {account_id_prefix}").to_string(),
343 ));
344 }
345
346 Ok(accounts.pop().expect("account_ids should always have one element"))
347}