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 = qp_rusty_crystals_dilithium::ml_dsa_87::Keypair::generate(&entropy);
604		let quantum_keypair = keystore::QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
605
606		let mut metadata = std::collections::HashMap::new();
607		metadata.insert("test_key".to_string(), "test_value".to_string());
608
609		let original_wallet_data = keystore::WalletData {
610			name: "test-wallet".to_string(),
611			keypair: quantum_keypair,
612			mnemonic: Some(
613				"test mnemonic phrase with twenty four words here for testing purposes only"
614					.to_string(),
615			),
616			derivation_path: DEFAULT_DERIVATION_PATH.to_string(),
617			metadata,
618		};
619
620		// Test encryption
621		let encrypted_wallet = keystore
622			.encrypt_wallet_data(&original_wallet_data, "test-password")
623			.expect("Failed to encrypt wallet data");
624
625		assert_eq!(encrypted_wallet.name, "test-wallet");
626		assert!(!encrypted_wallet.encrypted_data.is_empty());
627		assert!(!encrypted_wallet.argon2_salt.is_empty());
628		assert!(!encrypted_wallet.aes_nonce.is_empty());
629
630		// Test decryption
631		let decrypted_wallet_data = keystore
632			.decrypt_wallet_data(&encrypted_wallet, "test-password")
633			.expect("Failed to decrypt wallet data");
634
635		// Verify decrypted data matches original
636		assert_eq!(decrypted_wallet_data.name, original_wallet_data.name);
637		assert_eq!(decrypted_wallet_data.mnemonic, original_wallet_data.mnemonic);
638		assert_eq!(decrypted_wallet_data.metadata, original_wallet_data.metadata);
639		assert_eq!(
640			decrypted_wallet_data.keypair.public_key,
641			original_wallet_data.keypair.public_key
642		);
643		assert_eq!(
644			decrypted_wallet_data.keypair.private_key,
645			original_wallet_data.keypair.private_key
646		);
647	}
648
649	#[tokio::test]
650	async fn test_quantum_keypair_address_generation() {
651		// Generate keypair
652		let entropy = [2u8; 32]; // Use different entropy for variety
653		let dilithium_keypair = qp_rusty_crystals_dilithium::ml_dsa_87::Keypair::generate(&entropy);
654		let quantum_keypair = keystore::QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
655
656		// Test address generation
657		let account_id = quantum_keypair.to_account_id_32();
658		let ss58_address = quantum_keypair.to_account_id_ss58check();
659
660		// Verify SS58 address format
661		assert!(ss58_address.starts_with("qz"), "SS58 address should start with 5");
662		assert!(ss58_address.len() >= 47, "SS58 address should be at least 47 characters");
663
664		// Test round-trip conversion
665		let converted_account_bytes = keystore::QuantumKeyPair::ss58_to_account_id(&ss58_address);
666		let account_bytes: &[u8] = account_id.as_ref();
667		assert_eq!(converted_account_bytes, account_bytes);
668	}
669
670	#[tokio::test]
671	async fn test_keystore_save_and_load() {
672		let temp_dir = TempDir::new().expect("Failed to create temp directory");
673		let keystore = keystore::Keystore::new(temp_dir.path());
674
675		// Create and encrypt wallet data
676		let entropy = [3u8; 32]; // Use different entropy for each test
677		let dilithium_keypair = qp_rusty_crystals_dilithium::ml_dsa_87::Keypair::generate(&entropy);
678		let quantum_keypair = keystore::QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
679
680		let wallet_data = keystore::WalletData {
681			name: "save-load-test".to_string(),
682			keypair: quantum_keypair,
683			mnemonic: Some("save load test mnemonic phrase".to_string()),
684			derivation_path: DEFAULT_DERIVATION_PATH.to_string(),
685			metadata: std::collections::HashMap::new(),
686		};
687
688		let encrypted_wallet = keystore
689			.encrypt_wallet_data(&wallet_data, "save-load-password")
690			.expect("Failed to encrypt wallet");
691
692		// Save wallet
693		keystore.save_wallet(&encrypted_wallet).expect("Failed to save wallet");
694
695		// Load wallet
696		let loaded_wallet = keystore
697			.load_wallet("save-load-test")
698			.expect("Failed to load wallet")
699			.expect("Wallet should exist");
700
701		// Verify loaded wallet matches saved wallet
702		assert_eq!(loaded_wallet.name, encrypted_wallet.name);
703		assert_eq!(loaded_wallet.encrypted_data, encrypted_wallet.encrypted_data);
704		assert_eq!(loaded_wallet.argon2_salt, encrypted_wallet.argon2_salt);
705		assert_eq!(loaded_wallet.aes_nonce, encrypted_wallet.aes_nonce);
706
707		// Test loading non-existent wallet
708		let non_existent = keystore
709			.load_wallet("non-existent-wallet")
710			.expect("Load should succeed but return None");
711		assert!(non_existent.is_none());
712	}
713
714	#[tokio::test]
715	async fn test_mnemonic_generation_and_key_derivation() {
716		let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
717
718		// Create multiple wallets to test mnemonic uniqueness
719		let wallet1 = wallet_manager
720			.create_wallet("mnemonic-test-1", None)
721			.await
722			.expect("Failed to create wallet 1");
723
724		let wallet2 = wallet_manager
725			.create_wallet("mnemonic-test-2", None)
726			.await
727			.expect("Failed to create wallet 2");
728
729		// Addresses should be different (extremely unlikely to be the same)
730		assert_ne!(wallet1.address, wallet2.address);
731
732		// Both should be valid SS58 addresses
733		assert!(wallet1.address.starts_with("qz"));
734		assert!(wallet2.address.starts_with("qz"));
735	}
736
737	#[tokio::test]
738	async fn test_wallet_import() {
739		let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
740
741		// Test mnemonic phrase (24 words)
742		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";
743
744		// Import wallet
745		let imported_wallet = wallet_manager
746			.import_wallet("imported-test-wallet", test_mnemonic, Some("import-password"))
747			.await
748			.expect("Failed to import wallet");
749
750		// Verify wallet info
751		assert_eq!(imported_wallet.name, "imported-test-wallet");
752		assert!(imported_wallet.address.starts_with("qz"));
753		assert_eq!(imported_wallet.key_type, "Dilithium ML-DSA-87");
754
755		// Import the same mnemonic again should create the same address
756		let imported_wallet2 = wallet_manager
757			.import_wallet("imported-test-wallet-2", test_mnemonic, None)
758			.await
759			.expect("Failed to import wallet again");
760
761		assert_eq!(imported_wallet.address, imported_wallet2.address);
762	}
763
764	#[tokio::test]
765	async fn test_known_values() {
766		sp_core::crypto::set_default_ss58_version(sp_core::crypto::Ss58AddressFormat::custom(189));
767
768		let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
769		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";
770		let expected_address = "qznMJss7Ls1SWBhvvL2CSHVbgTxEfnL9GgpvMTq5CWMEwfCoe"; // default derivation path index 0
771		let expected_address_no_derive = "qznBvupPsA9T8VJDuTDokKPiNUe88zMMUtHGA1AsGc8fXKSSA";
772
773		let imported_wallet = wallet_manager
774			.import_wallet("imported-test-wallet", test_mnemonic, Some("import-password"))
775			.await
776			.expect("Failed to import wallet");
777
778		let imported_wallet_no_derive = wallet_manager
779			.import_wallet_no_derivation(
780				"imported-test-wallet_no_derive",
781				test_mnemonic,
782				Some("import-password"),
783			)
784			.await
785			.expect("Failed to import wallet");
786
787		assert_eq!(imported_wallet.address, expected_address, "address at index 0 is wrong");
788		assert_eq!(
789			imported_wallet_no_derive.address, expected_address_no_derive,
790			"no-derivation address is wrong"
791		);
792	}
793
794	#[tokio::test]
795	async fn test_wallet_import_invalid_mnemonic() {
796		let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
797
798		// Test with invalid mnemonic
799		let invalid_mnemonic = "invalid mnemonic phrase that should not work";
800
801		let result = wallet_manager.import_wallet("invalid-wallet", invalid_mnemonic, None).await;
802
803		assert!(result.is_err());
804		match result.unwrap_err() {
805			crate::error::QuantusError::Wallet(WalletError::InvalidMnemonic) => {},
806			_ => panic!("Expected InvalidMnemonic error"),
807		}
808	}
809
810	#[tokio::test]
811	async fn test_wallet_import_already_exists() {
812		let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
813
814		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";
815
816		// Import first wallet
817		wallet_manager
818			.import_wallet("duplicate-import-wallet", test_mnemonic, None)
819			.await
820			.expect("Failed to import first wallet");
821
822		// Try to import with same name
823		let result = wallet_manager
824			.import_wallet("duplicate-import-wallet", test_mnemonic, None)
825			.await;
826
827		assert!(result.is_err());
828		match result.unwrap_err() {
829			crate::error::QuantusError::Wallet(WalletError::AlreadyExists) => {},
830			_ => panic!("Expected AlreadyExists error"),
831		}
832	}
833
834	#[tokio::test]
835	async fn test_list_wallets() {
836		let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
837
838		// Initially should be empty
839		let wallets = wallet_manager.list_wallets().expect("Failed to list wallets");
840		assert_eq!(wallets.len(), 0);
841
842		// Create some wallets
843		wallet_manager
844			.create_wallet("wallet-1", Some("password1"))
845			.await
846			.expect("Failed to create wallet 1");
847
848		wallet_manager
849			.create_wallet("wallet-2", None)
850			.await
851			.expect("Failed to create wallet 2");
852
853		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";
854		wallet_manager
855			.import_wallet("imported-wallet", test_mnemonic, Some("password3"))
856			.await
857			.expect("Failed to import wallet");
858
859		// List wallets
860		let wallets = wallet_manager.list_wallets().expect("Failed to list wallets");
861
862		assert_eq!(wallets.len(), 3);
863
864		// Check that all wallet names are present
865		let wallet_names: Vec<&String> = wallets.iter().map(|w| &w.name).collect();
866		assert!(wallet_names.contains(&&"wallet-1".to_string()));
867		assert!(wallet_names.contains(&&"wallet-2".to_string()));
868		assert!(wallet_names.contains(&&"imported-wallet".to_string()));
869
870		// Check that addresses are real addresses (now stored unencrypted)
871		for wallet in &wallets {
872			assert!(wallet.address.starts_with("qz")); // Real SS58 addresses start with 5
873			assert_eq!(wallet.key_type, "Dilithium ML-DSA-87");
874		}
875
876		// Check sorting (newest first)
877		assert!(wallets[0].created_at >= wallets[1].created_at);
878		assert!(wallets[1].created_at >= wallets[2].created_at);
879	}
880
881	#[tokio::test]
882	async fn test_get_wallet() {
883		let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
884
885		// Create a wallet
886		let created_wallet = wallet_manager
887			.create_wallet("test-get-wallet", Some("test-password"))
888			.await
889			.expect("Failed to create wallet");
890
891		// Test getting wallet without password
892		let wallet_info = wallet_manager
893			.get_wallet("test-get-wallet", None)
894			.expect("Failed to get wallet")
895			.expect("Wallet should exist");
896
897		assert_eq!(wallet_info.name, "test-get-wallet");
898		assert_eq!(wallet_info.address, created_wallet.address); // Now returns real address
899
900		// Test getting wallet with wrong password
901		// Now with real quantum-safe encryption, wrong password should be detected
902		let wallet_info = wallet_manager
903			.get_wallet("test-get-wallet", Some("wrong-password"))
904			.expect("Failed to get wallet")
905			.expect("Wallet should exist");
906
907		assert_eq!(wallet_info.name, "test-get-wallet");
908		// With real encryption, wrong password returns placeholder text
909		assert_eq!(wallet_info.address, "[Wrong password]");
910
911		// Test getting wallet with correct password
912		let wallet_info = wallet_manager
913			.get_wallet("test-get-wallet", Some("test-password"))
914			.expect("Failed to get wallet")
915			.expect("Wallet should exist");
916
917		assert_eq!(wallet_info.name, "test-get-wallet");
918		assert_eq!(wallet_info.address, created_wallet.address);
919		assert!(wallet_info.address.starts_with("qz"));
920
921		// Test getting non-existent wallet
922		let result = wallet_manager
923			.get_wallet("non-existent-wallet", None)
924			.expect("Should not error on non-existent wallet");
925
926		assert!(result.is_none());
927	}
928}