quantus_cli/cli/
wallet.rs

1//! `quantus wallet` subcommand - wallet operations
2use crate::{
3	chain::quantus_subxt,
4	error::QuantusError,
5	log_error, log_print, log_success, log_verbose,
6	wallet::{password::get_mnemonic_from_user, WalletManager},
7};
8use clap::Subcommand;
9use colored::Colorize;
10use sp_core::crypto::{AccountId32, Ss58Codec};
11use std::io::{self, Write};
12
13/// Wallet management commands
14#[derive(Subcommand, Debug)]
15pub enum WalletCommands {
16	/// Create a new wallet with quantum-safe keys
17	Create {
18		/// Wallet name
19		#[arg(short, long)]
20		name: String,
21
22		/// Password to encrypt the wallet (optional, will prompt if not provided)
23		#[arg(short, long)]
24		password: Option<String>,
25	},
26
27	/// View wallet information
28	View {
29		/// Wallet name to view
30		#[arg(short, long)]
31		name: Option<String>,
32
33		/// Show all wallets if no name specified
34		#[arg(short, long)]
35		all: bool,
36	},
37
38	/// Export wallet (private key or mnemonic)
39	Export {
40		/// Wallet name to export
41		#[arg(short, long)]
42		name: String,
43
44		/// Password to decrypt the wallet (optional, will prompt if not provided)
45		#[arg(short, long)]
46		password: Option<String>,
47
48		/// Export format: mnemonic, private-key
49		#[arg(short, long, default_value = "mnemonic")]
50		format: String,
51	},
52
53	/// Import wallet from mnemonic phrase
54	Import {
55		/// Wallet name
56		#[arg(short, long)]
57		name: String,
58
59		/// Mnemonic phrase (24 words, will prompt if not provided)
60		#[arg(short, long)]
61		mnemonic: Option<String>,
62
63		/// Password to encrypt the wallet (optional, will prompt if not provided)
64		#[arg(short, long)]
65		password: Option<String>,
66	},
67
68	/// List all wallets
69	List,
70
71	/// Delete a wallet
72	Delete {
73		/// Wallet name to delete
74		#[arg(short, long)]
75		name: String,
76
77		/// Skip confirmation prompt
78		#[arg(short, long)]
79		force: bool,
80	},
81
82	/// Get the nonce (transaction count) of an account
83	Nonce {
84		/// Account address to query (optional, uses wallet address if not provided)
85		#[arg(short, long)]
86		address: Option<String>,
87
88		/// Wallet name (used for address if --address not provided)
89		#[arg(short, long, required_unless_present("address"))]
90		wallet: Option<String>,
91
92		/// Password for the wallet
93		#[arg(short, long)]
94		password: Option<String>,
95	},
96}
97
98/// Get the nonce (transaction count) of an account
99pub async fn get_account_nonce(
100	quantus_client: &crate::chain::client::QuantusClient,
101	account_address: &str,
102) -> crate::error::Result<u32> {
103	log_verbose!("#️⃣ Querying nonce for account: {}", account_address.bright_green());
104
105	// Parse the SS58 address to AccountId32 (sp-core)
106	let account_id_sp = AccountId32::from_ss58check(account_address)
107		.map_err(|e| QuantusError::NetworkError(format!("Invalid SS58 address: {e:?}")))?;
108
109	log_verbose!("🔍 SP Account ID: {:?}", account_id_sp);
110
111	// Convert to subxt_core AccountId32 for storage query
112	let account_bytes: [u8; 32] = *account_id_sp.as_ref();
113	let account_id = subxt::ext::subxt_core::utils::AccountId32::from(account_bytes);
114
115	log_verbose!("🔍 SubXT Account ID: {:?}", account_id);
116
117	// Use SubXT to query System::Account storage directly (like send_subxt.rs)
118	use quantus_subxt::api;
119	let storage_addr = api::storage().system().account(account_id);
120
121	// Get the latest block hash to read from the latest state (not finalized)
122	let latest_block_hash = quantus_client.get_latest_block().await?;
123
124	let storage_at = quantus_client.client().storage().at(latest_block_hash);
125
126	let account_info = storage_at
127		.fetch_or_default(&storage_addr)
128		.await
129		.map_err(|e| QuantusError::NetworkError(format!("Failed to fetch account info: {e:?}")))?;
130
131	log_verbose!("✅ Account info retrieved with storage query!");
132	log_verbose!("🔢 Nonce: {}", account_info.nonce);
133
134	Ok(account_info.nonce)
135}
136
137/// Handle wallet commands
138pub async fn handle_wallet_command(
139	command: WalletCommands,
140	node_url: &str,
141) -> crate::error::Result<()> {
142	match command {
143		WalletCommands::Create { name, password } => {
144			log_print!("🔐 Creating new quantum wallet...");
145
146			let wallet_manager = WalletManager::new()?;
147
148			match wallet_manager.create_wallet(&name, password.as_deref()).await {
149				Ok(wallet_info) => {
150					log_success!("Wallet name: {}", name.bright_green());
151					log_success!("Address: {}", wallet_info.address.bright_cyan());
152					log_success!("Key type: {}", wallet_info.key_type.bright_yellow());
153					log_success!(
154						"Created: {}",
155						wallet_info.created_at.format("%Y-%m-%d %H:%M:%S UTC").to_string().dimmed()
156					);
157					log_success!("✅ Wallet created successfully!");
158				},
159				Err(e) => {
160					log_error!("{}", format!("❌ Failed to create wallet: {e}").red());
161					return Err(e);
162				},
163			}
164
165			Ok(())
166		},
167
168		WalletCommands::View { name, all } => {
169			log_print!("👁️  Viewing wallet information...");
170
171			let wallet_manager = WalletManager::new()?;
172
173			if all {
174				// Show all wallets (same as list command but with different header)
175				match wallet_manager.list_wallets() {
176					Ok(wallets) =>
177						if wallets.is_empty() {
178							log_print!("{}", "No wallets found.".dimmed());
179						} else {
180							log_print!("All wallets ({}):\n", wallets.len());
181
182							for (i, wallet) in wallets.iter().enumerate() {
183								log_print!(
184									"{}. {}",
185									(i + 1).to_string().bright_yellow(),
186									wallet.name.bright_green()
187								);
188								log_print!("   Address: {}", wallet.address.bright_cyan());
189								log_print!("   Type: {}", wallet.key_type.bright_yellow());
190								log_print!(
191									"   Created: {}",
192									wallet
193										.created_at
194										.format("%Y-%m-%d %H:%M:%S UTC")
195										.to_string()
196										.dimmed()
197								);
198								if i < wallets.len() - 1 {
199									log_print!();
200								}
201							}
202						},
203					Err(e) => {
204						log_error!("{}", format!("❌ Failed to view wallets: {e}").red());
205						return Err(e);
206					},
207				}
208			} else if let Some(wallet_name) = name {
209				// Show specific wallet details
210				match wallet_manager.get_wallet(&wallet_name, None) {
211					Ok(Some(wallet_info)) => {
212						log_print!("Wallet Details:\n");
213						log_print!("Name: {}", wallet_info.name.bright_green());
214						log_print!("Address: {}", wallet_info.address.bright_cyan());
215						log_print!("Key Type: {}", wallet_info.key_type.bright_yellow());
216						log_print!(
217							"Created: {}",
218							wallet_info
219								.created_at
220								.format("%Y-%m-%d %H:%M:%S UTC")
221								.to_string()
222								.dimmed()
223						);
224
225						if wallet_info.address.contains("[") {
226							log_print!(
227								"\n{}",
228								"💡 To see the full address, use the export command with password"
229									.dimmed()
230							);
231						}
232					},
233					Ok(None) => {
234						log_error!("{}", format!("❌ Wallet '{wallet_name}' not found").red());
235						log_print!(
236							"Use {} to see available wallets",
237							"quantus wallet list".bright_green()
238						);
239					},
240					Err(e) => {
241						log_error!("{}", format!("❌ Failed to view wallet: {e}").red());
242						return Err(e);
243					},
244				}
245			} else {
246				log_print!(
247					"{}",
248					"Please specify a wallet name with --name or use --all to show all wallets"
249						.yellow()
250				);
251				log_print!("Examples:");
252				log_print!("  {}", "quantus wallet view --name my-wallet".bright_green());
253				log_print!("  {}", "quantus wallet view --all".bright_green());
254			}
255
256			Ok(())
257		},
258
259		WalletCommands::Export { name, password, format } => {
260			log_print!("📤 Exporting wallet...");
261
262			if format.to_lowercase() != "mnemonic" {
263				log_error!("Only 'mnemonic' export format is currently supported.");
264				return Err(crate::error::QuantusError::Generic(
265					"Export format not supported".to_string(),
266				));
267			}
268
269			let wallet_manager = WalletManager::new()?;
270
271			match wallet_manager.export_mnemonic(&name, password.as_deref()) {
272				Ok(mnemonic) => {
273					log_success!("✅ Wallet exported successfully!");
274					log_print!("\nYour secret mnemonic phrase:");
275					log_print!("{}", "--------------------------------------------------".dimmed());
276					log_print!("{}", mnemonic.bright_yellow());
277					log_print!("{}", "--------------------------------------------------".dimmed());
278					log_print!(
279                        "\n{}",
280                        "⚠️  Keep this phrase safe and secret. Anyone with this phrase can access your funds."
281                            .bright_red()
282                    );
283				},
284				Err(e) => {
285					log_error!("{}", format!("❌ Failed to export wallet: {e}").red());
286					return Err(e);
287				},
288			}
289
290			Ok(())
291		},
292
293		WalletCommands::Import { name, mnemonic, password } => {
294			log_print!("📥 Importing wallet...");
295
296			let wallet_manager = WalletManager::new()?;
297
298			// Get mnemonic from user if not provided
299			let mnemonic_phrase =
300				if let Some(mnemonic) = mnemonic { mnemonic } else { get_mnemonic_from_user()? };
301
302			// Get password from user if not provided
303			let final_password =
304				crate::wallet::password::get_wallet_password(&name, password, None)?;
305
306			match wallet_manager
307				.import_wallet(&name, &mnemonic_phrase, Some(&final_password))
308				.await
309			{
310				Ok(wallet_info) => {
311					log_success!("Wallet name: {}", name.bright_green());
312					log_success!("Address: {}", wallet_info.address.bright_cyan());
313					log_success!("Key type: {}", wallet_info.key_type.bright_yellow());
314					log_success!(
315						"Imported: {}",
316						wallet_info.created_at.format("%Y-%m-%d %H:%M:%S UTC").to_string().dimmed()
317					);
318					log_success!("✅ Wallet imported successfully!");
319				},
320				Err(e) => {
321					log_error!("{}", format!("❌ Failed to import wallet: {e}").red());
322					return Err(e);
323				},
324			}
325
326			Ok(())
327		},
328
329		WalletCommands::List => {
330			log_print!("📋 Listing all wallets...");
331
332			let wallet_manager = WalletManager::new()?;
333
334			match wallet_manager.list_wallets() {
335				Ok(wallets) =>
336					if wallets.is_empty() {
337						log_print!("{}", "No wallets found.".dimmed());
338						log_print!(
339							"Create a new wallet with: {}",
340							"quantus wallet create --name <name>".bright_green()
341						);
342					} else {
343						log_print!("Found {} wallet(s):\n", wallets.len());
344
345						for (i, wallet) in wallets.iter().enumerate() {
346							log_print!(
347								"{}. {}",
348								(i + 1).to_string().bright_yellow(),
349								wallet.name.bright_green()
350							);
351							log_print!("   Address: {}", wallet.address.bright_cyan());
352							log_print!("   Type: {}", wallet.key_type.bright_yellow());
353							log_print!(
354								"   Created: {}",
355								wallet
356									.created_at
357									.format("%Y-%m-%d %H:%M:%S UTC")
358									.to_string()
359									.dimmed()
360							);
361							if i < wallets.len() - 1 {
362								log_print!();
363							}
364						}
365
366						log_print!(
367							"\n{}",
368							"💡 Use 'quantus wallet view --name <wallet>' to see full details"
369								.dimmed()
370						);
371					},
372				Err(e) => {
373					log_error!("{}", format!("❌ Failed to list wallets: {e}").red());
374					return Err(e);
375				},
376			}
377
378			Ok(())
379		},
380
381		WalletCommands::Delete { name, force } => {
382			log_print!("🗑️  Deleting wallet...");
383
384			let wallet_manager = WalletManager::new()?;
385
386			// Check if wallet exists first
387			match wallet_manager.get_wallet(&name, None) {
388				Ok(Some(wallet_info)) => {
389					// Show wallet info before deletion
390					log_print!("Wallet to delete:");
391					log_print!("  Name: {}", wallet_info.name.bright_green());
392					log_print!("  Address: {}", wallet_info.address.bright_cyan());
393					log_print!("  Type: {}", wallet_info.key_type.bright_yellow());
394					log_print!(
395						"  Created: {}",
396						wallet_info.created_at.format("%Y-%m-%d %H:%M:%S UTC").to_string().dimmed()
397					);
398
399					// Confirmation prompt unless --force is used
400					if !force {
401						log_print!("\n{}", "⚠️  This action cannot be undone!".bright_red());
402						log_print!("Type the wallet name to confirm deletion:");
403
404						print!("Confirm wallet name: ");
405						io::stdout().flush().unwrap();
406
407						let mut input = String::new();
408						io::stdin().read_line(&mut input).unwrap();
409						let input = input.trim();
410
411						if input != name {
412							log_print!(
413								"{}",
414								"❌ Wallet name doesn't match. Deletion cancelled.".red()
415							);
416							return Ok(());
417						}
418					}
419
420					// Perform deletion
421					match wallet_manager.delete_wallet(&name) {
422						Ok(true) => {
423							log_success!("✅ Wallet '{}' deleted successfully!", name);
424						},
425						Ok(false) => {
426							log_error!("{}", format!("❌ Wallet '{name}' was not found").red());
427						},
428						Err(e) => {
429							log_error!("{}", format!("❌ Failed to delete wallet: {e}").red());
430							return Err(e);
431						},
432					}
433				},
434				Ok(None) => {
435					log_error!("{}", format!("❌ Wallet '{name}' not found").red());
436					log_print!(
437						"Use {} to see available wallets",
438						"quantus wallet list".bright_green()
439					);
440				},
441				Err(e) => {
442					log_error!("{}", format!("❌ Failed to check wallet: {e}").red());
443					return Err(e);
444				},
445			}
446
447			Ok(())
448		},
449
450		WalletCommands::Nonce { address, wallet, password } => {
451			log_print!("🔢 Querying account nonce...");
452
453			let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
454
455			// Determine which address to query
456			let target_address = match (address, wallet) {
457				(Some(addr), _) => {
458					// Validate the provided address
459					AccountId32::from_ss58check(&addr)
460						.map_err(|e| QuantusError::Generic(format!("Invalid address: {e:?}")))?;
461					addr
462				},
463				(None, Some(wallet_name)) => {
464					// Load wallet and get its address
465					let keypair =
466						crate::wallet::load_keypair_from_wallet(&wallet_name, password, None)?;
467					keypair.to_account_id_ss58check()
468				},
469				(None, None) => {
470					// This case should be prevented by clap's `required_unless_present`
471					unreachable!("Either --address or --wallet must be provided");
472				},
473			};
474
475			log_print!("Account: {}", target_address.bright_cyan());
476
477			match get_account_nonce(&quantus_client, &target_address).await {
478				Ok(nonce) => {
479					log_success!("Nonce: {}", nonce.to_string().bright_green());
480				},
481				Err(e) => {
482					log_print!("❌ Failed to get nonce: {}", e);
483					return Err(e);
484				},
485			}
486
487			Ok(())
488		},
489	}
490}