Skip to main content

quantus_cli/wallet/
keystore.rs

1/// Quantum-safe keystore for wallet data
2///
3/// This module handles:
4/// - Quantum-safe encrypting and storing wallet data using Argon2 + AES-256-GCM
5/// - Loading and decrypting wallet data with post-quantum cryptography
6/// - Managing wallet files on disk with quantum-resistant security
7use crate::error::{Result, WalletError};
8use qp_rusty_crystals_dilithium::ml_dsa_87::{Keypair, PublicKey, SecretKey};
9#[cfg(test)]
10use qp_rusty_crystals_hdwallet::SensitiveBytes32;
11use serde::{Deserialize, Serialize};
12#[cfg(test)]
13use sp_core::crypto::Ss58AddressFormat;
14use sp_core::{
15	crypto::{AccountId32, Ss58Codec},
16	ByteArray,
17};
18// Quantum-safe encryption imports
19use aes_gcm::{
20	aead::{Aead, AeadCore, KeyInit, OsRng as AesOsRng},
21	Aes256Gcm, Key, Nonce,
22};
23use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
24use rand::{rng, RngCore};
25
26use std::path::Path;
27
28use qp_dilithium_crypto::types::{DilithiumPair, DilithiumPublic};
29use sp_runtime::traits::IdentifyAccount;
30
31/// Quantum-safe key pair using Dilithium post-quantum signatures
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct QuantumKeyPair {
34	pub public_key: Vec<u8>,
35	pub private_key: Vec<u8>,
36}
37
38impl QuantumKeyPair {
39	/// Create from rusty-crystals Keypair
40	pub fn from_dilithium_keypair(keypair: &Keypair) -> Self {
41		Self {
42			public_key: keypair.public.to_bytes().to_vec(),
43			private_key: keypair.secret.to_bytes().to_vec(),
44		}
45	}
46
47	/// Convert to rusty-crystals Keypair
48	#[allow(dead_code)]
49	pub fn to_dilithium_keypair(&self) -> Result<Keypair> {
50		// TODO: Implement conversion from bytes back to Keypair
51		// For now, generate a new one as placeholder
52		// This function should properly reconstruct the Keypair from stored bytes
53		Ok(Keypair {
54			public: PublicKey::from_bytes(&self.public_key).expect("Failed to parse public key"),
55			secret: SecretKey::from_bytes(&self.private_key).expect("Failed to parse private key"),
56		})
57	}
58
59	/// Convert to DilithiumPair for use with substrate-api-client
60	pub fn to_resonance_pair(&self) -> Result<DilithiumPair> {
61		// Convert our QuantumKeyPair to DilithiumPair using from_raw
62		Ok(DilithiumPair::from_raw(&self.public_key, &self.private_key)
63			.map_err(|_| crate::error::WalletError::KeyGeneration)?)
64	}
65
66	pub fn from_resonance_pair(keypair: &DilithiumPair) -> Self {
67		use sp_core::Pair;
68		Self {
69			public_key: keypair.public().as_ref().to_vec(),
70			private_key: keypair.secret_bytes().to_vec(),
71		}
72	}
73
74	pub fn to_account_id_32(&self) -> AccountId32 {
75		// Use the DilithiumPublic's into_account method for correct address generation
76		let resonance_public =
77			DilithiumPublic::from_slice(&self.public_key).expect("Invalid public key");
78		resonance_public.into_account()
79	}
80
81	pub fn to_account_id_ss58check(&self) -> String {
82		use crate::cli::address_format::quantus_ss58_format;
83		let account = self.to_account_id_32();
84		account.to_ss58check_with_version(quantus_ss58_format())
85	}
86
87	/// Convert to subxt Signer for use
88	pub fn to_subxt_signer(&self) -> Result<qp_dilithium_crypto::types::DilithiumPair> {
89		// Convert to DilithiumPair first - now it implements subxt::tx::Signer<ChainConfig>
90		let resonance_pair = self.to_resonance_pair()?;
91
92		Ok(resonance_pair)
93	}
94
95	#[allow(dead_code)]
96	pub fn ss58_to_account_id(s: &str) -> Vec<u8> {
97		// from_ss58check returns a Result, we unwrap it to panic on invalid input.
98		// We then convert the AccountId32 struct to a Vec<u8> to be compatible with Polkadart's
99		// typedef.
100		AsRef::<[u8]>::as_ref(&AccountId32::from_ss58check_with_version(s).unwrap().0).to_vec()
101	}
102}
103
104/// Quantum-safe encrypted wallet data structure
105#[derive(Debug, Serialize, Deserialize)]
106pub struct EncryptedWallet {
107	pub name: String,
108	pub address: String, // SS58-encoded address (public, not encrypted)
109	pub encrypted_data: Vec<u8>,
110	pub kyber_ciphertext: Vec<u8>, // Reserved for future ML-KEM implementation
111	pub kyber_public_key: Vec<u8>, // Reserved for future ML-KEM implementation
112	pub argon2_salt: Vec<u8>,      // Salt for password-based key derivation
113	pub argon2_params: String,     // Argon2 parameters for verification
114	pub aes_nonce: Vec<u8>,        // AES-GCM nonce
115	pub encryption_version: u32,   // Version for future crypto upgrades
116	pub created_at: chrono::DateTime<chrono::Utc>,
117}
118
119/// Wallet data structure (before encryption)
120#[derive(Debug, Serialize, Deserialize)]
121pub struct WalletData {
122	pub name: String,
123	pub keypair: QuantumKeyPair,
124	pub mnemonic: Option<String>,
125	pub derivation_path: String,
126	pub metadata: std::collections::HashMap<String, String>,
127}
128
129/// Keystore manager for handling encrypted wallet storage
130pub struct Keystore {
131	storage_path: std::path::PathBuf,
132}
133
134impl Keystore {
135	/// Create a new keystore instance
136	pub fn new<P: AsRef<Path>>(storage_path: P) -> Self {
137		Self { storage_path: storage_path.as_ref().to_path_buf() }
138	}
139
140	/// Save an encrypted wallet to disk
141	pub fn save_wallet(&self, wallet: &EncryptedWallet) -> Result<()> {
142		let wallet_file = self.storage_path.join(format!("{}.json", wallet.name));
143		let wallet_json = serde_json::to_string_pretty(wallet)?;
144		std::fs::write(wallet_file, wallet_json)?;
145		Ok(())
146	}
147
148	/// Load an encrypted wallet from disk
149	pub fn load_wallet(&self, name: &str) -> Result<Option<EncryptedWallet>> {
150		let wallet_file = self.storage_path.join(format!("{name}.json"));
151
152		if !wallet_file.exists() {
153			return Ok(None);
154		}
155
156		let wallet_json = std::fs::read_to_string(wallet_file)?;
157		let wallet: EncryptedWallet = serde_json::from_str(&wallet_json)?;
158		Ok(Some(wallet))
159	}
160
161	/// List all wallet files
162	pub fn list_wallets(&self) -> Result<Vec<String>> {
163		let mut wallets = Vec::new();
164
165		if !self.storage_path.exists() {
166			return Ok(wallets);
167		}
168
169		for entry in std::fs::read_dir(&self.storage_path)? {
170			let entry = entry?;
171			let path = entry.path();
172
173			if path.extension().and_then(|s| s.to_str()) == Some("json") {
174				if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
175					wallets.push(name.to_string());
176				}
177			}
178		}
179
180		Ok(wallets)
181	}
182
183	/// Delete a wallet file
184	pub fn delete_wallet(&self, name: &str) -> Result<bool> {
185		let wallet_file = self.storage_path.join(format!("{name}.json"));
186
187		if wallet_file.exists() {
188			std::fs::remove_file(wallet_file)?;
189			Ok(true)
190		} else {
191			Ok(false)
192		}
193	}
194
195	/// Encrypt wallet data using quantum-safe Argon2 + AES-256-GCM
196	/// This provides quantum-safe symmetric encryption with strong password derivation
197	pub fn encrypt_wallet_data(
198		&self,
199		data: &WalletData,
200		password: &str,
201	) -> Result<EncryptedWallet> {
202		// 1. Generate salt for Argon2
203		let mut argon2_salt = [0u8; 16];
204		rng().fill_bytes(&mut argon2_salt);
205
206		// 2. Derive encryption key from password using Argon2 (quantum-safe)
207		let argon2 = Argon2::default();
208		let salt_string = argon2::password_hash::SaltString::encode_b64(&argon2_salt)
209			.map_err(|e| WalletError::Encryption(e.to_string()))?;
210		let password_hash = argon2
211			.hash_password(password.as_bytes(), &salt_string)
212			.map_err(|e| WalletError::Encryption(e.to_string()))?;
213
214		// 3. Use password hash as AES-256 key (quantum-safe with 256-bit key)
215		let hash_bytes = password_hash.hash.as_ref().unwrap().as_bytes();
216		let aes_key = Key::<Aes256Gcm>::from(<[u8; 32]>::try_from(&hash_bytes[..32]).unwrap());
217		let cipher = Aes256Gcm::new(&aes_key);
218
219		// 4. Generate nonce and encrypt the wallet data
220		let nonce = Aes256Gcm::generate_nonce(&mut AesOsRng);
221		let serialized_data = serde_json::to_vec(data)?;
222		let encrypted_data = cipher
223			.encrypt(&nonce, serialized_data.as_ref())
224			.map_err(|e| WalletError::Encryption(e.to_string()))?;
225
226		Ok(EncryptedWallet {
227			name: data.name.clone(),
228			address: data.keypair.to_account_id_ss58check(), // Store public address
229			encrypted_data,
230			kyber_ciphertext: vec![], // Reserved for future ML-KEM implementation
231			kyber_public_key: vec![], // Reserved for future ML-KEM implementation
232			argon2_salt: argon2_salt.to_vec(),
233			argon2_params: password_hash.to_string(),
234			aes_nonce: nonce.to_vec(),
235			encryption_version: 1, // Version 1: Argon2 + AES-256-GCM (quantum-safe)
236			created_at: chrono::Utc::now(),
237		})
238	}
239
240	/// Decrypt wallet data using quantum-safe decryption
241	pub fn decrypt_wallet_data(
242		&self,
243		encrypted: &EncryptedWallet,
244		password: &str,
245	) -> Result<WalletData> {
246		// 1. Verify password using stored Argon2 hash
247		let argon2 = Argon2::default();
248		let password_hash = PasswordHash::new(&encrypted.argon2_params)
249			.map_err(|_| WalletError::InvalidPassword)?;
250
251		argon2
252			.verify_password(password.as_bytes(), &password_hash)
253			.map_err(|_| WalletError::InvalidPassword)?;
254
255		// 2. Derive AES key from verified password hash
256		let hash_bytes = password_hash.hash.as_ref().unwrap().as_bytes();
257		let aes_key = Key::<Aes256Gcm>::from(<[u8; 32]>::try_from(&hash_bytes[..32]).unwrap());
258		let cipher = Aes256Gcm::new(&aes_key);
259
260		// 3. Decrypt the data
261		let nonce = Nonce::from(<[u8; 12]>::try_from(&encrypted.aes_nonce[..]).unwrap());
262		let decrypted_data = cipher
263			.decrypt(&nonce, encrypted.encrypted_data.as_ref())
264			.map_err(|_| WalletError::Decryption)?;
265
266		// 4. Deserialize the wallet data
267		let wallet_data: WalletData = serde_json::from_slice(&decrypted_data)?;
268
269		Ok(wallet_data)
270	}
271}
272
273#[cfg(test)]
274mod tests {
275	use super::*;
276	use qp_dilithium_crypto::{crystal_alice, crystal_charlie, dilithium_bob};
277	use qp_rusty_crystals_dilithium::ml_dsa_87::Keypair;
278	use sp_core::Pair;
279	use tempfile::TempDir;
280
281	#[test]
282	fn test_quantum_keypair_from_dilithium_keypair() {
283		// Generate a test keypair
284		let mut entropy = [1u8; 32];
285		let dilithium_keypair = Keypair::generate(SensitiveBytes32::from(&mut entropy));
286
287		// Convert to QuantumKeyPair
288		let quantum_keypair = QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
289
290		// Verify the conversion
291		assert_eq!(quantum_keypair.public_key, dilithium_keypair.public.to_bytes().to_vec());
292		assert_eq!(quantum_keypair.private_key, dilithium_keypair.secret.to_bytes().to_vec());
293	}
294
295	#[test]
296	fn test_quantum_keypair_to_dilithium_keypair_roundtrip() {
297		// Generate a test keypair
298		let mut entropy = [2u8; 32];
299		let original_keypair = Keypair::generate(SensitiveBytes32::from(&mut entropy));
300
301		// Convert to QuantumKeyPair and back
302		let quantum_keypair = QuantumKeyPair::from_dilithium_keypair(&original_keypair);
303		let converted_keypair =
304			quantum_keypair.to_dilithium_keypair().expect("Conversion should succeed");
305
306		// Verify round-trip conversion preserves data
307		assert_eq!(original_keypair.public.to_bytes(), converted_keypair.public.to_bytes());
308		assert_eq!(original_keypair.secret.to_bytes(), converted_keypair.secret.to_bytes());
309	}
310
311	#[test]
312	fn test_quantum_keypair_from_resonance_pair() {
313		// Test with crystal_alice
314		let resonance_pair = crystal_alice();
315		let quantum_keypair = QuantumKeyPair::from_resonance_pair(&resonance_pair);
316
317		// Verify the conversion
318		assert_eq!(quantum_keypair.public_key, resonance_pair.public().as_ref().to_vec());
319		assert_eq!(quantum_keypair.private_key.as_slice(), resonance_pair.secret_bytes());
320	}
321
322	#[test]
323	fn test_quantum_keypair_to_resonance_pair_roundtrip() {
324		// Test with crystal_bob
325		let original_pair = dilithium_bob();
326		let quantum_keypair = QuantumKeyPair::from_resonance_pair(&original_pair);
327		let converted_pair =
328			quantum_keypair.to_resonance_pair().expect("Conversion should succeed");
329
330		// Verify round-trip conversion preserves data
331		assert_eq!(original_pair.public().as_ref(), converted_pair.public().as_ref());
332		assert_eq!(original_pair.secret_bytes(), converted_pair.secret_bytes());
333	}
334
335	/// Wallet address must match chain: same AccountId (Poseidon hash of Dilithium public)
336	/// and same SS58 prefix (189, "qz") as in chain runtime and genesis.
337	#[test]
338	fn test_quantum_keypair_address_generation() {
339		sp_core::crypto::set_default_ss58_version(sp_core::crypto::Ss58AddressFormat::custom(189));
340		// Same test keypairs as chain genesis (crystal_alice, dilithium_bob, crystal_charlie)
341		let test_pairs = vec![
342			("crystal_alice", crystal_alice()),
343			("crystal_bob", dilithium_bob()),
344			("crystal_charlie", crystal_charlie()),
345		];
346
347		for (name, resonance_pair) in test_pairs {
348			let quantum_keypair = QuantumKeyPair::from_resonance_pair(&resonance_pair);
349
350			// Generate address using both methods
351			let account_id = quantum_keypair.to_account_id_32();
352			let ss58_address = quantum_keypair.to_account_id_ss58check();
353
354			// Verify address format (Quantus SS58 prefix 189 = "qz")
355			assert!(
356				ss58_address.starts_with("qz"),
357				"SS58 address for {name} should start with qz (Quantus prefix 189)"
358			);
359			assert!(
360				ss58_address.len() >= 47,
361				"SS58 address for {name} should be at least 47 characters"
362			);
363
364			// Verify consistency between methods
365			use crate::cli::address_format::quantus_ss58_format;
366			assert_eq!(
367				account_id.to_ss58check_with_version(quantus_ss58_format()),
368				ss58_address,
369				"Address methods should be consistent for {name}"
370			);
371
372			// Must match chain: chain uses same qp_dilithium_crypto IdentifyAccount (into_account)
373			// and SS58 189 in genesis_config_presets and runtime config
374			let chain_expected_address = resonance_pair
375				.public()
376				.into_account()
377				.to_ss58check_with_version(quantus_ss58_format());
378			assert_eq!(
379				ss58_address, chain_expected_address,
380				"Wallet address for {name} must match chain dev account (same derivation and SS58 189)"
381			);
382		}
383	}
384
385	#[test]
386	fn test_ss58_to_account_id_conversion() {
387		sp_core::crypto::set_default_ss58_version(sp_core::crypto::Ss58AddressFormat::custom(189));
388		// Test with known addresses
389		use crate::cli::address_format::quantus_ss58_format;
390		let test_cases = vec![
391			crystal_alice()
392				.public()
393				.into_account()
394				.to_ss58check_with_version(quantus_ss58_format()),
395			dilithium_bob()
396				.public()
397				.into_account()
398				.to_ss58check_with_version(quantus_ss58_format()),
399			crystal_charlie()
400				.public()
401				.into_account()
402				.to_ss58check_with_version(quantus_ss58_format()),
403		];
404
405		for ss58_address in test_cases {
406			// Convert SS58 to account ID bytes
407			let account_bytes = QuantumKeyPair::ss58_to_account_id(&ss58_address);
408
409			// Verify length (AccountId32 should be 32 bytes)
410			assert_eq!(account_bytes.len(), 32, "Account ID should be 32 bytes");
411
412			// Convert back to SS58 and verify round-trip
413			let account_id =
414				AccountId32::from_slice(&account_bytes).expect("Should create valid AccountId32");
415			let round_trip_address =
416				account_id.to_ss58check_with_version(Ss58AddressFormat::custom(189));
417			assert_eq!(
418				ss58_address, round_trip_address,
419				"Round-trip conversion should preserve address"
420			);
421		}
422	}
423
424	#[test]
425	fn test_address_consistency_across_conversions() {
426		// Start with a Dilithium keypair
427		sp_core::crypto::set_default_ss58_version(sp_core::crypto::Ss58AddressFormat::custom(189));
428
429		let mut entropy = [3u8; 32];
430		let dilithium_keypair = Keypair::generate(SensitiveBytes32::from(&mut entropy));
431
432		// Convert through different paths
433		let quantum_from_dilithium = QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
434		let resonance_from_quantum =
435			quantum_from_dilithium.to_resonance_pair().expect("Should convert");
436		let quantum_from_resonance = QuantumKeyPair::from_resonance_pair(&resonance_from_quantum);
437
438		// All should generate the same address
439		let addr1 = quantum_from_dilithium.to_account_id_ss58check();
440		let addr2 = quantum_from_resonance.to_account_id_ss58check();
441		let addr3 = resonance_from_quantum
442			.public()
443			.into_account()
444			.to_ss58check_with_version(Ss58AddressFormat::custom(189));
445
446		assert_eq!(addr1, addr2, "Addresses should be consistent across conversion paths");
447		assert_eq!(addr2, addr3, "Address should match direct DilithiumPair calculation");
448	}
449
450	#[test]
451	fn test_known_test_wallet_addresses() {
452		// Test that our test wallets generate expected addresses
453		sp_core::crypto::set_default_ss58_version(sp_core::crypto::Ss58AddressFormat::custom(189));
454		let alice_pair = crystal_alice();
455		let bob_pair = dilithium_bob();
456		let charlie_pair = crystal_charlie();
457
458		let alice_quantum = QuantumKeyPair::from_resonance_pair(&alice_pair);
459		let bob_quantum = QuantumKeyPair::from_resonance_pair(&bob_pair);
460		let charlie_quantum = QuantumKeyPair::from_resonance_pair(&charlie_pair);
461
462		let alice_addr = alice_quantum.to_account_id_ss58check();
463		let bob_addr = bob_quantum.to_account_id_ss58check();
464		let charlie_addr = charlie_quantum.to_account_id_ss58check();
465
466		// Addresses should be different
467		assert_ne!(alice_addr, bob_addr, "Alice and Bob should have different addresses");
468		assert_ne!(bob_addr, charlie_addr, "Bob and Charlie should have different addresses");
469		assert_ne!(alice_addr, charlie_addr, "Alice and Charlie should have different addresses");
470
471		// All should be valid SS58 addresses
472		assert!(alice_addr.starts_with("qz"), "Alice address should be valid SS58");
473		assert!(bob_addr.starts_with("qz"), "Bob address should be valid SS58");
474		assert!(charlie_addr.starts_with("qz"), "Charlie address should be valid SS58");
475
476		println!("Test wallet addresses:");
477		println!("  Alice:   {alice_addr}");
478		println!("  Bob:     {bob_addr}");
479		println!("  Charlie: {charlie_addr}");
480	}
481
482	#[test]
483	fn test_invalid_ss58_address_handling() {
484		// Test with invalid SS58 addresses
485		let invalid_addresses = vec![
486			"invalid",
487			"5",          // Too short
488			"1234567890", // Wrong format
489			"",           // Empty
490		];
491
492		for invalid_addr in invalid_addresses {
493			let result =
494				std::panic::catch_unwind(|| QuantumKeyPair::ss58_to_account_id(invalid_addr));
495			assert!(result.is_err(), "Should panic on invalid address: {invalid_addr}");
496		}
497	}
498
499	#[test]
500	fn test_stored_wallet_address_generation() {
501		sp_core::crypto::set_default_ss58_version(sp_core::crypto::Ss58AddressFormat::custom(189));
502
503		// This test reproduces the error that occurs when loading a wallet from disk
504		// and trying to generate its address - simulating the real-world scenario
505
506		// Create a test wallet like the developer wallets
507		let alice_pair = crystal_alice();
508		let quantum_keypair = QuantumKeyPair::from_resonance_pair(&alice_pair);
509
510		// Create wallet data like what gets stored
511		let mut metadata = std::collections::HashMap::new();
512		metadata.insert("version".to_string(), "1.0.0".to_string());
513		metadata.insert("algorithm".to_string(), "ML-DSA-87".to_string());
514		metadata.insert("test_wallet".to_string(), "true".to_string());
515
516		let wallet_data = WalletData {
517			name: "test_crystal_alice".to_string(),
518			keypair: quantum_keypair.clone(),
519			mnemonic: None,
520			derivation_path: "m/".to_string(),
521			metadata,
522		};
523
524		// Test that we can generate address from the stored keypair
525		let result = std::panic::catch_unwind(|| wallet_data.keypair.to_account_id_ss58check());
526
527		match result {
528			Ok(address) => {
529				println!("✅ Address generation successful: {address}");
530				// Verify it matches the expected address
531				let expected = alice_pair
532					.public()
533					.into_account()
534					.to_ss58check_with_version(Ss58AddressFormat::custom(189));
535				assert_eq!(address, expected, "Stored wallet should generate correct address");
536			},
537			Err(_) => {
538				panic!("❌ Address generation failed - this is the bug we need to fix!");
539			},
540		}
541	}
542
543	#[test]
544	fn test_encrypted_wallet_address_generation() {
545		// This test simulates the full encryption/decryption cycle that happens
546		// when creating a developer wallet and then trying to use it for sending
547
548		let temp_dir = tempfile::TempDir::new().expect("Failed to create temp directory");
549		let keystore = Keystore::new(temp_dir.path());
550
551		// Create a developer wallet like crystal_alice
552		let alice_pair = crystal_alice();
553		let quantum_keypair = QuantumKeyPair::from_resonance_pair(&alice_pair);
554
555		let mut metadata = std::collections::HashMap::new();
556		metadata.insert("version".to_string(), "1.0.0".to_string());
557		metadata.insert("algorithm".to_string(), "ML-DSA-87".to_string());
558		metadata.insert("test_wallet".to_string(), "true".to_string());
559
560		let wallet_data = WalletData {
561			name: "test_crystal_alice".to_string(),
562			keypair: quantum_keypair,
563			mnemonic: None,
564			derivation_path: "m/".to_string(),
565			metadata,
566		};
567
568		// Encrypt the wallet (like developer wallets use empty password)
569		let encrypted_wallet = keystore
570			.encrypt_wallet_data(&wallet_data, "")
571			.expect("Encryption should succeed");
572
573		// Save and reload the wallet
574		keystore.save_wallet(&encrypted_wallet).expect("Save should succeed");
575		let loaded_wallet = keystore
576			.load_wallet("test_crystal_alice")
577			.expect("Load should succeed")
578			.expect("Wallet should exist");
579
580		// Decrypt the wallet (this is where the send command would decrypt it)
581		let decrypted_data = keystore
582			.decrypt_wallet_data(&loaded_wallet, "")
583			.expect("Decryption should succeed");
584
585		// Test that we can generate address from the decrypted keypair
586		let result = std::panic::catch_unwind(|| decrypted_data.keypair.to_account_id_ss58check());
587
588		match result {
589			Ok(address) => {
590				println!("✅ Encrypted wallet address generation successful: {address}");
591				// Verify it matches the expected address
592				let expected = alice_pair
593					.public()
594					.into_account()
595					.to_ss58check_with_version(Ss58AddressFormat::custom(189));
596				assert_eq!(address, expected, "Decrypted wallet should generate correct address");
597			},
598			Err(_) => {
599				panic!("❌ Encrypted wallet address generation failed - this reproduces the send command bug!");
600			},
601		}
602	}
603
604	#[test]
605	fn test_send_command_wallet_loading_flow() {
606		// This test reproduces the exact bug in the send command
607		// The send command calls wallet_manager.load_wallet() which returns dummy data
608		// then tries to generate an address from that dummy data, causing the panic
609
610		let temp_dir = TempDir::new().expect("Failed to create temp directory");
611		let keystore = Keystore::new(temp_dir.path());
612
613		// Create and save a developer wallet like crystal_alice
614		let alice_pair = crystal_alice();
615		let quantum_keypair = QuantumKeyPair::from_resonance_pair(&alice_pair);
616
617		let mut metadata = std::collections::HashMap::new();
618		metadata.insert("version".to_string(), "1.0.0".to_string());
619		metadata.insert("algorithm".to_string(), "ML-DSA-87".to_string());
620		metadata.insert("test_wallet".to_string(), "true".to_string());
621
622		let wallet_data = WalletData {
623			name: "crystal_alice".to_string(),
624			keypair: quantum_keypair,
625			mnemonic: None,
626			derivation_path: "m/".to_string(),
627			metadata,
628		};
629
630		// Encrypt and save the wallet (like developer wallets use empty password)
631		let encrypted_wallet = keystore
632			.encrypt_wallet_data(&wallet_data, "")
633			.expect("Encryption should succeed");
634		keystore.save_wallet(&encrypted_wallet).expect("Save should succeed");
635
636		// Now simulate what the send command does:
637		// 1. Create a WalletManager and load the wallet with password
638		use crate::wallet::WalletManager;
639		let wallet_manager = WalletManager { wallets_dir: temp_dir.path().to_path_buf() };
640		let loaded_wallet_data =
641			wallet_manager.load_wallet("crystal_alice", "").expect("Should load wallet");
642
643		// 2. Try to generate address from the loaded keypair (should work now)
644		let result = std::panic::catch_unwind(|| {
645			// The keypair is already decrypted, so we can use it directly
646			loaded_wallet_data.keypair.to_account_id_ss58check()
647		});
648
649		match result {
650			Ok(address) => {
651				println!("✅ Send command flow works: {address}");
652				// If this passes, the bug is fixed
653				let expected = alice_pair
654					.public()
655					.into_account()
656					.to_ss58check_with_version(Ss58AddressFormat::custom(189));
657				assert_eq!(address, expected, "Loaded wallet should generate correct address");
658			},
659			Err(_) => {
660				println!("❌ Send command flow failed - this reproduces the bug!");
661				// This test should fail initially, proving we found the bug
662				panic!(
663					"This test reproduces the send command bug - load_wallet returns dummy data!"
664				);
665			},
666		}
667	}
668
669	#[test]
670	fn test_keypair_data_integrity() {
671		// Generate multiple keypairs and verify they maintain data integrity
672		for i in 0..5 {
673			let mut entropy = [i as u8; 32];
674			let dilithium_keypair = Keypair::generate(SensitiveBytes32::from(&mut entropy));
675			let quantum_keypair = QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
676
677			// Print actual key sizes for debugging (first iteration only)
678			if i == 0 {
679				println!("Actual public key size: {}", quantum_keypair.public_key.len());
680				println!("Actual private key size: {}", quantum_keypair.private_key.len());
681			}
682
683			// Verify key sizes are consistent and reasonable
684			assert!(
685				quantum_keypair.public_key.len() > 1000,
686				"Public key should be reasonably large (actual: {})",
687				quantum_keypair.public_key.len()
688			);
689			assert!(
690				quantum_keypair.private_key.len() > 2000,
691				"Private key should be reasonably large (actual: {})",
692				quantum_keypair.private_key.len()
693			);
694
695			// Verify keys are not all zeros
696			assert!(
697				quantum_keypair.public_key.iter().any(|&b| b != 0),
698				"Public key should not be all zeros"
699			);
700			assert!(
701				quantum_keypair.private_key.iter().any(|&b| b != 0),
702				"Private key should not be all zeros"
703			);
704		}
705	}
706}