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