quantus_cli/wallet/
mod.rs

1/// Wallet management module
2///
3/// This module provides functionality for:
4/// - Creating quantum-safe wallets using Dilithium keys
5/// - Importing/exporting wallets with mnemonic phrases
6/// - Encrypting/decrypting wallet data
7/// - Managing multiple wallets
8pub mod keystore;
9pub mod password;
10
11use crate::error::{Result, WalletError};
12pub use keystore::{Keystore, QuantumKeyPair, WalletData};
13use qp_rusty_crystals_hdwallet::{generate_mnemonic, HDLattice};
14use rand::{rng, RngCore};
15use serde::{Deserialize, Serialize};
16use sp_runtime::traits::IdentifyAccount;
17
18/// Default derivation path for Quantus wallets: m/44'/189189'/0'/0/0
19pub const DEFAULT_DERIVATION_PATH: &str = "m/44'/189189'/0'/0/0";
20
21/// Wallet information structure
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct WalletInfo {
24	pub name: String,
25	pub address: String,
26	pub created_at: chrono::DateTime<chrono::Utc>,
27	pub key_type: String,
28	pub derivation_path: String,
29}
30
31/// Main wallet manager
32pub struct WalletManager {
33	wallets_dir: std::path::PathBuf,
34}
35
36impl WalletManager {
37	/// Create a new wallet manager
38	pub fn new() -> Result<Self> {
39		let wallets_dir = dirs::home_dir()
40			.ok_or(WalletError::KeyGeneration)?
41			.join(".quantus")
42			.join("wallets");
43
44		// Create directory if it doesn't exist
45		std::fs::create_dir_all(&wallets_dir)?;
46
47		Ok(Self { wallets_dir })
48	}
49
50	/// Create a new wallet
51	pub async fn create_wallet(&self, name: &str, password: Option<&str>) -> Result<WalletInfo> {
52		self.create_wallet_with_derivation_path(name, password, DEFAULT_DERIVATION_PATH)
53			.await
54	}
55
56	/// Create a new wallet with custom derivation path
57	pub async fn create_wallet_with_derivation_path(
58		&self,
59		name: &str,
60		password: Option<&str>,
61		derivation_path: &str,
62	) -> Result<WalletInfo> {
63		// Check if wallet already exists
64		let keystore = Keystore::new(&self.wallets_dir);
65		if keystore.load_wallet(name)?.is_some() {
66			return Err(WalletError::AlreadyExists.into());
67		}
68
69		// Generate a new Dilithium keypair using derivation path
70		let mut seed = [0u8; 32];
71		rng().fill_bytes(&mut seed);
72		let mnemonic = generate_mnemonic(24, seed).map_err(|_| WalletError::KeyGeneration)?;
73		let lattice =
74			HDLattice::from_mnemonic(&mnemonic, None).expect("Failed to generate lattice");
75		let dilithium_keypair = lattice
76			.generate_derived_keys(derivation_path)
77			.map_err(|_| WalletError::KeyGeneration)?;
78		let quantum_keypair = QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
79
80		// Create wallet data
81		let mut metadata = std::collections::HashMap::new();
82		metadata.insert("version".to_string(), "1.0.0".to_string());
83		metadata.insert("algorithm".to_string(), "ML-DSA-87".to_string());
84		metadata.insert("derivation_path".to_string(), derivation_path.to_string());
85
86		// Generate address from public key (simplified version)
87		let address = quantum_keypair.to_account_id_ss58check();
88
89		let wallet_data = WalletData {
90			name: name.to_string(),
91			keypair: quantum_keypair,
92			mnemonic: Some(mnemonic.clone()),
93			derivation_path: derivation_path.to_string(),
94			metadata,
95		};
96
97		// Encrypt and save the wallet
98		let password = password.unwrap_or(""); // Use empty password if none provided
99		let encrypted_wallet = keystore.encrypt_wallet_data(&wallet_data, password)?;
100		keystore.save_wallet(&encrypted_wallet)?;
101
102		Ok(WalletInfo {
103			name: name.to_string(),
104			address,
105			created_at: encrypted_wallet.created_at,
106			key_type: "Dilithium ML-DSA-87".to_string(),
107			derivation_path: derivation_path.to_string(),
108		})
109	}
110
111	/// Create a new developer wallet
112	pub async fn create_developer_wallet(&self, name: &str) -> Result<WalletInfo> {
113		// Check if wallet already exists
114		let keystore = Keystore::new(&self.wallets_dir);
115
116		// Generate the appropriate test keypair
117		let resonance_pair = match name {
118			"crystal_alice" => qp_dilithium_crypto::crystal_alice(),
119			"crystal_bob" => qp_dilithium_crypto::dilithium_bob(),
120			"crystal_charlie" => qp_dilithium_crypto::crystal_charlie(),
121			_ => return Err(WalletError::KeyGeneration.into()),
122		};
123
124		let quantum_keypair = QuantumKeyPair::from_resonance_pair(&resonance_pair);
125
126		// Format addresses with SS58 version 189 (Quantus format)
127		use sp_core::crypto::Ss58Codec;
128		let resonance_addr = resonance_pair
129			.public()
130			.into_account()
131			.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
132		println!("🔑 Resonance pair: {:?}", resonance_addr);
133		println!("🔑 Quantum keypair: {:?}", quantum_keypair.to_account_id_ss58check());
134
135		// Create wallet data
136		let mut metadata = std::collections::HashMap::new();
137		metadata.insert("version".to_string(), "1.0.0".to_string());
138		metadata.insert("algorithm".to_string(), "ML-DSA-87".to_string());
139		metadata.insert("test_wallet".to_string(), "true".to_string());
140
141		// Generate address from public key
142		let address = quantum_keypair.to_account_id_ss58check();
143
144		let wallet_data = WalletData {
145			name: name.to_string(),
146			keypair: quantum_keypair,
147			mnemonic: None,                    // Test wallets don't have mnemonics
148			derivation_path: "m/".to_string(), // Developer wallets use root path
149			metadata,
150		};
151
152		// Encrypt and save the wallet with empty password for test wallets
153		let encrypted_wallet = keystore.encrypt_wallet_data(&wallet_data, "")?;
154		keystore.save_wallet(&encrypted_wallet)?;
155
156		Ok(WalletInfo {
157			name: name.to_string(),
158			address,
159			created_at: encrypted_wallet.created_at,
160			key_type: "Dilithium ML-DSA-87".to_string(),
161			derivation_path: "m/".to_string(),
162		})
163	}
164
165	/// Export a wallet's mnemonic phrase
166	pub fn export_mnemonic(&self, name: &str, password: Option<&str>) -> Result<String> {
167		let final_password = password::get_wallet_password(name, password.map(String::from), None)?;
168
169		let wallet_data = self.load_wallet(name, &final_password)?;
170
171		wallet_data.mnemonic.ok_or_else(|| WalletError::MnemonicNotAvailable.into())
172	}
173
174	/// List all wallets
175	pub fn list_wallets(&self) -> Result<Vec<WalletInfo>> {
176		let keystore = Keystore::new(&self.wallets_dir);
177		let wallet_names = keystore.list_wallets()?;
178
179		let mut wallets = Vec::new();
180		for name in wallet_names {
181			if let Some(encrypted_wallet) = keystore.load_wallet(&name)? {
182				// Create wallet info using stored public address
183				let wallet_info = WalletInfo {
184					name: encrypted_wallet.name,
185					address: encrypted_wallet.address, // Address is stored unencrypted
186					created_at: encrypted_wallet.created_at,
187					key_type: "Dilithium ML-DSA-87".to_string(),
188					derivation_path: "[Encrypted]".to_string(), // Derivation path is encrypted
189				};
190				wallets.push(wallet_info);
191			}
192		}
193
194		// Sort by creation date (newest first)
195		wallets.sort_by(|a, b| b.created_at.cmp(&a.created_at));
196		Ok(wallets)
197	}
198
199	/// Import wallet from mnemonic phrase
200	pub async fn import_wallet(
201		&self,
202		name: &str,
203		mnemonic: &str,
204		password: Option<&str>,
205	) -> Result<WalletInfo> {
206		self.import_wallet_with_derivation_path(name, mnemonic, password, DEFAULT_DERIVATION_PATH)
207			.await
208	}
209
210	/// Create wallet from mnemonic without derivation (master seed)
211	pub async fn create_wallet_no_derivation(
212		&self,
213		name: &str,
214		password: Option<&str>,
215	) -> Result<WalletInfo> {
216		// Check if wallet already exists
217		let keystore = Keystore::new(&self.wallets_dir);
218		if keystore.load_wallet(name)?.is_some() {
219			return Err(WalletError::AlreadyExists.into());
220		}
221
222		// Generate new mnemonic and use master seed directly
223		let mut seed = [0u8; 32];
224		rng().fill_bytes(&mut seed);
225		let mnemonic = generate_mnemonic(24, seed).map_err(|_| WalletError::KeyGeneration)?;
226		let lattice =
227			HDLattice::from_mnemonic(&mnemonic, None).map_err(|_| WalletError::KeyGeneration)?;
228		let dilithium_keypair = lattice.generate_keys();
229		let quantum_keypair = QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
230
231		// Create wallet data
232		let mut metadata = std::collections::HashMap::new();
233		metadata.insert("version".to_string(), "1.0.0".to_string());
234		metadata.insert("algorithm".to_string(), "ML-DSA-87".to_string());
235		metadata.insert("no_derivation".to_string(), "true".to_string());
236
237		// Generate address from public key
238		let address = quantum_keypair.to_account_id_ss58check();
239
240		let wallet_data = WalletData {
241			name: name.to_string(),
242			keypair: quantum_keypair,
243			mnemonic: Some(mnemonic),
244			derivation_path: "master".to_string(),
245			metadata,
246		};
247
248		// Encrypt and save the wallet
249		let password = password.unwrap_or(""); // Use empty password if none provided
250		let encrypted_wallet = keystore.encrypt_wallet_data(&wallet_data, password)?;
251		keystore.save_wallet(&encrypted_wallet)?;
252
253		Ok(WalletInfo {
254			name: name.to_string(),
255			address,
256			created_at: chrono::Utc::now(),
257			key_type: "Dilithium ML-DSA-87".to_string(),
258			derivation_path: "master".to_string(),
259		})
260	}
261
262	/// Import wallet from mnemonic without derivation (master seed)
263	pub async fn import_wallet_no_derivation(
264		&self,
265		name: &str,
266		mnemonic: &str,
267		password: Option<&str>,
268	) -> Result<WalletInfo> {
269		// Check if wallet already exists
270		let keystore = Keystore::new(&self.wallets_dir);
271		if keystore.load_wallet(name)?.is_some() {
272			return Err(WalletError::AlreadyExists.into());
273		}
274
275		// Use mnemonic to generate master seed directly
276		let lattice =
277			HDLattice::from_mnemonic(mnemonic, None).map_err(|_| WalletError::InvalidMnemonic)?;
278		let dilithium_keypair = lattice.generate_keys();
279		let quantum_keypair = QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
280
281		// Create wallet data
282		let mut metadata = std::collections::HashMap::new();
283		metadata.insert("version".to_string(), "1.0.0".to_string());
284		metadata.insert("algorithm".to_string(), "ML-DSA-87".to_string());
285		metadata.insert("imported".to_string(), "true".to_string());
286		metadata.insert("no_derivation".to_string(), "true".to_string());
287
288		// Generate address from public key
289		let address = quantum_keypair.to_account_id_ss58check();
290
291		let wallet_data = WalletData {
292			name: name.to_string(),
293			keypair: quantum_keypair,
294			mnemonic: Some(mnemonic.to_string()),
295			derivation_path: "master".to_string(),
296			metadata,
297		};
298
299		// Encrypt and save the wallet
300		let password = password.unwrap_or(""); // Use empty password if none provided
301		let encrypted_wallet = keystore.encrypt_wallet_data(&wallet_data, password)?;
302		keystore.save_wallet(&encrypted_wallet)?;
303
304		Ok(WalletInfo {
305			name: name.to_string(),
306			address,
307			created_at: chrono::Utc::now(),
308			key_type: "Dilithium ML-DSA-87".to_string(),
309			derivation_path: "master".to_string(),
310		})
311	}
312
313	/// Import wallet from mnemonic phrase with custom derivation path
314	pub async fn import_wallet_with_derivation_path(
315		&self,
316		name: &str,
317		mnemonic: &str,
318		password: Option<&str>,
319		derivation_path: &str,
320	) -> Result<WalletInfo> {
321		// Check if wallet already exists
322		let keystore = Keystore::new(&self.wallets_dir);
323		if keystore.load_wallet(name)?.is_some() {
324			return Err(WalletError::AlreadyExists.into());
325		}
326
327		// Validate and import from mnemonic using derivation path
328		let lattice =
329			HDLattice::from_mnemonic(mnemonic, None).map_err(|_| WalletError::InvalidMnemonic)?;
330		let dilithium_keypair = lattice
331			.generate_derived_keys(derivation_path)
332			.map_err(|_| WalletError::KeyGeneration)?;
333		let quantum_keypair = QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
334
335		// Create wallet data
336		let mut metadata = std::collections::HashMap::new();
337		metadata.insert("version".to_string(), "1.0.0".to_string());
338		metadata.insert("algorithm".to_string(), "ML-DSA-87".to_string());
339		metadata.insert("imported".to_string(), "true".to_string());
340		metadata.insert("derivation_path".to_string(), derivation_path.to_string());
341
342		// Generate address from public key
343		let address = quantum_keypair.to_account_id_ss58check();
344
345		let wallet_data = WalletData {
346			name: name.to_string(),
347			keypair: quantum_keypair,
348			mnemonic: Some(mnemonic.to_string()),
349			derivation_path: derivation_path.to_string(),
350			metadata,
351		};
352
353		// Encrypt and save the wallet
354		let password = password.unwrap_or(""); // Use empty password if none provided
355		let encrypted_wallet = keystore.encrypt_wallet_data(&wallet_data, password)?;
356		keystore.save_wallet(&encrypted_wallet)?;
357
358		Ok(WalletInfo {
359			name: name.to_string(),
360			address,
361			created_at: encrypted_wallet.created_at,
362			key_type: "Dilithium ML-DSA-87".to_string(),
363			derivation_path: derivation_path.to_string(),
364		})
365	}
366
367	/// Create wallet from 32-byte seed
368	pub async fn create_wallet_from_seed(
369		&self,
370		name: &str,
371		seed_hex: &str,
372		password: Option<&str>,
373	) -> Result<WalletInfo> {
374		// Check if wallet already exists
375		let keystore = Keystore::new(&self.wallets_dir);
376		if keystore.load_wallet(name)?.is_some() {
377			return Err(WalletError::AlreadyExists.into());
378		}
379
380		// Validate seed hex format (should be 64 hex characters for 32 bytes)
381		if seed_hex.len() != 64 {
382			return Err(WalletError::InvalidMnemonic.into()); // Reusing error type
383		}
384
385		// Convert hex to bytes
386		let seed_bytes = hex::decode(seed_hex).map_err(|_| WalletError::InvalidMnemonic)?;
387		if seed_bytes.len() != 32 {
388			return Err(WalletError::InvalidMnemonic.into());
389		}
390
391		// Create DilithiumPair from seed
392		let seed_array: [u8; 32] =
393			seed_bytes.try_into().map_err(|_| WalletError::InvalidMnemonic)?;
394
395		println!("Debug: seed_array length: {}", seed_array.len());
396		println!("Debug: seed_hex: {}", seed_hex);
397		println!("Debug: calling DilithiumPair::from_seed");
398
399		let dilithium_pair = qp_dilithium_crypto::types::DilithiumPair::from_seed(&seed_array)
400			.map_err(|e| {
401				println!("Debug: DilithiumPair::from_seed failed with error: {:?}", e);
402				WalletError::InvalidMnemonic
403			})?;
404
405		println!("Debug: DilithiumPair created successfully");
406
407		// Convert to QuantumKeyPair
408		let quantum_keypair = QuantumKeyPair::from_resonance_pair(&dilithium_pair);
409
410		// Create wallet data
411		let mut metadata = std::collections::HashMap::new();
412		metadata.insert("version".to_string(), "1.0.0".to_string());
413		metadata.insert("algorithm".to_string(), "ML-DSA-87".to_string());
414		metadata.insert("from_seed".to_string(), "true".to_string());
415
416		// Generate address from public key
417		let address = quantum_keypair.to_account_id_ss58check();
418
419		let wallet_data = WalletData {
420			name: name.to_string(),
421			keypair: quantum_keypair,
422			mnemonic: None,                    // No mnemonic for seed-based wallets
423			derivation_path: "m/".to_string(), // Seed-based wallets use root path
424			metadata,
425		};
426
427		// Encrypt and save the wallet
428		let password = password.unwrap_or(""); // Use empty password if none provided
429		let encrypted_wallet = keystore.encrypt_wallet_data(&wallet_data, password)?;
430		keystore.save_wallet(&encrypted_wallet)?;
431
432		Ok(WalletInfo {
433			name: name.to_string(),
434			address,
435			created_at: encrypted_wallet.created_at,
436			key_type: "Dilithium ML-DSA-87".to_string(),
437			derivation_path: "m/".to_string(),
438		})
439	}
440
441	/// Get wallet by name with password for decryption
442	pub fn get_wallet(&self, name: &str, password: Option<&str>) -> Result<Option<WalletInfo>> {
443		let keystore = Keystore::new(&self.wallets_dir);
444
445		if let Some(encrypted_wallet) = keystore.load_wallet(name)? {
446			if let Some(pwd) = password {
447				// Decrypt and show full details
448				match keystore.decrypt_wallet_data(&encrypted_wallet, pwd) {
449					Ok(wallet_data) => {
450						let address = wallet_data.keypair.to_account_id_ss58check();
451						Ok(Some(WalletInfo {
452							name: wallet_data.name,
453							address,
454							created_at: encrypted_wallet.created_at,
455							key_type: "Dilithium ML-DSA-87".to_string(),
456							derivation_path: wallet_data.derivation_path,
457						}))
458					},
459					Err(_) => {
460						// Wrong password, return basic info
461						Ok(Some(WalletInfo {
462							name: encrypted_wallet.name,
463							address: "[Wrong password]".to_string(),
464							created_at: encrypted_wallet.created_at,
465							key_type: "Dilithium ML-DSA-87".to_string(),
466							derivation_path: "[Wrong password]".to_string(),
467						}))
468					},
469				}
470			} else {
471				// No password provided, return basic info with public address
472				Ok(Some(WalletInfo {
473					name: encrypted_wallet.name,
474					address: encrypted_wallet.address, // Address is public
475					created_at: encrypted_wallet.created_at,
476					key_type: "Dilithium ML-DSA-87".to_string(),
477					derivation_path: "[Encrypted]".to_string(), // Derivation path is encrypted
478				}))
479			}
480		} else {
481			Ok(None)
482		}
483	}
484
485	/// Load a wallet from disk and decrypt it with the provided password
486	pub fn load_wallet(&self, name: &str, password: &str) -> Result<WalletData> {
487		let keystore = Keystore::new(&self.wallets_dir);
488
489		// Load the encrypted wallet
490		let encrypted_wallet = keystore.load_wallet(name)?.ok_or(WalletError::NotFound)?;
491
492		// Decrypt the wallet data using the provided password
493		let wallet_data = keystore.decrypt_wallet_data(&encrypted_wallet, password)?;
494
495		Ok(wallet_data)
496	}
497
498	/// Delete a wallet
499	pub fn delete_wallet(&self, name: &str) -> Result<bool> {
500		let keystore = Keystore::new(&self.wallets_dir);
501		keystore.delete_wallet(name)
502	}
503
504	/// Find wallet by name and return its address
505	pub fn find_wallet_address(&self, name: &str) -> Result<Option<String>> {
506		let keystore = Keystore::new(&self.wallets_dir);
507
508		if let Some(encrypted_wallet) = keystore.load_wallet(name)? {
509			// Return the stored address (it's stored unencrypted)
510			Ok(Some(encrypted_wallet.address))
511		} else {
512			Ok(None)
513		}
514	}
515}
516
517pub fn load_keypair_from_wallet(
518	wallet_name: &str,
519	password: Option<String>,
520	password_file: Option<String>,
521) -> Result<QuantumKeyPair> {
522	let wallet_manager = WalletManager::new()?;
523	let wallet_password = password::get_wallet_password(wallet_name, password, password_file)?;
524	let wallet_data = wallet_manager.load_wallet(wallet_name, &wallet_password)?;
525	let keypair = wallet_data.keypair;
526	Ok(keypair)
527}
528
529#[cfg(test)]
530mod tests {
531	use super::*;
532	use std::fs;
533	use tempfile::TempDir;
534
535	async fn create_test_wallet_manager() -> (WalletManager, TempDir) {
536		let temp_dir = TempDir::new().expect("Failed to create temp directory");
537		let wallets_dir = temp_dir.path().join("wallets");
538		fs::create_dir_all(&wallets_dir).expect("Failed to create wallets directory");
539
540		let wallet_manager = WalletManager { wallets_dir };
541
542		(wallet_manager, temp_dir)
543	}
544
545	#[tokio::test]
546	async fn test_wallet_creation() {
547		let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
548
549		// Test wallet creation
550		let wallet_info = wallet_manager
551			.create_wallet("test-wallet", Some("test-password"))
552			.await
553			.expect("Failed to create wallet");
554
555		// Verify wallet info
556		assert_eq!(wallet_info.name, "test-wallet");
557		assert!(wallet_info.address.starts_with("qz")); // SS58 addresses start with 5
558		assert_eq!(wallet_info.key_type, "Dilithium ML-DSA-87");
559		assert!(wallet_info.created_at <= chrono::Utc::now());
560	}
561
562	#[tokio::test]
563	async fn test_wallet_already_exists() {
564		let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
565
566		// Create first wallet
567		wallet_manager
568			.create_wallet("duplicate-wallet", None)
569			.await
570			.expect("Failed to create first wallet");
571
572		// Try to create wallet with same name
573		let result = wallet_manager.create_wallet("duplicate-wallet", None).await;
574
575		assert!(result.is_err());
576		match result.unwrap_err() {
577			crate::error::QuantusError::Wallet(WalletError::AlreadyExists) => {},
578			_ => panic!("Expected AlreadyExists error"),
579		}
580	}
581
582	#[tokio::test]
583	async fn test_wallet_file_creation() {
584		let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
585
586		// Create wallet
587		let _ = wallet_manager
588			.create_wallet("file-test-wallet", Some("password123"))
589			.await
590			.expect("Failed to create wallet");
591
592		// Check if wallet file exists
593		let wallet_file = wallet_manager.wallets_dir.join("file-test-wallet.json");
594		assert!(wallet_file.exists(), "Wallet file should exist");
595
596		// Verify file is not empty
597		let file_size = fs::metadata(&wallet_file).expect("Failed to get file metadata").len();
598		assert!(file_size > 0, "Wallet file should not be empty");
599	}
600
601	#[tokio::test]
602	async fn test_keystore_encryption_decryption() {
603		let temp_dir = TempDir::new().expect("Failed to create temp directory");
604		let keystore = keystore::Keystore::new(temp_dir.path());
605
606		// Create test wallet data
607		let entropy = [1u8; 32]; // Use fixed entropy for deterministic tests
608		let dilithium_keypair = qp_rusty_crystals_dilithium::ml_dsa_87::Keypair::generate(&entropy);
609		let quantum_keypair = keystore::QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
610
611		let mut metadata = std::collections::HashMap::new();
612		metadata.insert("test_key".to_string(), "test_value".to_string());
613
614		let original_wallet_data = keystore::WalletData {
615			name: "test-wallet".to_string(),
616			keypair: quantum_keypair,
617			mnemonic: Some(
618				"test mnemonic phrase with twenty four words here for testing purposes only"
619					.to_string(),
620			),
621			derivation_path: DEFAULT_DERIVATION_PATH.to_string(),
622			metadata,
623		};
624
625		// Test encryption
626		let encrypted_wallet = keystore
627			.encrypt_wallet_data(&original_wallet_data, "test-password")
628			.expect("Failed to encrypt wallet data");
629
630		assert_eq!(encrypted_wallet.name, "test-wallet");
631		assert!(!encrypted_wallet.encrypted_data.is_empty());
632		assert!(!encrypted_wallet.argon2_salt.is_empty());
633		assert!(!encrypted_wallet.aes_nonce.is_empty());
634
635		// Test decryption
636		let decrypted_wallet_data = keystore
637			.decrypt_wallet_data(&encrypted_wallet, "test-password")
638			.expect("Failed to decrypt wallet data");
639
640		// Verify decrypted data matches original
641		assert_eq!(decrypted_wallet_data.name, original_wallet_data.name);
642		assert_eq!(decrypted_wallet_data.mnemonic, original_wallet_data.mnemonic);
643		assert_eq!(decrypted_wallet_data.metadata, original_wallet_data.metadata);
644		assert_eq!(
645			decrypted_wallet_data.keypair.public_key,
646			original_wallet_data.keypair.public_key
647		);
648		assert_eq!(
649			decrypted_wallet_data.keypair.private_key,
650			original_wallet_data.keypair.private_key
651		);
652	}
653
654	#[tokio::test]
655	async fn test_quantum_keypair_address_generation() {
656		// Generate keypair
657		let entropy = [2u8; 32]; // Use different entropy for variety
658		let dilithium_keypair = qp_rusty_crystals_dilithium::ml_dsa_87::Keypair::generate(&entropy);
659		let quantum_keypair = keystore::QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
660
661		// Test address generation
662		let account_id = quantum_keypair.to_account_id_32();
663		let ss58_address = quantum_keypair.to_account_id_ss58check();
664
665		// Verify SS58 address format
666		assert!(ss58_address.starts_with("qz"), "SS58 address should start with 5");
667		assert!(ss58_address.len() >= 47, "SS58 address should be at least 47 characters");
668
669		// Test round-trip conversion
670		let converted_account_bytes = keystore::QuantumKeyPair::ss58_to_account_id(&ss58_address);
671		let account_bytes: &[u8] = account_id.as_ref();
672		assert_eq!(converted_account_bytes, account_bytes);
673	}
674
675	#[tokio::test]
676	async fn test_keystore_save_and_load() {
677		let temp_dir = TempDir::new().expect("Failed to create temp directory");
678		let keystore = keystore::Keystore::new(temp_dir.path());
679
680		// Create and encrypt wallet data
681		let entropy = [3u8; 32]; // Use different entropy for each test
682		let dilithium_keypair = qp_rusty_crystals_dilithium::ml_dsa_87::Keypair::generate(&entropy);
683		let quantum_keypair = keystore::QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
684
685		let wallet_data = keystore::WalletData {
686			name: "save-load-test".to_string(),
687			keypair: quantum_keypair,
688			mnemonic: Some("save load test mnemonic phrase".to_string()),
689			derivation_path: DEFAULT_DERIVATION_PATH.to_string(),
690			metadata: std::collections::HashMap::new(),
691		};
692
693		let encrypted_wallet = keystore
694			.encrypt_wallet_data(&wallet_data, "save-load-password")
695			.expect("Failed to encrypt wallet");
696
697		// Save wallet
698		keystore.save_wallet(&encrypted_wallet).expect("Failed to save wallet");
699
700		// Load wallet
701		let loaded_wallet = keystore
702			.load_wallet("save-load-test")
703			.expect("Failed to load wallet")
704			.expect("Wallet should exist");
705
706		// Verify loaded wallet matches saved wallet
707		assert_eq!(loaded_wallet.name, encrypted_wallet.name);
708		assert_eq!(loaded_wallet.encrypted_data, encrypted_wallet.encrypted_data);
709		assert_eq!(loaded_wallet.argon2_salt, encrypted_wallet.argon2_salt);
710		assert_eq!(loaded_wallet.aes_nonce, encrypted_wallet.aes_nonce);
711
712		// Test loading non-existent wallet
713		let non_existent = keystore
714			.load_wallet("non-existent-wallet")
715			.expect("Load should succeed but return None");
716		assert!(non_existent.is_none());
717	}
718
719	#[tokio::test]
720	async fn test_mnemonic_generation_and_key_derivation() {
721		let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
722
723		// Create multiple wallets to test mnemonic uniqueness
724		let wallet1 = wallet_manager
725			.create_wallet("mnemonic-test-1", None)
726			.await
727			.expect("Failed to create wallet 1");
728
729		let wallet2 = wallet_manager
730			.create_wallet("mnemonic-test-2", None)
731			.await
732			.expect("Failed to create wallet 2");
733
734		// Addresses should be different (extremely unlikely to be the same)
735		assert_ne!(wallet1.address, wallet2.address);
736
737		// Both should be valid SS58 addresses
738		assert!(wallet1.address.starts_with("qz"));
739		assert!(wallet2.address.starts_with("qz"));
740	}
741
742	#[tokio::test]
743	async fn test_wallet_import() {
744		let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
745
746		// Test mnemonic phrase (24 words)
747		let test_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";
748
749		// Import wallet
750		let imported_wallet = wallet_manager
751			.import_wallet("imported-test-wallet", test_mnemonic, Some("import-password"))
752			.await
753			.expect("Failed to import wallet");
754
755		// Verify wallet info
756		assert_eq!(imported_wallet.name, "imported-test-wallet");
757		assert!(imported_wallet.address.starts_with("qz"));
758		assert_eq!(imported_wallet.key_type, "Dilithium ML-DSA-87");
759
760		// Import the same mnemonic again should create the same address
761		let imported_wallet2 = wallet_manager
762			.import_wallet("imported-test-wallet-2", test_mnemonic, None)
763			.await
764			.expect("Failed to import wallet again");
765
766		assert_eq!(imported_wallet.address, imported_wallet2.address);
767	}
768
769	#[tokio::test]
770	async fn test_known_values() {
771		sp_core::crypto::set_default_ss58_version(sp_core::crypto::Ss58AddressFormat::custom(189));
772
773		let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
774		let test_mnemonic = "orchard answer curve patient visual flower maze noise retreat penalty cage small earth domain scan pitch bottom crunch theme club client swap slice raven";
775		let expected_address = "qznMJss7Ls1SWBhvvL2CSHVbgTxEfnL9GgpvMTq5CWMEwfCoe"; // default derivation path index 0
776		let expected_address_no_derive = "qznBvupPsA9T8VJDuTDokKPiNUe88zMMUtHGA1AsGc8fXKSSA";
777
778		let imported_wallet = wallet_manager
779			.import_wallet("imported-test-wallet", test_mnemonic, Some("import-password"))
780			.await
781			.expect("Failed to import wallet");
782
783		let imported_wallet_no_derive = wallet_manager
784			.import_wallet_no_derivation(
785				"imported-test-wallet_no_derive",
786				test_mnemonic,
787				Some("import-password"),
788			)
789			.await
790			.expect("Failed to import wallet");
791
792		assert_eq!(imported_wallet.address, expected_address, "address at index 0 is wrong");
793		assert_eq!(
794			imported_wallet_no_derive.address, expected_address_no_derive,
795			"no-derivation address is wrong"
796		);
797	}
798
799	#[tokio::test]
800	async fn test_wallet_import_invalid_mnemonic() {
801		let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
802
803		// Test with invalid mnemonic
804		let invalid_mnemonic = "invalid mnemonic phrase that should not work";
805
806		let result = wallet_manager.import_wallet("invalid-wallet", invalid_mnemonic, None).await;
807
808		assert!(result.is_err());
809		match result.unwrap_err() {
810			crate::error::QuantusError::Wallet(WalletError::InvalidMnemonic) => {},
811			_ => panic!("Expected InvalidMnemonic error"),
812		}
813	}
814
815	#[tokio::test]
816	async fn test_wallet_import_already_exists() {
817		let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
818
819		let test_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";
820
821		// Import first wallet
822		wallet_manager
823			.import_wallet("duplicate-import-wallet", test_mnemonic, None)
824			.await
825			.expect("Failed to import first wallet");
826
827		// Try to import with same name
828		let result = wallet_manager
829			.import_wallet("duplicate-import-wallet", test_mnemonic, None)
830			.await;
831
832		assert!(result.is_err());
833		match result.unwrap_err() {
834			crate::error::QuantusError::Wallet(WalletError::AlreadyExists) => {},
835			_ => panic!("Expected AlreadyExists error"),
836		}
837	}
838
839	#[tokio::test]
840	async fn test_list_wallets() {
841		let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
842
843		// Initially should be empty
844		let wallets = wallet_manager.list_wallets().expect("Failed to list wallets");
845		assert_eq!(wallets.len(), 0);
846
847		// Create some wallets
848		wallet_manager
849			.create_wallet("wallet-1", Some("password1"))
850			.await
851			.expect("Failed to create wallet 1");
852
853		wallet_manager
854			.create_wallet("wallet-2", None)
855			.await
856			.expect("Failed to create wallet 2");
857
858		let test_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";
859		wallet_manager
860			.import_wallet("imported-wallet", test_mnemonic, Some("password3"))
861			.await
862			.expect("Failed to import wallet");
863
864		// List wallets
865		let wallets = wallet_manager.list_wallets().expect("Failed to list wallets");
866
867		assert_eq!(wallets.len(), 3);
868
869		// Check that all wallet names are present
870		let wallet_names: Vec<&String> = wallets.iter().map(|w| &w.name).collect();
871		assert!(wallet_names.contains(&&"wallet-1".to_string()));
872		assert!(wallet_names.contains(&&"wallet-2".to_string()));
873		assert!(wallet_names.contains(&&"imported-wallet".to_string()));
874
875		// Check that addresses are real addresses (now stored unencrypted)
876		for wallet in &wallets {
877			assert!(wallet.address.starts_with("qz")); // Real SS58 addresses start with 5
878			assert_eq!(wallet.key_type, "Dilithium ML-DSA-87");
879		}
880
881		// Check sorting (newest first)
882		assert!(wallets[0].created_at >= wallets[1].created_at);
883		assert!(wallets[1].created_at >= wallets[2].created_at);
884	}
885
886	#[tokio::test]
887	async fn test_get_wallet() {
888		let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
889
890		// Create a wallet
891		let created_wallet = wallet_manager
892			.create_wallet("test-get-wallet", Some("test-password"))
893			.await
894			.expect("Failed to create wallet");
895
896		// Test getting wallet without password
897		let wallet_info = wallet_manager
898			.get_wallet("test-get-wallet", None)
899			.expect("Failed to get wallet")
900			.expect("Wallet should exist");
901
902		assert_eq!(wallet_info.name, "test-get-wallet");
903		assert_eq!(wallet_info.address, created_wallet.address); // Now returns real address
904
905		// Test getting wallet with wrong password
906		// Now with real quantum-safe encryption, wrong password should be detected
907		let wallet_info = wallet_manager
908			.get_wallet("test-get-wallet", Some("wrong-password"))
909			.expect("Failed to get wallet")
910			.expect("Wallet should exist");
911
912		assert_eq!(wallet_info.name, "test-get-wallet");
913		// With real encryption, wrong password returns placeholder text
914		assert_eq!(wallet_info.address, "[Wrong password]");
915
916		// Test getting wallet with correct password
917		let wallet_info = wallet_manager
918			.get_wallet("test-get-wallet", Some("test-password"))
919			.expect("Failed to get wallet")
920			.expect("Wallet should exist");
921
922		assert_eq!(wallet_info.name, "test-get-wallet");
923		assert_eq!(wallet_info.address, created_wallet.address);
924		assert!(wallet_info.address.starts_with("qz"));
925
926		// Test getting non-existent wallet
927		let result = wallet_manager
928			.get_wallet("non-existent-wallet", None)
929			.expect("Should not error on non-existent wallet");
930
931		assert!(result.is_none());
932	}
933}