diff --git a/src/cli/wallet.rs b/src/cli/wallet.rs index ee30a21..ea636a9 100644 --- a/src/cli/wallet.rs +++ b/src/cli/wallet.rs @@ -1,13 +1,14 @@ //! `quantus wallet` subcommand - wallet operations use crate::{ chain::quantus_subxt, + cli::address_format::QuantusSS58, error::QuantusError, log_error, log_print, log_success, log_verbose, wallet::{password::get_mnemonic_from_user, WalletManager, DEFAULT_DERIVATION_PATH}, }; use clap::Subcommand; use colored::Colorize; -use sp_core::crypto::{AccountId32, Ss58Codec}; +use sp_core::crypto::{AccountId32 as SpAccountId32, Ss58Codec}; use std::io::{self, Write}; /// Wallet management commands @@ -134,7 +135,7 @@ pub async fn get_account_nonce( log_verbose!("#️⃣ Querying nonce for account: {}", account_address.bright_green()); // Parse the SS58 address to AccountId32 (sp-core) - let (account_id_sp, _) = AccountId32::from_ss58check_with_version(account_address) + let (account_id_sp, _) = SpAccountId32::from_ss58check_with_version(account_address) .map_err(|e| QuantusError::NetworkError(format!("Invalid SS58 address: {e:?}")))?; log_verbose!("🔍 SP Account ID: {:?}", account_id_sp); @@ -165,6 +166,105 @@ pub async fn get_account_nonce( Ok(account_info.nonce) } +/// Fetch high-security status from chain for an account (SS58). Returns None if disabled or on +/// error. +async fn fetch_high_security_status( + quantus_client: &crate::chain::client::QuantusClient, + account_ss58: &str, +) -> crate::error::Result> { + use quantus_subxt::api::runtime_types::qp_scheduler::BlockNumberOrTimestamp; + + let (account_id_sp, _) = SpAccountId32::from_ss58check_with_version(account_ss58) + .map_err(|e| QuantusError::Generic(format!("Invalid SS58 for HS lookup: {e:?}")))?; + let account_bytes: [u8; 32] = *account_id_sp.as_ref(); + let account_id = subxt::ext::subxt_core::utils::AccountId32::from(account_bytes); + + let storage_addr = quantus_subxt::api::storage() + .reversible_transfers() + .high_security_accounts(account_id); + let latest = quantus_client.get_latest_block().await?; + let value = quantus_client + .client() + .storage() + .at(latest) + .fetch(&storage_addr) + .await + .map_err(|e| QuantusError::NetworkError(format!("Fetch HS storage: {e:?}")))?; + + let Some(data) = value else { + return Ok(None); + }; + + let interceptor_ss58 = data.interceptor.to_quantus_ss58(); + let delay_str = match data.delay { + BlockNumberOrTimestamp::BlockNumber(blocks) => format!("{} blocks", blocks), + BlockNumberOrTimestamp::Timestamp(ms) => format!("{} seconds", ms / 1000), + }; + Ok(Some((interceptor_ss58, delay_str))) +} + +/// Fetch list of accounts for which this account is guardian (interceptor_index). +/// Returns an empty vec when the storage entry is absent (`None`), and an error on failure. +async fn fetch_guardian_for_list( + quantus_client: &crate::chain::client::QuantusClient, + account_ss58: &str, +) -> crate::error::Result> { + let account_id_sp = SpAccountId32::from_ss58check(account_ss58) + .map_err(|e| QuantusError::Generic(format!("Invalid SS58 for interceptor_index: {e:?}")))?; + let account_bytes: [u8; 32] = *account_id_sp.as_ref(); + let account_id = subxt::ext::subxt_core::utils::AccountId32::from(account_bytes); + + let storage_addr = quantus_subxt::api::storage() + .reversible_transfers() + .interceptor_index(account_id); + let latest = quantus_client.get_latest_block().await?; + let value = quantus_client + .client() + .storage() + .at(latest) + .fetch(&storage_addr) + .await + .map_err(|e| QuantusError::NetworkError(format!("Fetch interceptor_index: {e:?}")))?; + + let list = value + .map(|bounded| bounded.0.iter().map(|a| a.to_quantus_ss58()).collect()) + .unwrap_or_default(); + Ok(list) +} + +/// For each entrusted account (SS58), count pending reversible transfers by sender. Returns (total, +/// per-account list). +async fn fetch_pending_transfers_for_guardian( + quantus_client: &crate::chain::client::QuantusClient, + entrusted_ss58: &[String], +) -> crate::error::Result<(u32, Vec<(String, u32)>)> { + let latest = quantus_client.get_latest_block().await?; + let storage = quantus_client.client().storage().at(latest); + let mut total = 0u32; + let mut per_account = Vec::with_capacity(entrusted_ss58.len()); + + for ss58 in entrusted_ss58 { + let account_id_sp = SpAccountId32::from_ss58check(ss58).map_err(|e| { + QuantusError::Generic(format!("Invalid SS58 for pending lookup: {e:?}")) + })?; + let account_bytes: [u8; 32] = *account_id_sp.as_ref(); + let account_id = subxt::ext::subxt_core::utils::AccountId32::from(account_bytes); + + let addr = quantus_subxt::api::storage() + .reversible_transfers() + .pending_transfers_by_sender(account_id); + let value = storage.fetch(&addr).await.map_err(|e| { + QuantusError::NetworkError(format!("Fetch pending_transfers_by_sender: {e:?}")) + })?; + + let count = value.map(|bounded| bounded.0.len() as u32).unwrap_or(0); + total += count; + per_account.push((ss58.clone(), count)); + } + + Ok((total, per_account)) +} + /// Handle wallet commands pub async fn handle_wallet_command( command: WalletCommands, @@ -288,6 +388,93 @@ pub async fn handle_wallet_command( .dimmed() ); } + + // High-Security status and Guardian-for list from chain (optional; don't + // fail view if node unavailable) + if !wallet_info.address.contains("[") { + if let Ok(quantus_client) = + crate::chain::client::QuantusClient::new(node_url).await + { + match fetch_high_security_status( + &quantus_client, + &wallet_info.address, + ) + .await + { + Ok(Some((interceptor_ss58, delay_str))) => { + log_print!( + "\n🛡️ High Security: {}", + "ENABLED".bright_green().bold() + ); + log_print!( + " Guardian/Interceptor: {}", + interceptor_ss58.bright_cyan() + ); + log_print!(" Delay: {}", delay_str.bright_yellow()); + }, + Ok(None) => { + log_print!("\n🛡️ High Security: {}", "DISABLED".dimmed()); + }, + Err(e) => { + log_verbose!("High Security status skipped: {}", e); + log_print!( + "\n{}", + "💡 Run quantus high-security status --account
to check on-chain" + .dimmed() + ); + }, + } + + // Guardian for: accounts that have this wallet as their interceptor + if let Ok(entrusted) = + fetch_guardian_for_list(&quantus_client, &wallet_info.address) + .await + { + if entrusted.is_empty() { + log_print!("🛡️ Guardian for: {}", "none".dimmed()); + } else { + log_print!( + "\n🛡️ Guardian for: {} account(s)", + entrusted.len().to_string().bright_green() + ); + for (i, addr) in entrusted.iter().enumerate() { + log_print!(" {}. {}", i + 1, addr.bright_cyan()); + } + // Pending reversible transfers that this guardian can + // intercept + if let Ok((total, per_account)) = + fetch_pending_transfers_for_guardian( + &quantus_client, + &entrusted, + ) + .await + { + if total > 0 { + log_print!( + "\n {} {} pending transfer(s) you can intercept", + "⚠️".bright_yellow(), + total.to_string().bright_yellow().bold() + ); + for (addr, count) in per_account { + if count > 0 { + log_print!( + " from {}: {}", + addr.bright_cyan(), + count + ); + } + } + log_print!(" {}", "Use: quantus reversible cancel --tx-id --from ".dimmed()); + } + } + } + } + } else { + log_verbose!( + "Could not connect to node; High Security status skipped." + ); + } + } }, Ok(None) => { log_error!("{}", format!("❌ Wallet '{wallet_name}' not found").red()); @@ -569,7 +756,7 @@ pub async fn handle_wallet_command( let target_address = match (address, wallet) { (Some(addr), _) => { // Validate the provided address - AccountId32::from_ss58check(&addr) + SpAccountId32::from_ss58check(&addr) .map_err(|e| QuantusError::Generic(format!("Invalid address: {e:?}")))?; addr }, diff --git a/src/wallet/keystore.rs b/src/wallet/keystore.rs index 752a8f9..37c25d2 100644 --- a/src/wallet/keystore.rs +++ b/src/wallet/keystore.rs @@ -334,10 +334,12 @@ mod tests { assert_eq!(original_pair.secret.as_ref(), converted_pair.secret.as_ref()); } + /// Wallet address must match chain: same AccountId (Poseidon hash of Dilithium public) + /// and same SS58 prefix (189, "qz") as in chain runtime and genesis. #[test] fn test_quantum_keypair_address_generation() { sp_core::crypto::set_default_ss58_version(sp_core::crypto::Ss58AddressFormat::custom(189)); - // Test with known test keypairs + // Same test keypairs as chain genesis (crystal_alice, dilithium_bob, crystal_charlie) let test_pairs = vec![ ("crystal_alice", crystal_alice()), ("crystal_bob", dilithium_bob()), @@ -351,8 +353,11 @@ mod tests { let account_id = quantum_keypair.to_account_id_32(); let ss58_address = quantum_keypair.to_account_id_ss58check(); - // Verify address format - assert!(ss58_address.starts_with("qz"), "SS58 address for {name} should start with 5"); + // Verify address format (Quantus SS58 prefix 189 = "qz") + assert!( + ss58_address.starts_with("qz"), + "SS58 address for {name} should start with qz (Quantus prefix 189)" + ); assert!( ss58_address.len() >= 47, "SS58 address for {name} should be at least 47 characters" @@ -366,14 +371,15 @@ mod tests { "Address methods should be consistent for {name}" ); - // Verify it matches the direct DilithiumPair method - let expected_address = resonance_pair + // Must match chain: chain uses same qp_dilithium_crypto IdentifyAccount (into_account) + // and SS58 189 in genesis_config_presets and runtime config + let chain_expected_address = resonance_pair .public() .into_account() .to_ss58check_with_version(quantus_ss58_format()); assert_eq!( - ss58_address, expected_address, - "Address should match DilithiumPair method for {name}" + ss58_address, chain_expected_address, + "Wallet address for {name} must match chain dev account (same derivation and SS58 189)" ); } }