1use crate::{
3 chain::quantus_subxt,
4 cli::address_format::QuantusSS58,
5 error::QuantusError,
6 log_error, log_print, log_success, log_verbose,
7 wallet::{password::get_mnemonic_from_user, WalletManager, DEFAULT_DERIVATION_PATH},
8};
9use clap::Subcommand;
10use colored::Colorize;
11use sp_core::crypto::{AccountId32 as SpAccountId32, Ss58Codec};
12use std::io::{self, Write};
13
14#[derive(Subcommand, Debug)]
16pub enum WalletCommands {
17 Create {
19 #[arg(short, long)]
21 name: String,
22
23 #[arg(short, long)]
25 password: Option<String>,
26
27 #[arg(short = 'd', long, default_value = DEFAULT_DERIVATION_PATH)]
29 derivation_path: String,
30
31 #[arg(long)]
33 no_derivation: bool,
34 },
35
36 View {
38 #[arg(short, long)]
40 name: Option<String>,
41
42 #[arg(short, long)]
44 all: bool,
45 },
46
47 Export {
49 #[arg(short, long)]
51 name: String,
52
53 #[arg(short, long)]
55 password: Option<String>,
56
57 #[arg(short, long, default_value = "mnemonic")]
59 format: String,
60 },
61
62 Import {
64 #[arg(short, long)]
66 name: String,
67
68 #[arg(short, long)]
70 mnemonic: Option<String>,
71
72 #[arg(short, long)]
74 password: Option<String>,
75
76 #[arg(short = 'd', long, default_value = DEFAULT_DERIVATION_PATH)]
78 derivation_path: String,
79
80 #[arg(long)]
82 no_derivation: bool,
83 },
84
85 FromSeed {
87 #[arg(short, long)]
89 name: String,
90
91 #[arg(short, long)]
93 seed: String,
94
95 #[arg(short, long)]
97 password: Option<String>,
98 },
99
100 List,
102
103 Delete {
105 #[arg(short, long)]
107 name: String,
108
109 #[arg(short, long)]
111 force: bool,
112 },
113
114 Nonce {
116 #[arg(short, long)]
118 address: Option<String>,
119
120 #[arg(short, long, required_unless_present("address"))]
122 wallet: Option<String>,
123
124 #[arg(short, long)]
126 password: Option<String>,
127 },
128}
129
130pub async fn get_account_nonce(
132 quantus_client: &crate::chain::client::QuantusClient,
133 account_address: &str,
134) -> crate::error::Result<u32> {
135 log_verbose!("#ļøā£ Querying nonce for account: {}", account_address.bright_green());
136
137 let (account_id_sp, _) = SpAccountId32::from_ss58check_with_version(account_address)
139 .map_err(|e| QuantusError::NetworkError(format!("Invalid SS58 address: {e:?}")))?;
140
141 log_verbose!("š SP Account ID: {:?}", account_id_sp);
142
143 let account_bytes: [u8; 32] = *account_id_sp.as_ref();
145 let account_id = subxt::ext::subxt_core::utils::AccountId32::from(account_bytes);
146
147 log_verbose!("š SubXT Account ID: {:?}", account_id);
148
149 use quantus_subxt::api;
151 let storage_addr = api::storage().system().account(account_id);
152
153 let latest_block_hash = quantus_client.get_latest_block().await?;
155
156 let storage_at = quantus_client.client().storage().at(latest_block_hash);
157
158 let account_info = storage_at
159 .fetch_or_default(&storage_addr)
160 .await
161 .map_err(|e| QuantusError::NetworkError(format!("Failed to fetch account info: {e:?}")))?;
162
163 log_verbose!("ā
Account info retrieved with storage query!");
164 log_verbose!("š¢ Nonce: {}", account_info.nonce);
165
166 Ok(account_info.nonce)
167}
168
169async fn fetch_high_security_status(
172 quantus_client: &crate::chain::client::QuantusClient,
173 account_ss58: &str,
174) -> crate::error::Result<Option<(String, String)>> {
175 use quantus_subxt::api::runtime_types::qp_scheduler::BlockNumberOrTimestamp;
176
177 let (account_id_sp, _) = SpAccountId32::from_ss58check_with_version(account_ss58)
178 .map_err(|e| QuantusError::Generic(format!("Invalid SS58 for HS lookup: {e:?}")))?;
179 let account_bytes: [u8; 32] = *account_id_sp.as_ref();
180 let account_id = subxt::ext::subxt_core::utils::AccountId32::from(account_bytes);
181
182 let storage_addr = quantus_subxt::api::storage()
183 .reversible_transfers()
184 .high_security_accounts(account_id);
185 let latest = quantus_client.get_latest_block().await?;
186 let value = quantus_client
187 .client()
188 .storage()
189 .at(latest)
190 .fetch(&storage_addr)
191 .await
192 .map_err(|e| QuantusError::NetworkError(format!("Fetch HS storage: {e:?}")))?;
193
194 let Some(data) = value else {
195 return Ok(None);
196 };
197
198 let interceptor_ss58 = data.interceptor.to_quantus_ss58();
199 let delay_str = match data.delay {
200 BlockNumberOrTimestamp::BlockNumber(blocks) => format!("{} blocks", blocks),
201 BlockNumberOrTimestamp::Timestamp(ms) => format!("{} seconds", ms / 1000),
202 };
203 Ok(Some((interceptor_ss58, delay_str)))
204}
205
206async fn fetch_guardian_for_list(
209 quantus_client: &crate::chain::client::QuantusClient,
210 account_ss58: &str,
211) -> crate::error::Result<Vec<String>> {
212 let account_id_sp = SpAccountId32::from_ss58check(account_ss58)
213 .map_err(|e| QuantusError::Generic(format!("Invalid SS58 for interceptor_index: {e:?}")))?;
214 let account_bytes: [u8; 32] = *account_id_sp.as_ref();
215 let account_id = subxt::ext::subxt_core::utils::AccountId32::from(account_bytes);
216
217 let storage_addr = quantus_subxt::api::storage()
218 .reversible_transfers()
219 .interceptor_index(account_id);
220 let latest = quantus_client.get_latest_block().await?;
221 let value = quantus_client
222 .client()
223 .storage()
224 .at(latest)
225 .fetch(&storage_addr)
226 .await
227 .map_err(|e| QuantusError::NetworkError(format!("Fetch interceptor_index: {e:?}")))?;
228
229 let list = value
230 .map(|bounded| bounded.0.iter().map(|a| a.to_quantus_ss58()).collect())
231 .unwrap_or_default();
232 Ok(list)
233}
234
235async fn fetch_pending_transfers_for_guardian(
238 quantus_client: &crate::chain::client::QuantusClient,
239 entrusted_ss58: &[String],
240) -> crate::error::Result<(u32, Vec<(String, u32)>)> {
241 let latest = quantus_client.get_latest_block().await?;
242 let storage = quantus_client.client().storage().at(latest);
243 let mut total = 0u32;
244 let mut per_account = Vec::with_capacity(entrusted_ss58.len());
245
246 for ss58 in entrusted_ss58 {
247 let account_id_sp = SpAccountId32::from_ss58check(ss58).map_err(|e| {
248 QuantusError::Generic(format!("Invalid SS58 for pending lookup: {e:?}"))
249 })?;
250 let account_bytes: [u8; 32] = *account_id_sp.as_ref();
251 let account_id = subxt::ext::subxt_core::utils::AccountId32::from(account_bytes);
252
253 let addr = quantus_subxt::api::storage()
254 .reversible_transfers()
255 .pending_transfers_by_sender(account_id);
256 let value = storage.fetch(&addr).await.map_err(|e| {
257 QuantusError::NetworkError(format!("Fetch pending_transfers_by_sender: {e:?}"))
258 })?;
259
260 let count = value.map(|bounded| bounded.0.len() as u32).unwrap_or(0);
261 total += count;
262 per_account.push((ss58.clone(), count));
263 }
264
265 Ok((total, per_account))
266}
267
268pub async fn handle_wallet_command(
270 command: WalletCommands,
271 node_url: &str,
272) -> crate::error::Result<()> {
273 match command {
274 WalletCommands::Create { name, password, derivation_path, no_derivation } => {
275 log_print!("š Creating new quantum wallet...");
276
277 let wallet_manager = WalletManager::new()?;
278
279 let result = if no_derivation {
281 wallet_manager.create_wallet_no_derivation(&name, password.as_deref()).await
283 } else if derivation_path == DEFAULT_DERIVATION_PATH {
284 wallet_manager.create_wallet(&name, password.as_deref()).await
285 } else {
286 wallet_manager
287 .create_wallet_with_derivation_path(
288 &name,
289 password.as_deref(),
290 &derivation_path,
291 )
292 .await
293 };
294
295 match result {
296 Ok(wallet_info) => {
297 log_success!("Wallet name: {}", name.bright_green());
298 log_success!("Address: {}", wallet_info.address.bright_cyan());
299 log_success!("Key type: {}", wallet_info.key_type.bright_yellow());
300 log_success!(
301 "Derivation path: {}",
302 wallet_info.derivation_path.bright_magenta()
303 );
304 log_success!(
305 "Created: {}",
306 wallet_info.created_at.format("%Y-%m-%d %H:%M:%S UTC").to_string().dimmed()
307 );
308 log_success!("ā
Wallet created successfully!");
309 },
310 Err(e) => {
311 log_error!("{}", format!("ā Failed to create wallet: {e}").red());
312 return Err(e);
313 },
314 }
315
316 Ok(())
317 },
318
319 WalletCommands::View { name, all } => {
320 log_print!("šļø Viewing wallet information...");
321
322 let wallet_manager = WalletManager::new()?;
323
324 if all {
325 match wallet_manager.list_wallets() {
327 Ok(wallets) =>
328 if wallets.is_empty() {
329 log_print!("{}", "No wallets found.".dimmed());
330 } else {
331 log_print!("All wallets ({}):\n", wallets.len());
332
333 for (i, wallet) in wallets.iter().enumerate() {
334 log_print!(
335 "{}. {}",
336 (i + 1).to_string().bright_yellow(),
337 wallet.name.bright_green()
338 );
339 log_print!(" Address: {}", wallet.address.bright_cyan());
340 log_print!(" Type: {}", wallet.key_type.bright_yellow());
341 log_print!(
342 " Derivation Path: {}",
343 wallet.derivation_path.bright_magenta()
344 );
345 log_print!(
346 " Created: {}",
347 wallet
348 .created_at
349 .format("%Y-%m-%d %H:%M:%S UTC")
350 .to_string()
351 .dimmed()
352 );
353 if i < wallets.len() - 1 {
354 log_print!();
355 }
356 }
357 },
358 Err(e) => {
359 log_error!("{}", format!("ā Failed to view wallets: {e}").red());
360 return Err(e);
361 },
362 }
363 } else if let Some(wallet_name) = name {
364 match wallet_manager.get_wallet(&wallet_name, None) {
366 Ok(Some(wallet_info)) => {
367 log_print!("Wallet Details:\n");
368 log_print!("Name: {}", wallet_info.name.bright_green());
369 log_print!("Address: {}", wallet_info.address.bright_cyan());
370 log_print!("Key Type: {}", wallet_info.key_type.bright_yellow());
371 log_print!(
372 "Derivation Path: {}",
373 wallet_info.derivation_path.bright_magenta()
374 );
375 log_print!(
376 "Created: {}",
377 wallet_info
378 .created_at
379 .format("%Y-%m-%d %H:%M:%S UTC")
380 .to_string()
381 .dimmed()
382 );
383
384 if wallet_info.address.contains("[") {
385 log_print!(
386 "\n{}",
387 "š” To see the full address, use the export command with password"
388 .dimmed()
389 );
390 }
391
392 if !wallet_info.address.contains("[") {
395 if let Ok(quantus_client) =
396 crate::chain::client::QuantusClient::new(node_url).await
397 {
398 match fetch_high_security_status(
399 &quantus_client,
400 &wallet_info.address,
401 )
402 .await
403 {
404 Ok(Some((interceptor_ss58, delay_str))) => {
405 log_print!(
406 "\nš”ļø High Security: {}",
407 "ENABLED".bright_green().bold()
408 );
409 log_print!(
410 " Guardian/Interceptor: {}",
411 interceptor_ss58.bright_cyan()
412 );
413 log_print!(" Delay: {}", delay_str.bright_yellow());
414 },
415 Ok(None) => {
416 log_print!("\nš”ļø High Security: {}", "DISABLED".dimmed());
417 },
418 Err(e) => {
419 log_verbose!("High Security status skipped: {}", e);
420 log_print!(
421 "\n{}",
422 "š” Run quantus high-security status --account <address> to check on-chain"
423 .dimmed()
424 );
425 },
426 }
427
428 if let Ok(entrusted) =
430 fetch_guardian_for_list(&quantus_client, &wallet_info.address)
431 .await
432 {
433 if entrusted.is_empty() {
434 log_print!("š”ļø Guardian for: {}", "none".dimmed());
435 } else {
436 log_print!(
437 "\nš”ļø Guardian for: {} account(s)",
438 entrusted.len().to_string().bright_green()
439 );
440 for (i, addr) in entrusted.iter().enumerate() {
441 log_print!(" {}. {}", i + 1, addr.bright_cyan());
442 }
443 if let Ok((total, per_account)) =
446 fetch_pending_transfers_for_guardian(
447 &quantus_client,
448 &entrusted,
449 )
450 .await
451 {
452 if total > 0 {
453 log_print!(
454 "\n {} {} pending transfer(s) you can intercept",
455 "ā ļø".bright_yellow(),
456 total.to_string().bright_yellow().bold()
457 );
458 for (addr, count) in per_account {
459 if count > 0 {
460 log_print!(
461 " from {}: {}",
462 addr.bright_cyan(),
463 count
464 );
465 }
466 }
467 log_print!(" {}", "Use: quantus reversible cancel --tx-id <id> --from <you>".dimmed());
468 }
469 }
470 }
471 }
472 } else {
473 log_verbose!(
474 "Could not connect to node; High Security status skipped."
475 );
476 }
477 }
478 },
479 Ok(None) => {
480 log_error!("{}", format!("ā Wallet '{wallet_name}' not found").red());
481 log_print!(
482 "Use {} to see available wallets",
483 "quantus wallet list".bright_green()
484 );
485 },
486 Err(e) => {
487 log_error!("{}", format!("ā Failed to view wallet: {e}").red());
488 return Err(e);
489 },
490 }
491 } else {
492 log_print!(
493 "{}",
494 "Please specify a wallet name with --name or use --all to show all wallets"
495 .yellow()
496 );
497 log_print!("Examples:");
498 log_print!(" {}", "quantus wallet view --name my-wallet".bright_green());
499 log_print!(" {}", "quantus wallet view --all".bright_green());
500 }
501
502 Ok(())
503 },
504
505 WalletCommands::Export { name, password, format } => {
506 log_print!("š¤ Exporting wallet...");
507
508 if format.to_lowercase() != "mnemonic" {
509 log_error!("Only 'mnemonic' export format is currently supported.");
510 return Err(crate::error::QuantusError::Generic(
511 "Export format not supported".to_string(),
512 ));
513 }
514
515 let wallet_manager = WalletManager::new()?;
516
517 match wallet_manager.export_mnemonic(&name, password.as_deref()) {
518 Ok(mnemonic) => {
519 log_success!("ā
Wallet exported successfully!");
520 log_print!("\nYour secret mnemonic phrase:");
521 log_print!("{}", "--------------------------------------------------".dimmed());
522 log_print!("{}", mnemonic.bright_yellow());
523 log_print!("{}", "--------------------------------------------------".dimmed());
524 log_print!(
525 "\n{}",
526 "ā ļø Keep this phrase safe and secret. Anyone with this phrase can access your funds."
527 .bright_red()
528 );
529 },
530 Err(e) => {
531 log_error!("{}", format!("ā Failed to export wallet: {e}").red());
532 return Err(e);
533 },
534 }
535
536 Ok(())
537 },
538
539 WalletCommands::Import { name, mnemonic, password, derivation_path, no_derivation } => {
540 log_print!("š„ Importing wallet...");
541
542 let wallet_manager = WalletManager::new()?;
543
544 let mnemonic_phrase =
546 if let Some(mnemonic) = mnemonic { mnemonic } else { get_mnemonic_from_user()? };
547
548 let final_password =
550 crate::wallet::password::get_wallet_password(&name, password, None)?;
551
552 let result = if no_derivation {
554 wallet_manager
556 .import_wallet_no_derivation(&name, &mnemonic_phrase, Some(&final_password))
557 .await
558 } else if derivation_path == DEFAULT_DERIVATION_PATH {
559 wallet_manager
560 .import_wallet(&name, &mnemonic_phrase, Some(&final_password))
561 .await
562 } else {
563 wallet_manager
564 .import_wallet_with_derivation_path(
565 &name,
566 &mnemonic_phrase,
567 Some(&final_password),
568 &derivation_path,
569 )
570 .await
571 };
572
573 match result {
574 Ok(wallet_info) => {
575 log_success!("Wallet name: {}", name.bright_green());
576 log_success!("Address: {}", wallet_info.address.bright_cyan());
577 log_success!("Key type: {}", wallet_info.key_type.bright_yellow());
578 log_success!(
579 "Derivation path: {}",
580 wallet_info.derivation_path.bright_magenta()
581 );
582 log_success!(
583 "Imported: {}",
584 wallet_info.created_at.format("%Y-%m-%d %H:%M:%S UTC").to_string().dimmed()
585 );
586 log_success!("ā
Wallet imported successfully!");
587 },
588 Err(e) => {
589 log_error!("{}", format!("ā Failed to import wallet: {e}").red());
590 return Err(e);
591 },
592 }
593
594 Ok(())
595 },
596
597 WalletCommands::FromSeed { name, seed, password } => {
598 log_print!("š± Creating wallet from seed...");
599
600 let wallet_manager = WalletManager::new()?;
601
602 let final_password =
604 crate::wallet::password::get_wallet_password(&name, password, None)?;
605
606 match wallet_manager
607 .create_wallet_from_seed(&name, &seed, Some(&final_password))
608 .await
609 {
610 Ok(wallet_info) => {
611 log_success!("Wallet name: {}", name.bright_green());
612 log_success!("Address: {}", wallet_info.address.bright_cyan());
613 log_success!("Key type: {}", wallet_info.key_type.bright_yellow());
614 log_success!(
615 "Created: {}",
616 wallet_info.created_at.format("%Y-%m-%d %H:%M:%S UTC").to_string().dimmed()
617 );
618 log_success!("ā
Wallet created from seed successfully!");
619 },
620 Err(e) => {
621 log_error!("{}", format!("ā Failed to create wallet from seed: {e}").red());
622 return Err(e);
623 },
624 }
625
626 Ok(())
627 },
628
629 WalletCommands::List => {
630 log_print!("š Listing all wallets...");
631
632 let wallet_manager = WalletManager::new()?;
633
634 match wallet_manager.list_wallets() {
635 Ok(wallets) =>
636 if wallets.is_empty() {
637 log_print!("{}", "No wallets found.".dimmed());
638 log_print!(
639 "Create a new wallet with: {}",
640 "quantus wallet create --name <name>".bright_green()
641 );
642 } else {
643 log_print!("Found {} wallet(s):\n", wallets.len());
644
645 for (i, wallet) in wallets.iter().enumerate() {
646 log_print!(
647 "{}. {}",
648 (i + 1).to_string().bright_yellow(),
649 wallet.name.bright_green()
650 );
651 log_print!(" Address: {}", wallet.address.bright_cyan());
652 log_print!(" Type: {}", wallet.key_type.bright_yellow());
653 log_print!(
654 " Created: {}",
655 wallet
656 .created_at
657 .format("%Y-%m-%d %H:%M:%S UTC")
658 .to_string()
659 .dimmed()
660 );
661 if i < wallets.len() - 1 {
662 log_print!();
663 }
664 }
665
666 log_print!(
667 "\n{}",
668 "š” Use 'quantus wallet view --name <wallet>' to see full details"
669 .dimmed()
670 );
671 },
672 Err(e) => {
673 log_error!("{}", format!("ā Failed to list wallets: {e}").red());
674 return Err(e);
675 },
676 }
677
678 Ok(())
679 },
680
681 WalletCommands::Delete { name, force } => {
682 log_print!("šļø Deleting wallet...");
683
684 let wallet_manager = WalletManager::new()?;
685
686 match wallet_manager.get_wallet(&name, None) {
688 Ok(Some(wallet_info)) => {
689 log_print!("Wallet to delete:");
691 log_print!(" Name: {}", wallet_info.name.bright_green());
692 log_print!(" Address: {}", wallet_info.address.bright_cyan());
693 log_print!(" Type: {}", wallet_info.key_type.bright_yellow());
694 log_print!(
695 " Created: {}",
696 wallet_info.created_at.format("%Y-%m-%d %H:%M:%S UTC").to_string().dimmed()
697 );
698
699 if !force {
701 log_print!("\n{}", "ā ļø This action cannot be undone!".bright_red());
702 log_print!("Type the wallet name to confirm deletion:");
703
704 print!("Confirm wallet name: ");
705 io::stdout().flush().unwrap();
706
707 let mut input = String::new();
708 io::stdin().read_line(&mut input).unwrap();
709 let input = input.trim();
710
711 if input != name {
712 log_print!(
713 "{}",
714 "ā Wallet name doesn't match. Deletion cancelled.".red()
715 );
716 return Ok(());
717 }
718 }
719
720 match wallet_manager.delete_wallet(&name) {
722 Ok(true) => {
723 log_success!("ā
Wallet '{}' deleted successfully!", name);
724 },
725 Ok(false) => {
726 log_error!("{}", format!("ā Wallet '{name}' was not found").red());
727 },
728 Err(e) => {
729 log_error!("{}", format!("ā Failed to delete wallet: {e}").red());
730 return Err(e);
731 },
732 }
733 },
734 Ok(None) => {
735 log_error!("{}", format!("ā Wallet '{name}' not found").red());
736 log_print!(
737 "Use {} to see available wallets",
738 "quantus wallet list".bright_green()
739 );
740 },
741 Err(e) => {
742 log_error!("{}", format!("ā Failed to check wallet: {e}").red());
743 return Err(e);
744 },
745 }
746
747 Ok(())
748 },
749
750 WalletCommands::Nonce { address, wallet, password } => {
751 log_print!("š¢ Querying account nonce...");
752
753 let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
754
755 let target_address = match (address, wallet) {
757 (Some(addr), _) => {
758 SpAccountId32::from_ss58check(&addr)
760 .map_err(|e| QuantusError::Generic(format!("Invalid address: {e:?}")))?;
761 addr
762 },
763 (None, Some(wallet_name)) => {
764 let keypair =
766 crate::wallet::load_keypair_from_wallet(&wallet_name, password, None)?;
767 keypair.to_account_id_ss58check()
768 },
769 (None, None) => {
770 unreachable!("Either --address or --wallet must be provided");
772 },
773 };
774
775 log_print!("Account: {}", target_address.bright_cyan());
776
777 match get_account_nonce(&quantus_client, &target_address).await {
778 Ok(nonce) => {
779 log_success!("Nonce: {}", nonce.to_string().bright_green());
780 },
781 Err(e) => {
782 log_print!("ā Failed to get nonce: {}", e);
783 return Err(e);
784 },
785 }
786
787 Ok(())
788 },
789 }
790}