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