quantus-cli 1.3.4

Command line interface and library for interacting with the Quantus Network
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
/// Quantum-safe keystore for wallet data
///
/// This module handles:
/// - Quantum-safe encrypting and storing wallet data using Argon2 + AES-256-GCM
/// - Loading and decrypting wallet data with post-quantum cryptography
/// - Managing wallet files on disk with quantum-resistant security
use crate::error::{Result, WalletError};
use qp_rusty_crystals_dilithium::ml_dsa_87::{Keypair, PublicKey, SecretKey};
#[cfg(test)]
use qp_rusty_crystals_hdwallet::SensitiveBytes32;
use serde::{Deserialize, Serialize};
#[cfg(test)]
use sp_core::crypto::Ss58AddressFormat;
use sp_core::{
	crypto::{AccountId32, Ss58Codec},
	ByteArray,
};
// Quantum-safe encryption imports
use aes_gcm::{
	aead::{Aead, AeadCore, KeyInit, OsRng as AesOsRng},
	Aes256Gcm, Key, Nonce,
};
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use rand::{rng, RngCore};

use std::path::Path;

use qp_dilithium_crypto::types::{DilithiumPair, DilithiumPublic};
use sp_runtime::traits::IdentifyAccount;

/// Quantum-safe key pair using Dilithium post-quantum signatures
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuantumKeyPair {
	pub public_key: Vec<u8>,
	pub private_key: Vec<u8>,
}

impl QuantumKeyPair {
	/// Create from rusty-crystals Keypair
	pub fn from_dilithium_keypair(keypair: &Keypair) -> Self {
		Self {
			public_key: keypair.public.to_bytes().to_vec(),
			private_key: keypair.secret.to_bytes().to_vec(),
		}
	}

	/// Convert to rusty-crystals Keypair
	#[allow(dead_code)]
	pub fn to_dilithium_keypair(&self) -> Result<Keypair> {
		// TODO: Implement conversion from bytes back to Keypair
		// For now, generate a new one as placeholder
		// This function should properly reconstruct the Keypair from stored bytes
		Ok(Keypair {
			public: PublicKey::from_bytes(&self.public_key).expect("Failed to parse public key"),
			secret: SecretKey::from_bytes(&self.private_key).expect("Failed to parse private key"),
		})
	}

	/// Convert to DilithiumPair for use with substrate-api-client
	pub fn to_resonance_pair(&self) -> Result<DilithiumPair> {
		// Convert our QuantumKeyPair to DilithiumPair using from_raw
		Ok(DilithiumPair::from_raw(&self.public_key, &self.private_key)
			.map_err(|_| crate::error::WalletError::KeyGeneration)?)
	}

	pub fn from_resonance_pair(keypair: &DilithiumPair) -> Self {
		use sp_core::Pair;
		Self {
			public_key: keypair.public().as_ref().to_vec(),
			private_key: keypair.secret_bytes().to_vec(),
		}
	}

	pub fn to_account_id_32(&self) -> AccountId32 {
		// Use the DilithiumPublic's into_account method for correct address generation
		let resonance_public =
			DilithiumPublic::from_slice(&self.public_key).expect("Invalid public key");
		resonance_public.into_account()
	}

	pub fn to_account_id_ss58check(&self) -> String {
		use crate::cli::address_format::quantus_ss58_format;
		let account = self.to_account_id_32();
		account.to_ss58check_with_version(quantus_ss58_format())
	}

	/// Convert to subxt Signer for use
	pub fn to_subxt_signer(&self) -> Result<qp_dilithium_crypto::types::DilithiumPair> {
		// Convert to DilithiumPair first - now it implements subxt::tx::Signer<ChainConfig>
		let resonance_pair = self.to_resonance_pair()?;

		Ok(resonance_pair)
	}

	#[allow(dead_code)]
	pub fn ss58_to_account_id(s: &str) -> Vec<u8> {
		// from_ss58check returns a Result, we unwrap it to panic on invalid input.
		// We then convert the AccountId32 struct to a Vec<u8> to be compatible with Polkadart's
		// typedef.
		AsRef::<[u8]>::as_ref(&AccountId32::from_ss58check_with_version(s).unwrap().0).to_vec()
	}
}

/// Quantum-safe encrypted wallet data structure
#[derive(Debug, Serialize, Deserialize)]
pub struct EncryptedWallet {
	pub name: String,
	pub address: String, // SS58-encoded address (public, not encrypted)
	pub encrypted_data: Vec<u8>,
	pub kyber_ciphertext: Vec<u8>, // Reserved for future ML-KEM implementation
	pub kyber_public_key: Vec<u8>, // Reserved for future ML-KEM implementation
	pub argon2_salt: Vec<u8>,      // Salt for password-based key derivation
	pub argon2_params: String,     // Argon2 parameters for verification
	pub aes_nonce: Vec<u8>,        // AES-GCM nonce
	pub encryption_version: u32,   // Version for future crypto upgrades
	pub created_at: chrono::DateTime<chrono::Utc>,
}

/// Wallet data structure (before encryption)
#[derive(Debug, Serialize, Deserialize)]
pub struct WalletData {
	pub name: String,
	pub keypair: QuantumKeyPair,
	pub mnemonic: Option<String>,
	pub derivation_path: String,
	pub metadata: std::collections::HashMap<String, String>,
}

/// Keystore manager for handling encrypted wallet storage
pub struct Keystore {
	storage_path: std::path::PathBuf,
}

impl Keystore {
	/// Create a new keystore instance
	pub fn new<P: AsRef<Path>>(storage_path: P) -> Self {
		Self { storage_path: storage_path.as_ref().to_path_buf() }
	}

	/// Save an encrypted wallet to disk
	pub fn save_wallet(&self, wallet: &EncryptedWallet) -> Result<()> {
		let wallet_file = self.storage_path.join(format!("{}.json", wallet.name));
		let wallet_json = serde_json::to_string_pretty(wallet)?;
		std::fs::write(wallet_file, wallet_json)?;
		Ok(())
	}

	/// Load an encrypted wallet from disk
	pub fn load_wallet(&self, name: &str) -> Result<Option<EncryptedWallet>> {
		let wallet_file = self.storage_path.join(format!("{name}.json"));

		if !wallet_file.exists() {
			return Ok(None);
		}

		let wallet_json = std::fs::read_to_string(wallet_file)?;
		let wallet: EncryptedWallet = serde_json::from_str(&wallet_json)?;
		Ok(Some(wallet))
	}

	/// List all wallet files
	pub fn list_wallets(&self) -> Result<Vec<String>> {
		let mut wallets = Vec::new();

		if !self.storage_path.exists() {
			return Ok(wallets);
		}

		for entry in std::fs::read_dir(&self.storage_path)? {
			let entry = entry?;
			let path = entry.path();

			if path.extension().and_then(|s| s.to_str()) == Some("json") {
				if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
					wallets.push(name.to_string());
				}
			}
		}

		Ok(wallets)
	}

	/// Delete a wallet file
	pub fn delete_wallet(&self, name: &str) -> Result<bool> {
		let wallet_file = self.storage_path.join(format!("{name}.json"));

		if wallet_file.exists() {
			std::fs::remove_file(wallet_file)?;
			Ok(true)
		} else {
			Ok(false)
		}
	}

	/// Encrypt wallet data using quantum-safe Argon2 + AES-256-GCM
	/// This provides quantum-safe symmetric encryption with strong password derivation
	pub fn encrypt_wallet_data(
		&self,
		data: &WalletData,
		password: &str,
	) -> Result<EncryptedWallet> {
		// 1. Generate salt for Argon2
		let mut argon2_salt = [0u8; 16];
		rng().fill_bytes(&mut argon2_salt);

		// 2. Derive encryption key from password using Argon2 (quantum-safe)
		let argon2 = Argon2::default();
		let salt_string = argon2::password_hash::SaltString::encode_b64(&argon2_salt)
			.map_err(|e| WalletError::Encryption(e.to_string()))?;
		let password_hash = argon2
			.hash_password(password.as_bytes(), &salt_string)
			.map_err(|e| WalletError::Encryption(e.to_string()))?;

		// 3. Use password hash as AES-256 key (quantum-safe with 256-bit key)
		let hash_bytes = password_hash.hash.as_ref().unwrap().as_bytes();
		let aes_key = Key::<Aes256Gcm>::from(<[u8; 32]>::try_from(&hash_bytes[..32]).unwrap());
		let cipher = Aes256Gcm::new(&aes_key);

		// 4. Generate nonce and encrypt the wallet data
		let nonce = Aes256Gcm::generate_nonce(&mut AesOsRng);
		let serialized_data = serde_json::to_vec(data)?;
		let encrypted_data = cipher
			.encrypt(&nonce, serialized_data.as_ref())
			.map_err(|e| WalletError::Encryption(e.to_string()))?;

		Ok(EncryptedWallet {
			name: data.name.clone(),
			address: data.keypair.to_account_id_ss58check(), // Store public address
			encrypted_data,
			kyber_ciphertext: vec![], // Reserved for future ML-KEM implementation
			kyber_public_key: vec![], // Reserved for future ML-KEM implementation
			argon2_salt: argon2_salt.to_vec(),
			argon2_params: password_hash.to_string(),
			aes_nonce: nonce.to_vec(),
			encryption_version: 1, // Version 1: Argon2 + AES-256-GCM (quantum-safe)
			created_at: chrono::Utc::now(),
		})
	}

	/// Decrypt wallet data using quantum-safe decryption
	pub fn decrypt_wallet_data(
		&self,
		encrypted: &EncryptedWallet,
		password: &str,
	) -> Result<WalletData> {
		// 1. Verify password using stored Argon2 hash
		let argon2 = Argon2::default();
		let password_hash = PasswordHash::new(&encrypted.argon2_params)
			.map_err(|_| WalletError::InvalidPassword)?;

		argon2
			.verify_password(password.as_bytes(), &password_hash)
			.map_err(|_| WalletError::InvalidPassword)?;

		// 2. Derive AES key from verified password hash
		let hash_bytes = password_hash.hash.as_ref().unwrap().as_bytes();
		let aes_key = Key::<Aes256Gcm>::from(<[u8; 32]>::try_from(&hash_bytes[..32]).unwrap());
		let cipher = Aes256Gcm::new(&aes_key);

		// 3. Decrypt the data
		let nonce = Nonce::from(<[u8; 12]>::try_from(&encrypted.aes_nonce[..]).unwrap());
		let decrypted_data = cipher
			.decrypt(&nonce, encrypted.encrypted_data.as_ref())
			.map_err(|_| WalletError::Decryption)?;

		// 4. Deserialize the wallet data
		let wallet_data: WalletData = serde_json::from_slice(&decrypted_data)?;

		Ok(wallet_data)
	}
}

#[cfg(test)]
mod tests {
	use super::*;
	use qp_dilithium_crypto::{crystal_alice, crystal_charlie, dilithium_bob};
	use qp_rusty_crystals_dilithium::ml_dsa_87::Keypair;
	use sp_core::Pair;
	use tempfile::TempDir;

	#[test]
	fn test_quantum_keypair_from_dilithium_keypair() {
		// Generate a test keypair
		let mut entropy = [1u8; 32];
		let dilithium_keypair = Keypair::generate(SensitiveBytes32::from(&mut entropy));

		// Convert to QuantumKeyPair
		let quantum_keypair = QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);

		// Verify the conversion
		assert_eq!(quantum_keypair.public_key, dilithium_keypair.public.to_bytes().to_vec());
		assert_eq!(quantum_keypair.private_key, dilithium_keypair.secret.to_bytes().to_vec());
	}

	#[test]
	fn test_quantum_keypair_to_dilithium_keypair_roundtrip() {
		// Generate a test keypair
		let mut entropy = [2u8; 32];
		let original_keypair = Keypair::generate(SensitiveBytes32::from(&mut entropy));

		// Convert to QuantumKeyPair and back
		let quantum_keypair = QuantumKeyPair::from_dilithium_keypair(&original_keypair);
		let converted_keypair =
			quantum_keypair.to_dilithium_keypair().expect("Conversion should succeed");

		// Verify round-trip conversion preserves data
		assert_eq!(original_keypair.public.to_bytes(), converted_keypair.public.to_bytes());
		assert_eq!(original_keypair.secret.to_bytes(), converted_keypair.secret.to_bytes());
	}

	#[test]
	fn test_quantum_keypair_from_resonance_pair() {
		// Test with crystal_alice
		let resonance_pair = crystal_alice();
		let quantum_keypair = QuantumKeyPair::from_resonance_pair(&resonance_pair);

		// Verify the conversion
		assert_eq!(quantum_keypair.public_key, resonance_pair.public().as_ref().to_vec());
		assert_eq!(quantum_keypair.private_key.as_slice(), resonance_pair.secret_bytes());
	}

	#[test]
	fn test_quantum_keypair_to_resonance_pair_roundtrip() {
		// Test with crystal_bob
		let original_pair = dilithium_bob();
		let quantum_keypair = QuantumKeyPair::from_resonance_pair(&original_pair);
		let converted_pair =
			quantum_keypair.to_resonance_pair().expect("Conversion should succeed");

		// Verify round-trip conversion preserves data
		assert_eq!(original_pair.public().as_ref(), converted_pair.public().as_ref());
		assert_eq!(original_pair.secret_bytes(), converted_pair.secret_bytes());
	}

	/// Wallet address must match chain: same AccountId (Poseidon hash of Dilithium public)
	/// and same SS58 prefix (189, "qz") as in chain runtime and genesis.
	#[test]
	fn test_quantum_keypair_address_generation() {
		sp_core::crypto::set_default_ss58_version(sp_core::crypto::Ss58AddressFormat::custom(189));
		// Same test keypairs as chain genesis (crystal_alice, dilithium_bob, crystal_charlie)
		let test_pairs = vec![
			("crystal_alice", crystal_alice()),
			("crystal_bob", dilithium_bob()),
			("crystal_charlie", crystal_charlie()),
		];

		for (name, resonance_pair) in test_pairs {
			let quantum_keypair = QuantumKeyPair::from_resonance_pair(&resonance_pair);

			// Generate address using both methods
			let account_id = quantum_keypair.to_account_id_32();
			let ss58_address = quantum_keypair.to_account_id_ss58check();

			// Verify address format (Quantus SS58 prefix 189 = "qz")
			assert!(
				ss58_address.starts_with("qz"),
				"SS58 address for {name} should start with qz (Quantus prefix 189)"
			);
			assert!(
				ss58_address.len() >= 47,
				"SS58 address for {name} should be at least 47 characters"
			);

			// Verify consistency between methods
			use crate::cli::address_format::quantus_ss58_format;
			assert_eq!(
				account_id.to_ss58check_with_version(quantus_ss58_format()),
				ss58_address,
				"Address methods should be consistent for {name}"
			);

			// Must match chain: chain uses same qp_dilithium_crypto IdentifyAccount (into_account)
			// and SS58 189 in genesis_config_presets and runtime config
			let chain_expected_address = resonance_pair
				.public()
				.into_account()
				.to_ss58check_with_version(quantus_ss58_format());
			assert_eq!(
				ss58_address, chain_expected_address,
				"Wallet address for {name} must match chain dev account (same derivation and SS58 189)"
			);
		}
	}

	#[test]
	fn test_ss58_to_account_id_conversion() {
		sp_core::crypto::set_default_ss58_version(sp_core::crypto::Ss58AddressFormat::custom(189));
		// Test with known addresses
		use crate::cli::address_format::quantus_ss58_format;
		let test_cases = vec![
			crystal_alice()
				.public()
				.into_account()
				.to_ss58check_with_version(quantus_ss58_format()),
			dilithium_bob()
				.public()
				.into_account()
				.to_ss58check_with_version(quantus_ss58_format()),
			crystal_charlie()
				.public()
				.into_account()
				.to_ss58check_with_version(quantus_ss58_format()),
		];

		for ss58_address in test_cases {
			// Convert SS58 to account ID bytes
			let account_bytes = QuantumKeyPair::ss58_to_account_id(&ss58_address);

			// Verify length (AccountId32 should be 32 bytes)
			assert_eq!(account_bytes.len(), 32, "Account ID should be 32 bytes");

			// Convert back to SS58 and verify round-trip
			let account_id =
				AccountId32::from_slice(&account_bytes).expect("Should create valid AccountId32");
			let round_trip_address =
				account_id.to_ss58check_with_version(Ss58AddressFormat::custom(189));
			assert_eq!(
				ss58_address, round_trip_address,
				"Round-trip conversion should preserve address"
			);
		}
	}

	#[test]
	fn test_address_consistency_across_conversions() {
		// Start with a Dilithium keypair
		sp_core::crypto::set_default_ss58_version(sp_core::crypto::Ss58AddressFormat::custom(189));

		let mut entropy = [3u8; 32];
		let dilithium_keypair = Keypair::generate(SensitiveBytes32::from(&mut entropy));

		// Convert through different paths
		let quantum_from_dilithium = QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
		let resonance_from_quantum =
			quantum_from_dilithium.to_resonance_pair().expect("Should convert");
		let quantum_from_resonance = QuantumKeyPair::from_resonance_pair(&resonance_from_quantum);

		// All should generate the same address
		let addr1 = quantum_from_dilithium.to_account_id_ss58check();
		let addr2 = quantum_from_resonance.to_account_id_ss58check();
		let addr3 = resonance_from_quantum
			.public()
			.into_account()
			.to_ss58check_with_version(Ss58AddressFormat::custom(189));

		assert_eq!(addr1, addr2, "Addresses should be consistent across conversion paths");
		assert_eq!(addr2, addr3, "Address should match direct DilithiumPair calculation");
	}

	#[test]
	fn test_known_test_wallet_addresses() {
		// Test that our test wallets generate expected addresses
		sp_core::crypto::set_default_ss58_version(sp_core::crypto::Ss58AddressFormat::custom(189));
		let alice_pair = crystal_alice();
		let bob_pair = dilithium_bob();
		let charlie_pair = crystal_charlie();

		let alice_quantum = QuantumKeyPair::from_resonance_pair(&alice_pair);
		let bob_quantum = QuantumKeyPair::from_resonance_pair(&bob_pair);
		let charlie_quantum = QuantumKeyPair::from_resonance_pair(&charlie_pair);

		let alice_addr = alice_quantum.to_account_id_ss58check();
		let bob_addr = bob_quantum.to_account_id_ss58check();
		let charlie_addr = charlie_quantum.to_account_id_ss58check();

		// Addresses should be different
		assert_ne!(alice_addr, bob_addr, "Alice and Bob should have different addresses");
		assert_ne!(bob_addr, charlie_addr, "Bob and Charlie should have different addresses");
		assert_ne!(alice_addr, charlie_addr, "Alice and Charlie should have different addresses");

		// All should be valid SS58 addresses
		assert!(alice_addr.starts_with("qz"), "Alice address should be valid SS58");
		assert!(bob_addr.starts_with("qz"), "Bob address should be valid SS58");
		assert!(charlie_addr.starts_with("qz"), "Charlie address should be valid SS58");

		println!("Test wallet addresses:");
		println!("  Alice:   {alice_addr}");
		println!("  Bob:     {bob_addr}");
		println!("  Charlie: {charlie_addr}");
	}

	#[test]
	fn test_invalid_ss58_address_handling() {
		// Test with invalid SS58 addresses
		let invalid_addresses = vec![
			"invalid",
			"5",          // Too short
			"1234567890", // Wrong format
			"",           // Empty
		];

		for invalid_addr in invalid_addresses {
			let result =
				std::panic::catch_unwind(|| QuantumKeyPair::ss58_to_account_id(invalid_addr));
			assert!(result.is_err(), "Should panic on invalid address: {invalid_addr}");
		}
	}

	#[test]
	fn test_stored_wallet_address_generation() {
		sp_core::crypto::set_default_ss58_version(sp_core::crypto::Ss58AddressFormat::custom(189));

		// This test reproduces the error that occurs when loading a wallet from disk
		// and trying to generate its address - simulating the real-world scenario

		// Create a test wallet like the developer wallets
		let alice_pair = crystal_alice();
		let quantum_keypair = QuantumKeyPair::from_resonance_pair(&alice_pair);

		// Create wallet data like what gets stored
		let mut metadata = std::collections::HashMap::new();
		metadata.insert("version".to_string(), "1.0.0".to_string());
		metadata.insert("algorithm".to_string(), "ML-DSA-87".to_string());
		metadata.insert("test_wallet".to_string(), "true".to_string());

		let wallet_data = WalletData {
			name: "test_crystal_alice".to_string(),
			keypair: quantum_keypair.clone(),
			mnemonic: None,
			derivation_path: "m/".to_string(),
			metadata,
		};

		// Test that we can generate address from the stored keypair
		let result = std::panic::catch_unwind(|| wallet_data.keypair.to_account_id_ss58check());

		match result {
			Ok(address) => {
				println!("✅ Address generation successful: {address}");
				// Verify it matches the expected address
				let expected = alice_pair
					.public()
					.into_account()
					.to_ss58check_with_version(Ss58AddressFormat::custom(189));
				assert_eq!(address, expected, "Stored wallet should generate correct address");
			},
			Err(_) => {
				panic!("❌ Address generation failed - this is the bug we need to fix!");
			},
		}
	}

	#[test]
	fn test_encrypted_wallet_address_generation() {
		// This test simulates the full encryption/decryption cycle that happens
		// when creating a developer wallet and then trying to use it for sending

		let temp_dir = tempfile::TempDir::new().expect("Failed to create temp directory");
		let keystore = Keystore::new(temp_dir.path());

		// Create a developer wallet like crystal_alice
		let alice_pair = crystal_alice();
		let quantum_keypair = QuantumKeyPair::from_resonance_pair(&alice_pair);

		let mut metadata = std::collections::HashMap::new();
		metadata.insert("version".to_string(), "1.0.0".to_string());
		metadata.insert("algorithm".to_string(), "ML-DSA-87".to_string());
		metadata.insert("test_wallet".to_string(), "true".to_string());

		let wallet_data = WalletData {
			name: "test_crystal_alice".to_string(),
			keypair: quantum_keypair,
			mnemonic: None,
			derivation_path: "m/".to_string(),
			metadata,
		};

		// Encrypt the wallet (like developer wallets use empty password)
		let encrypted_wallet = keystore
			.encrypt_wallet_data(&wallet_data, "")
			.expect("Encryption should succeed");

		// Save and reload the wallet
		keystore.save_wallet(&encrypted_wallet).expect("Save should succeed");
		let loaded_wallet = keystore
			.load_wallet("test_crystal_alice")
			.expect("Load should succeed")
			.expect("Wallet should exist");

		// Decrypt the wallet (this is where the send command would decrypt it)
		let decrypted_data = keystore
			.decrypt_wallet_data(&loaded_wallet, "")
			.expect("Decryption should succeed");

		// Test that we can generate address from the decrypted keypair
		let result = std::panic::catch_unwind(|| decrypted_data.keypair.to_account_id_ss58check());

		match result {
			Ok(address) => {
				println!("✅ Encrypted wallet address generation successful: {address}");
				// Verify it matches the expected address
				let expected = alice_pair
					.public()
					.into_account()
					.to_ss58check_with_version(Ss58AddressFormat::custom(189));
				assert_eq!(address, expected, "Decrypted wallet should generate correct address");
			},
			Err(_) => {
				panic!("❌ Encrypted wallet address generation failed - this reproduces the send command bug!");
			},
		}
	}

	#[test]
	fn test_send_command_wallet_loading_flow() {
		// This test reproduces the exact bug in the send command
		// The send command calls wallet_manager.load_wallet() which returns dummy data
		// then tries to generate an address from that dummy data, causing the panic

		let temp_dir = TempDir::new().expect("Failed to create temp directory");
		let keystore = Keystore::new(temp_dir.path());

		// Create and save a developer wallet like crystal_alice
		let alice_pair = crystal_alice();
		let quantum_keypair = QuantumKeyPair::from_resonance_pair(&alice_pair);

		let mut metadata = std::collections::HashMap::new();
		metadata.insert("version".to_string(), "1.0.0".to_string());
		metadata.insert("algorithm".to_string(), "ML-DSA-87".to_string());
		metadata.insert("test_wallet".to_string(), "true".to_string());

		let wallet_data = WalletData {
			name: "crystal_alice".to_string(),
			keypair: quantum_keypair,
			mnemonic: None,
			derivation_path: "m/".to_string(),
			metadata,
		};

		// Encrypt and save the wallet (like developer wallets use empty password)
		let encrypted_wallet = keystore
			.encrypt_wallet_data(&wallet_data, "")
			.expect("Encryption should succeed");
		keystore.save_wallet(&encrypted_wallet).expect("Save should succeed");

		// Now simulate what the send command does:
		// 1. Create a WalletManager and load the wallet with password
		use crate::wallet::WalletManager;
		let wallet_manager = WalletManager { wallets_dir: temp_dir.path().to_path_buf() };
		let loaded_wallet_data =
			wallet_manager.load_wallet("crystal_alice", "").expect("Should load wallet");

		// 2. Try to generate address from the loaded keypair (should work now)
		let result = std::panic::catch_unwind(|| {
			// The keypair is already decrypted, so we can use it directly
			loaded_wallet_data.keypair.to_account_id_ss58check()
		});

		match result {
			Ok(address) => {
				println!("✅ Send command flow works: {address}");
				// If this passes, the bug is fixed
				let expected = alice_pair
					.public()
					.into_account()
					.to_ss58check_with_version(Ss58AddressFormat::custom(189));
				assert_eq!(address, expected, "Loaded wallet should generate correct address");
			},
			Err(_) => {
				println!("❌ Send command flow failed - this reproduces the bug!");
				// This test should fail initially, proving we found the bug
				panic!(
					"This test reproduces the send command bug - load_wallet returns dummy data!"
				);
			},
		}
	}

	#[test]
	fn test_keypair_data_integrity() {
		// Generate multiple keypairs and verify they maintain data integrity
		for i in 0..5 {
			let mut entropy = [i as u8; 32];
			let dilithium_keypair = Keypair::generate(SensitiveBytes32::from(&mut entropy));
			let quantum_keypair = QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);

			// Print actual key sizes for debugging (first iteration only)
			if i == 0 {
				println!("Actual public key size: {}", quantum_keypair.public_key.len());
				println!("Actual private key size: {}", quantum_keypair.private_key.len());
			}

			// Verify key sizes are consistent and reasonable
			assert!(
				quantum_keypair.public_key.len() > 1000,
				"Public key should be reasonably large (actual: {})",
				quantum_keypair.public_key.len()
			);
			assert!(
				quantum_keypair.private_key.len() > 2000,
				"Private key should be reasonably large (actual: {})",
				quantum_keypair.private_key.len()
			);

			// Verify keys are not all zeros
			assert!(
				quantum_keypair.public_key.iter().any(|&b| b != 0),
				"Public key should not be all zeros"
			);
			assert!(
				quantum_keypair.private_key.iter().any(|&b| b != 0),
				"Private key should not be all zeros"
			);
		}
	}
}