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