use crate::{
chain::quantus_subxt,
cli::address_format::QuantusSS58,
error::QuantusError,
log_error, log_print, log_success, log_verbose,
wallet::{
default_derivation_path,
password::{get_mnemonic_from_user, get_new_wallet_password},
DilithiumScheme, WalletManager,
},
};
use clap::Subcommand;
use colored::Colorize;
use sp_core::crypto::{AccountId32 as SpAccountId32, Ss58Codec};
use std::io::{self, Write};
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt;
#[derive(Subcommand, Debug)]
pub enum WalletCommands {
Create {
#[arg(short, long)]
name: String,
#[arg(short, long, hide = true)]
password: Option<String>,
#[arg(long)]
password_file: Option<String>,
#[arg(long)]
allow_empty_password: bool,
#[arg(short = 'd', long)]
derivation_path: Option<String>,
#[arg(long)]
no_derivation: bool,
#[arg(long, value_enum, default_value_t = DilithiumScheme::MlDsa65)]
scheme: DilithiumScheme,
},
View {
#[arg(short, long)]
name: Option<String>,
#[arg(short, long)]
all: bool,
},
Export {
#[arg(short, long)]
name: String,
#[arg(short, long, hide = true)]
password: Option<String>,
#[arg(short, long, default_value = "mnemonic")]
format: String,
#[arg(short, long)]
output: Option<std::path::PathBuf>,
},
Import {
#[arg(short, long)]
name: String,
#[arg(short, long, hide = true)]
password: Option<String>,
#[arg(long)]
password_file: Option<String>,
#[arg(long)]
allow_empty_password: bool,
#[arg(short = 'd', long)]
derivation_path: Option<String>,
#[arg(long)]
no_derivation: bool,
#[arg(long, value_enum, default_value_t = DilithiumScheme::MlDsa65)]
scheme: DilithiumScheme,
},
FromSeed {
#[arg(short, long)]
name: String,
#[arg(short, long, hide = true)]
password: Option<String>,
#[arg(long)]
password_file: Option<String>,
#[arg(long)]
allow_empty_password: bool,
#[arg(long, value_enum, default_value_t = DilithiumScheme::MlDsa65)]
scheme: DilithiumScheme,
},
List,
Delete {
#[arg(short, long)]
name: String,
#[arg(short, long)]
force: bool,
},
Nonce {
#[arg(short, long)]
address: Option<String>,
#[arg(short, long, required_unless_present("address"))]
wallet: Option<String>,
#[arg(short, long, hide = true)]
password: Option<String>,
},
}
pub async fn get_account_nonce(
quantus_client: &crate::chain::client::QuantusClient,
account_address: &str,
) -> crate::error::Result<u32> {
log_verbose!("#️⃣ Querying nonce for account: {}", account_address.bright_green());
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);
let account_bytes: [u8; 32] = *account_id_sp.as_ref();
let account_id = subxt::ext::subxt_core::utils::AccountId32::from(account_bytes);
log_verbose!("🔍 SubXT Account ID: {:?}", account_id);
use quantus_subxt::api;
let storage_addr = api::storage().system().account(account_id);
let latest_block_hash = quantus_client.get_latest_block().await?;
let storage_at = quantus_client.client().storage().at(latest_block_hash);
let account_info = storage_at
.fetch(&storage_addr)
.await
.map_err(|e| QuantusError::NetworkError(format!("Failed to fetch account info: {e:?}")))?;
let (nonce, exists) = crate::chain::client::QuantusClient::interpret_account_nonce(
account_info.map(|info| info.nonce),
);
if exists {
log_verbose!("✅ Account info retrieved with storage query!");
} else {
log_print!(
"⚠️ Account has no on-chain System::Account entry; reporting nonce 0 (new/unused account)"
);
}
log_verbose!("🔢 Nonce: {} (exists={})", nonce, exists);
Ok(nonce)
}
async fn fetch_high_security_status(
quantus_client: &crate::chain::client::QuantusClient,
account_ss58: &str,
) -> crate::error::Result<Option<(String, String)>> {
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 guardian_ss58 = data.guardian.to_quantus_ss58();
let delay_str = match data.delay {
BlockNumberOrTimestamp::BlockNumber(blocks) => format!("{} blocks", blocks),
BlockNumberOrTimestamp::Timestamp(ms) => format!("{} seconds", ms / 1000),
};
Ok(Some((guardian_ss58, delay_str)))
}
async fn fetch_guardian_for_list(
quantus_client: &crate::chain::client::QuantusClient,
account_ss58: &str,
) -> crate::error::Result<Vec<String>> {
let account_id_sp = SpAccountId32::from_ss58check(account_ss58)
.map_err(|e| QuantusError::Generic(format!("Invalid SS58 for guardian_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().guardian_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 guardian_index: {e:?}")))?;
let list: Vec<String> = value
.map(|bounded| {
bounded
.0
.iter()
.map(|a: &subxt::ext::subxt_core::utils::AccountId32| a.to_quantus_ss58())
.collect()
})
.unwrap_or_default();
Ok(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 pending_iter =
quantus_subxt::api::storage().reversible_transfers().pending_transfers_iter();
let entrusted_ids: Vec<[u8; 32]> = entrusted_ss58
.iter()
.map(|s| {
let id = SpAccountId32::from_ss58check(s).map_err(|e| {
QuantusError::Generic(format!("Invalid SS58 for pending lookup: {e:?}"))
})?;
Ok::<_, crate::error::QuantusError>(*id.as_ref())
})
.collect::<Result<Vec<_>, _>>()?;
let mut counts: std::collections::HashMap<[u8; 32], u32> =
entrusted_ids.iter().map(|id| (*id, 0u32)).collect();
let mut iter = storage
.iter(pending_iter)
.await
.map_err(|e| QuantusError::NetworkError(format!("Pending transfers iter: {e:?}")))?;
while let Some(result) = iter.next().await {
if let Ok(entry) = result {
let from_bytes: [u8; 32] = *entry.value.from.as_ref();
if let Some(c) = counts.get_mut(&from_bytes) {
*c += 1;
}
}
}
let mut total = 0u32;
let mut per_account = Vec::with_capacity(entrusted_ss58.len());
for (ss58, id) in entrusted_ss58.iter().zip(entrusted_ids.iter()) {
let count = *counts.get(id).unwrap_or(&0);
total += count;
per_account.push((ss58.clone(), count));
}
Ok((total, per_account))
}
fn write_mnemonic_to_protected_file(
path: &std::path::Path,
mnemonic: &str,
) -> crate::error::Result<()> {
let mut options = std::fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
options.mode(0o600);
let mut file = options.open(path).map_err(|e| {
QuantusError::Generic(format!("Failed to create mnemonic export file: {e}"))
})?;
file.write_all(mnemonic.as_bytes())
.map_err(|e| QuantusError::Generic(format!("Failed to write mnemonic export file: {e}")))?;
file.write_all(b"\n")
.map_err(|e| QuantusError::Generic(format!("Failed to write mnemonic export file: {e}")))?;
file.sync_all()
.map_err(|e| QuantusError::Generic(format!("Failed to sync mnemonic export file: {e}")))?;
Ok(())
}
fn checkphrase_line(address: &str) -> Option<String> {
(!address.starts_with('['))
.then(|| crate::wallet::checkphrase::checkphrase(address).bright_blue().to_string())
}
pub async fn handle_wallet_command(
command: WalletCommands,
node_url: &str,
) -> crate::error::Result<()> {
match command {
WalletCommands::Create {
name,
password,
password_file,
allow_empty_password,
derivation_path,
no_derivation,
scheme,
} => {
log_print!("🔐 Creating new quantum wallet...");
let final_password =
get_new_wallet_password(&name, password, password_file, allow_empty_password)?;
let wallet_manager = WalletManager::new()?;
let result = if no_derivation {
wallet_manager
.create_wallet_no_derivation_with_scheme(&name, Some(&final_password), scheme)
.await
} else {
let path =
derivation_path.as_deref().unwrap_or_else(|| default_derivation_path(scheme));
wallet_manager
.create_wallet_with_scheme(&name, Some(&final_password), path, scheme)
.await
};
match result {
Ok(wallet_info) => {
log_success!("Wallet name: {}", name.bright_green());
log_success!("Address: {}", wallet_info.address.bright_cyan());
if let Some(checkphrase) = checkphrase_line(&wallet_info.address) {
log_success!("Checkphrase: {}", checkphrase);
}
log_success!("Key type: {}", wallet_info.key_type.bright_yellow());
log_success!(
"Derivation path: {}",
wallet_info.derivation_path.bright_magenta()
);
log_success!(
"Created: {}",
wallet_info.created_at.format("%Y-%m-%d %H:%M:%S UTC").to_string().dimmed()
);
log_success!("✅ Wallet created successfully!");
},
Err(e) => {
log_error!("{}", format!("❌ Failed to create wallet: {e}").red());
return Err(e);
},
}
Ok(())
},
WalletCommands::View { name, all } => {
log_print!("👁️ Viewing wallet information...");
let wallet_manager = WalletManager::new()?;
if all {
match wallet_manager.list_wallets() {
Ok(wallets) =>
if wallets.is_empty() {
log_print!("{}", "No wallets found.".dimmed());
} else {
log_print!("All wallets ({}):\n", wallets.len());
for (i, wallet) in wallets.iter().enumerate() {
log_print!(
"{}. {}",
(i + 1).to_string().bright_yellow(),
wallet.name.bright_green()
);
log_print!(" Address: {}", wallet.address.bright_cyan());
if let Some(checkphrase) = checkphrase_line(&wallet.address) {
log_print!(" Checkphrase: {}", checkphrase);
}
log_print!(" Type: {}", wallet.key_type.bright_yellow());
log_print!(
" Derivation Path: {}",
wallet.derivation_path.bright_magenta()
);
log_print!(
" Created: {}",
wallet
.created_at
.format("%Y-%m-%d %H:%M:%S UTC")
.to_string()
.dimmed()
);
if i < wallets.len() - 1 {
log_print!();
}
}
},
Err(e) => {
log_error!("{}", format!("❌ Failed to view wallets: {e}").red());
return Err(e);
},
}
} else if let Some(wallet_name) = name {
match wallet_manager.get_wallet(&wallet_name, None) {
Ok(Some(wallet_info)) => {
log_print!("Wallet Details:\n");
log_print!("Name: {}", wallet_info.name.bright_green());
log_print!("Address: {}", wallet_info.address.bright_cyan());
if let Some(checkphrase) = checkphrase_line(&wallet_info.address) {
log_print!("Checkphrase: {}", checkphrase);
}
log_print!("Key Type: {}", wallet_info.key_type.bright_yellow());
log_print!(
"Derivation Path: {}",
wallet_info.derivation_path.bright_magenta()
);
log_print!(
"Created: {}",
wallet_info
.created_at
.format("%Y-%m-%d %H:%M:%S UTC")
.to_string()
.dimmed()
);
if wallet_info.address.contains("[") {
log_print!(
"\n{}",
"💡 To see the full address, use the export command with password"
.dimmed()
);
}
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 <address> to check on-chain"
.dimmed()
);
},
}
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());
}
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 <id> --from <you>".dimmed());
}
}
}
}
} else {
log_verbose!(
"Could not connect to node; High Security status skipped."
);
}
}
},
Ok(None) => {
log_error!("{}", format!("❌ Wallet '{wallet_name}' not found").red());
log_print!(
"Use {} to see available wallets",
"quantus wallet list".bright_green()
);
},
Err(e) => {
log_error!("{}", format!("❌ Failed to view wallet: {e}").red());
return Err(e);
},
}
} else {
log_print!(
"{}",
"Please specify a wallet name with --name or use --all to show all wallets"
.yellow()
);
log_print!("Examples:");
log_print!(" {}", "quantus wallet view --name my-wallet".bright_green());
log_print!(" {}", "quantus wallet view --all".bright_green());
}
Ok(())
},
WalletCommands::Export { name, password, format, output } => {
log_print!("📤 Exporting wallet...");
if format.to_lowercase() != "mnemonic" {
log_error!("Only 'mnemonic' export format is currently supported.");
return Err(crate::error::QuantusError::Generic(
"Export format not supported".to_string(),
));
}
let Some(output_path) = output else {
log_error!(
"Refusing to print the mnemonic to stdout. Use --output <file> to create a protected export file."
);
return Err(crate::error::QuantusError::Generic(
"Mnemonic export requires --output".to_string(),
));
};
let wallet_manager = WalletManager::new()?;
match wallet_manager.export_mnemonic(&name, password.as_deref()) {
Ok(mnemonic) => {
write_mnemonic_to_protected_file(&output_path, &mnemonic)?;
log_success!("✅ Wallet exported successfully!");
log_print!(
"Mnemonic written to: {}",
output_path.display().to_string().bright_cyan()
);
log_print!(
"{}",
"⚠️ Keep this file safe and secret. Anyone with this phrase can access your funds."
.bright_red()
);
},
Err(e) => {
log_error!("{}", format!("❌ Failed to export wallet: {e}").red());
return Err(e);
},
}
Ok(())
},
WalletCommands::Import {
name,
password,
password_file,
allow_empty_password,
derivation_path,
no_derivation,
scheme,
} => {
log_print!("📥 Importing wallet...");
let wallet_manager = WalletManager::new()?;
let final_password = crate::wallet::password::get_new_wallet_password(
&name,
password,
password_file,
allow_empty_password,
)?;
let mut mnemonic_phrase = get_mnemonic_from_user()?;
let result = if no_derivation {
wallet_manager
.import_wallet_no_derivation_with_scheme(
&name,
&mnemonic_phrase,
Some(&final_password),
scheme,
)
.await
} else {
let path =
derivation_path.as_deref().unwrap_or_else(|| default_derivation_path(scheme));
wallet_manager
.import_wallet_with_scheme(
&name,
&mnemonic_phrase,
Some(&final_password),
path,
scheme,
)
.await
};
crate::wallet::keystore::zeroize_string(&mut mnemonic_phrase);
match result {
Ok(wallet_info) => {
log_success!("Wallet name: {}", name.bright_green());
log_success!("Address: {}", wallet_info.address.bright_cyan());
if let Some(checkphrase) = checkphrase_line(&wallet_info.address) {
log_success!("Checkphrase: {}", checkphrase);
}
log_success!("Key type: {}", wallet_info.key_type.bright_yellow());
log_success!(
"Derivation path: {}",
wallet_info.derivation_path.bright_magenta()
);
log_success!(
"Imported: {}",
wallet_info.created_at.format("%Y-%m-%d %H:%M:%S UTC").to_string().dimmed()
);
log_success!("✅ Wallet imported successfully!");
},
Err(e) => {
log_error!("{}", format!("❌ Failed to import wallet: {e}").red());
return Err(e);
},
}
Ok(())
},
WalletCommands::FromSeed {
name,
password,
password_file,
allow_empty_password,
scheme,
} => {
log_print!("🌱 Creating wallet from seed...");
let wallet_manager = WalletManager::new()?;
let final_password = crate::wallet::password::get_new_wallet_password(
&name,
password,
password_file,
allow_empty_password,
)?;
log_print!("Enter 32-byte seed in hex format (64 hex characters):");
let mut seed_raw = rpassword::read_password()
.map_err(|e| QuantusError::Generic(format!("Failed to read seed: {e}")))?;
let mut seed = seed_raw.trim().to_string();
crate::wallet::keystore::zeroize_string(&mut seed_raw);
let result = wallet_manager
.create_wallet_from_seed_with_scheme(&name, &seed, Some(&final_password), scheme)
.await;
crate::wallet::keystore::zeroize_string(&mut seed);
match result {
Ok(wallet_info) => {
log_success!("Wallet name: {}", name.bright_green());
log_success!("Address: {}", wallet_info.address.bright_cyan());
if let Some(checkphrase) = checkphrase_line(&wallet_info.address) {
log_success!("Checkphrase: {}", checkphrase);
}
log_success!("Key type: {}", wallet_info.key_type.bright_yellow());
log_success!(
"Created: {}",
wallet_info.created_at.format("%Y-%m-%d %H:%M:%S UTC").to_string().dimmed()
);
log_success!("✅ Wallet created from seed successfully!");
},
Err(e) => {
log_error!("{}", format!("❌ Failed to create wallet from seed: {e}").red());
return Err(e);
},
}
Ok(())
},
WalletCommands::List => {
log_print!("📋 Listing all wallets...");
let wallet_manager = WalletManager::new()?;
match wallet_manager.list_wallets() {
Ok(wallets) =>
if wallets.is_empty() {
log_print!("{}", "No wallets found.".dimmed());
log_print!(
"Create a new wallet with: {}",
"quantus wallet create --name <name>".bright_green()
);
} else {
log_print!("Found {} wallet(s):\n", wallets.len());
for (i, wallet) in wallets.iter().enumerate() {
log_print!(
"{}. {}",
(i + 1).to_string().bright_yellow(),
wallet.name.bright_green()
);
log_print!(" Address: {}", wallet.address.bright_cyan());
if let Some(checkphrase) = checkphrase_line(&wallet.address) {
log_print!(" Checkphrase: {}", checkphrase);
}
log_print!(" Type: {}", wallet.key_type.bright_yellow());
log_print!(
" Created: {}",
wallet
.created_at
.format("%Y-%m-%d %H:%M:%S UTC")
.to_string()
.dimmed()
);
if i < wallets.len() - 1 {
log_print!();
}
}
log_print!(
"\n{}",
"💡 Use 'quantus wallet view --name <wallet>' to see full details"
.dimmed()
);
},
Err(e) => {
log_error!("{}", format!("❌ Failed to list wallets: {e}").red());
return Err(e);
},
}
Ok(())
},
WalletCommands::Delete { name, force } => {
log_print!("🗑️ Deleting wallet...");
let wallet_manager = WalletManager::new()?;
let wallet_info = match wallet_manager.get_wallet(&name, None) {
Ok(Some(wallet_info)) => Some(wallet_info),
Ok(None) => {
log_error!("{}", format!("❌ Wallet '{name}' not found").red());
log_print!(
"Use {} to see available wallets",
"quantus wallet list".bright_green()
);
return Ok(());
},
Err(e) => {
log_print!(
"{}",
format!("⚠️ Wallet file for '{name}' exists but cannot be parsed: {e}")
.yellow()
);
log_print!(" Deleting will remove the corrupt wallet file.");
None
},
};
if let Some(wallet_info) = wallet_info {
log_print!("Wallet to delete:");
log_print!(" Name: {}", wallet_info.name.bright_green());
log_print!(" Address: {}", wallet_info.address.bright_cyan());
if let Some(checkphrase) = checkphrase_line(&wallet_info.address) {
log_print!(" Checkphrase: {}", checkphrase);
}
log_print!(" Type: {}", wallet_info.key_type.bright_yellow());
log_print!(
" Created: {}",
wallet_info.created_at.format("%Y-%m-%d %H:%M:%S UTC").to_string().dimmed()
);
}
if !force {
log_print!("\n{}", "⚠️ This action cannot be undone!".bright_red());
log_print!("Type the wallet name to confirm deletion:");
print!("Confirm wallet name: ");
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
let input = input.trim();
if input != name {
log_print!("{}", "❌ Wallet name doesn't match. Deletion cancelled.".red());
return Ok(());
}
}
match wallet_manager.delete_wallet(&name) {
Ok(true) => {
log_success!("✅ Wallet '{}' deleted successfully!", name);
},
Ok(false) => {
log_error!("{}", format!("❌ Wallet '{name}' was not found").red());
},
Err(e) => {
log_error!("{}", format!("❌ Failed to delete wallet: {e}").red());
return Err(e);
},
}
Ok(())
},
WalletCommands::Nonce { address, wallet, password } => {
log_print!("🔢 Querying account nonce...");
let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
let target_address = match (address, wallet) {
(Some(addr), _) => {
SpAccountId32::from_ss58check(&addr)
.map_err(|e| QuantusError::Generic(format!("Invalid address: {e:?}")))?;
addr
},
(None, Some(wallet_name)) => {
let keypair =
crate::wallet::load_keypair_from_wallet(&wallet_name, password, None)?;
keypair.try_to_account_id_ss58check()?
},
(None, None) => {
unreachable!("Either --address or --wallet must be provided");
},
};
log_print!("Account: {}", target_address.bright_cyan());
match get_account_nonce(&quantus_client, &target_address).await {
Ok(nonce) => {
log_success!("Nonce: {}", nonce.to_string().bright_green());
},
Err(e) => {
log_print!("❌ Failed to get nonce: {}", e);
return Err(e);
},
}
Ok(())
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
use serial_test::serial;
use tempfile::TempDir;
#[derive(Parser, Debug)]
#[command(name = "quantus")]
struct TestCli {
#[command(subcommand)]
command: crate::cli::Commands,
}
#[tokio::test]
#[serial]
async fn wallet_export_without_output_refuses_stdout_mnemonic() {
let home = TempDir::new().expect("temp HOME");
std::env::set_var("HOME", home.path());
std::env::set_var("QUANTUS_NO_UPDATE_CHECK", "1");
let manager = WalletManager::new().expect("wallet manager");
manager.create_wallet("export-leak", Some("")).await.expect("create wallet");
let result = handle_wallet_command(
WalletCommands::Export {
name: "export-leak".to_string(),
password: None,
format: "mnemonic".to_string(),
output: None,
},
"ws://127.0.0.1:9944",
)
.await;
assert!(result.is_err(), "export without --output must refuse stdout mnemonic emission");
assert!(
result.unwrap_err().to_string().contains("requires --output"),
"error should mention --output"
);
}
#[tokio::test]
#[serial]
async fn wallet_export_writes_mnemonic_to_protected_file_not_stdout_path() {
let home = TempDir::new().expect("temp HOME");
std::env::set_var("HOME", home.path());
std::env::set_var("QUANTUS_NO_UPDATE_CHECK", "1");
std::env::remove_var("QUANTUS_WALLET_PASSWORD");
std::env::remove_var("QUANTUS_WALLET_PASSWORD_EXPORT_FILE");
let manager = WalletManager::new().expect("wallet manager");
manager.create_wallet("export-file", Some("")).await.expect("create wallet");
let mnemonic = manager
.export_mnemonic("export-file", None)
.expect("export mnemonic for fixture");
let out = home.path().join("mnemonic.txt");
handle_wallet_command(
WalletCommands::Export {
name: "export-file".to_string(),
password: None,
format: "mnemonic".to_string(),
output: Some(out.clone()),
},
"ws://127.0.0.1:9944",
)
.await
.expect("export with --output must succeed");
let written = std::fs::read_to_string(&out).expect("export file");
assert_eq!(written.trim(), mnemonic.trim());
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(&out).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600, "export file must be owner-read/write only");
}
}
#[test]
fn wallet_import_rejects_mnemonic_cli_argument() {
let result = TestCli::try_parse_from([
"quantus",
"wallet",
"import",
"--name",
"poc",
"--mnemonic",
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
]);
assert!(result.is_err(), "wallet import must not accept --mnemonic on the command line");
}
#[test]
fn wallet_from_seed_rejects_seed_cli_argument() {
let result = TestCli::try_parse_from([
"quantus",
"wallet",
"from-seed",
"--name",
"poc",
"--seed",
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
]);
assert!(result.is_err(), "wallet from-seed must not accept --seed on the command line");
}
#[test]
fn wallet_scheme_cli_uses_hyphen_before_security_level() {
let parsed = TestCli::try_parse_from([
"quantus",
"wallet",
"create",
"--name",
"poc",
"--scheme",
"ml-dsa-87",
"--allow-empty-password",
])
.expect("ml-dsa-87 must parse");
match parsed.command {
crate::cli::Commands::Wallet(WalletCommands::Create { scheme, .. }) =>
assert_eq!(scheme, DilithiumScheme::MlDsa87),
other => panic!("expected Wallet(Create), got {other:?}"),
}
let legacy = TestCli::try_parse_from([
"quantus",
"wallet",
"create",
"--name",
"poc",
"--scheme",
"ml-dsa87",
"--allow-empty-password",
]);
assert!(legacy.is_err(), "unhyphenated ml-dsa87 must not parse");
}
}