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