Skip to content

Commit 7288d5a

Browse files
authored
feat: HS details from Wallet perspective (#66)
* feat: HS details from Wallet perspective * fix: Comment fix
1 parent 2b11240 commit 7288d5a

2 files changed

Lines changed: 203 additions & 10 deletions

File tree

src/cli/wallet.rs

Lines changed: 190 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
//! `quantus wallet` subcommand - wallet operations
22
use crate::{
33
chain::quantus_subxt,
4+
cli::address_format::QuantusSS58,
45
error::QuantusError,
56
log_error, log_print, log_success, log_verbose,
67
wallet::{password::get_mnemonic_from_user, WalletManager, DEFAULT_DERIVATION_PATH},
78
};
89
use clap::Subcommand;
910
use colored::Colorize;
10-
use sp_core::crypto::{AccountId32, Ss58Codec};
11+
use sp_core::crypto::{AccountId32 as SpAccountId32, Ss58Codec};
1112
use std::io::{self, Write};
1213

1314
/// Wallet management commands
@@ -134,7 +135,7 @@ pub async fn get_account_nonce(
134135
log_verbose!("#️⃣ Querying nonce for account: {}", account_address.bright_green());
135136

136137
// 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)
138139
.map_err(|e| QuantusError::NetworkError(format!("Invalid SS58 address: {e:?}")))?;
139140

140141
log_verbose!("🔍 SP Account ID: {:?}", account_id_sp);
@@ -165,6 +166,105 @@ pub async fn get_account_nonce(
165166
Ok(account_info.nonce)
166167
}
167168

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+
168268
/// Handle wallet commands
169269
pub async fn handle_wallet_command(
170270
command: WalletCommands,
@@ -288,6 +388,93 @@ pub async fn handle_wallet_command(
288388
.dimmed()
289389
);
290390
}
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+
}
291478
},
292479
Ok(None) => {
293480
log_error!("{}", format!("❌ Wallet '{wallet_name}' not found").red());
@@ -569,7 +756,7 @@ pub async fn handle_wallet_command(
569756
let target_address = match (address, wallet) {
570757
(Some(addr), _) => {
571758
// Validate the provided address
572-
AccountId32::from_ss58check(&addr)
759+
SpAccountId32::from_ss58check(&addr)
573760
.map_err(|e| QuantusError::Generic(format!("Invalid address: {e:?}")))?;
574761
addr
575762
},

src/wallet/keystore.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -334,10 +334,12 @@ mod tests {
334334
assert_eq!(original_pair.secret.as_ref(), converted_pair.secret.as_ref());
335335
}
336336

337+
/// Wallet address must match chain: same AccountId (Poseidon hash of Dilithium public)
338+
/// and same SS58 prefix (189, "qz") as in chain runtime and genesis.
337339
#[test]
338340
fn test_quantum_keypair_address_generation() {
339341
sp_core::crypto::set_default_ss58_version(sp_core::crypto::Ss58AddressFormat::custom(189));
340-
// Test with known test keypairs
342+
// Same test keypairs as chain genesis (crystal_alice, dilithium_bob, crystal_charlie)
341343
let test_pairs = vec![
342344
("crystal_alice", crystal_alice()),
343345
("crystal_bob", dilithium_bob()),
@@ -351,8 +353,11 @@ mod tests {
351353
let account_id = quantum_keypair.to_account_id_32();
352354
let ss58_address = quantum_keypair.to_account_id_ss58check();
353355

354-
// Verify address format
355-
assert!(ss58_address.starts_with("qz"), "SS58 address for {name} should start with 5");
356+
// Verify address format (Quantus SS58 prefix 189 = "qz")
357+
assert!(
358+
ss58_address.starts_with("qz"),
359+
"SS58 address for {name} should start with qz (Quantus prefix 189)"
360+
);
356361
assert!(
357362
ss58_address.len() >= 47,
358363
"SS58 address for {name} should be at least 47 characters"
@@ -366,14 +371,15 @@ mod tests {
366371
"Address methods should be consistent for {name}"
367372
);
368373

369-
// Verify it matches the direct DilithiumPair method
370-
let expected_address = resonance_pair
374+
// Must match chain: chain uses same qp_dilithium_crypto IdentifyAccount (into_account)
375+
// and SS58 189 in genesis_config_presets and runtime config
376+
let chain_expected_address = resonance_pair
371377
.public()
372378
.into_account()
373379
.to_ss58check_with_version(quantus_ss58_format());
374380
assert_eq!(
375-
ss58_address, expected_address,
376-
"Address should match DilithiumPair method for {name}"
381+
ss58_address, chain_expected_address,
382+
"Wallet address for {name} must match chain dev account (same derivation and SS58 189)"
377383
);
378384
}
379385
}

0 commit comments

Comments
 (0)