|
1 | 1 | //! `quantus wallet` subcommand - wallet operations |
2 | 2 | use crate::{ |
3 | 3 | chain::quantus_subxt, |
| 4 | + cli::address_format::QuantusSS58, |
4 | 5 | error::QuantusError, |
5 | 6 | log_error, log_print, log_success, log_verbose, |
6 | 7 | wallet::{password::get_mnemonic_from_user, WalletManager, DEFAULT_DERIVATION_PATH}, |
7 | 8 | }; |
8 | 9 | use clap::Subcommand; |
9 | 10 | use colored::Colorize; |
10 | | -use sp_core::crypto::{AccountId32, Ss58Codec}; |
| 11 | +use sp_core::crypto::{AccountId32 as SpAccountId32, Ss58Codec}; |
11 | 12 | use std::io::{self, Write}; |
12 | 13 |
|
13 | 14 | /// Wallet management commands |
@@ -134,7 +135,7 @@ pub async fn get_account_nonce( |
134 | 135 | log_verbose!("#️⃣ Querying nonce for account: {}", account_address.bright_green()); |
135 | 136 |
|
136 | 137 | // Parse the SS58 address to AccountId32 (sp-core) |
137 | | - let (account_id_sp, _) = AccountId32::from_ss58check_with_version(account_address) |
| 138 | + let (account_id_sp, _) = SpAccountId32::from_ss58check_with_version(account_address) |
138 | 139 | .map_err(|e| QuantusError::NetworkError(format!("Invalid SS58 address: {e:?}")))?; |
139 | 140 |
|
140 | 141 | log_verbose!("🔍 SP Account ID: {:?}", account_id_sp); |
@@ -165,6 +166,105 @@ pub async fn get_account_nonce( |
165 | 166 | Ok(account_info.nonce) |
166 | 167 | } |
167 | 168 |
|
| 169 | +/// Fetch high-security status from chain for an account (SS58). Returns None if disabled or on |
| 170 | +/// error. |
| 171 | +async 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 | + |
| 206 | +/// Fetch list of accounts for which this account is guardian (interceptor_index). |
| 207 | +/// Returns an empty vec when the storage entry is absent (`None`), and an error on failure. |
| 208 | +async 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 | + |
| 235 | +/// For each entrusted account (SS58), count pending reversible transfers by sender. Returns (total, |
| 236 | +/// per-account list). |
| 237 | +async 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 | + |
168 | 268 | /// Handle wallet commands |
169 | 269 | pub async fn handle_wallet_command( |
170 | 270 | command: WalletCommands, |
@@ -288,6 +388,93 @@ pub async fn handle_wallet_command( |
288 | 388 | .dimmed() |
289 | 389 | ); |
290 | 390 | } |
| 391 | + |
| 392 | + // High-Security status and Guardian-for list from chain (optional; don't |
| 393 | + // fail view if node unavailable) |
| 394 | + 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 | + // Guardian for: accounts that have this wallet as their interceptor |
| 429 | + 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 | + // Pending reversible transfers that this guardian can |
| 444 | + // intercept |
| 445 | + 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 | + } |
291 | 478 | }, |
292 | 479 | Ok(None) => { |
293 | 480 | log_error!("{}", format!("❌ Wallet '{wallet_name}' not found").red()); |
@@ -569,7 +756,7 @@ pub async fn handle_wallet_command( |
569 | 756 | let target_address = match (address, wallet) { |
570 | 757 | (Some(addr), _) => { |
571 | 758 | // Validate the provided address |
572 | | - AccountId32::from_ss58check(&addr) |
| 759 | + SpAccountId32::from_ss58check(&addr) |
573 | 760 | .map_err(|e| QuantusError::Generic(format!("Invalid address: {e:?}")))?; |
574 | 761 | addr |
575 | 762 | }, |
|
0 commit comments