1pub 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 serde::{Deserialize, Serialize};
15use sp_core::crypto::Ss58Codec;
16use sp_runtime::traits::IdentifyAccount;
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct WalletInfo {
20 pub name: String,
21 pub address: String,
22 pub created_at: chrono::DateTime<chrono::Utc>,
23 pub key_type: String,
24}
25
26pub struct WalletManager {
28 wallets_dir: std::path::PathBuf,
29}
30
31impl WalletManager {
32 pub fn new() -> Result<Self> {
34 let wallets_dir = dirs::home_dir()
35 .ok_or(WalletError::KeyGeneration)?
36 .join(".quantus")
37 .join("wallets");
38
39 std::fs::create_dir_all(&wallets_dir)?;
41
42 Ok(Self { wallets_dir })
43 }
44
45 pub async fn create_wallet(&self, name: &str, password: Option<&str>) -> Result<WalletInfo> {
47 let keystore = Keystore::new(&self.wallets_dir);
49 if keystore.load_wallet(name)?.is_some() {
50 return Err(WalletError::AlreadyExists.into());
51 }
52
53 let mnemonic = generate_mnemonic(24).map_err(|_| WalletError::KeyGeneration)?;
55 let lattice =
56 HDLattice::from_mnemonic(&mnemonic, None).expect("Failed to generate lattice");
57 let dilithium_keypair = lattice.generate_keys();
58 let quantum_keypair = QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
59
60 let mut metadata = std::collections::HashMap::new();
62 metadata.insert("version".to_string(), "1.0.0".to_string());
63 metadata.insert("algorithm".to_string(), "ML-DSA-87".to_string());
64
65 let address = quantum_keypair.to_account_id_ss58check();
67
68 let wallet_data = WalletData {
69 name: name.to_string(),
70 keypair: quantum_keypair,
71 mnemonic: Some(mnemonic.clone()),
72 metadata,
73 };
74
75 let password = password.unwrap_or(""); let encrypted_wallet = keystore.encrypt_wallet_data(&wallet_data, password)?;
78 keystore.save_wallet(&encrypted_wallet)?;
79
80 Ok(WalletInfo {
81 name: name.to_string(),
82 address,
83 created_at: encrypted_wallet.created_at,
84 key_type: "Dilithium ML-DSA-87".to_string(),
85 })
86 }
87
88 pub async fn create_developer_wallet(&self, name: &str) -> Result<WalletInfo> {
90 let keystore = Keystore::new(&self.wallets_dir);
92
93 let resonance_pair = match name {
95 "crystal_alice" => qp_dilithium_crypto::crystal_alice(),
96 "crystal_bob" => qp_dilithium_crypto::dilithium_bob(),
97 "crystal_charlie" => qp_dilithium_crypto::crystal_charlie(),
98 _ => return Err(WalletError::KeyGeneration.into()),
99 };
100
101 let quantum_keypair = QuantumKeyPair::from_resonance_pair(&resonance_pair);
102
103 println!("🔑 Resonance pair: {:?}", resonance_pair.public().into_account().to_ss58check());
104 println!("🔑 Quantum keypair: {:?}", quantum_keypair.to_account_id_ss58check());
105
106 let mut metadata = std::collections::HashMap::new();
108 metadata.insert("version".to_string(), "1.0.0".to_string());
109 metadata.insert("algorithm".to_string(), "ML-DSA-87".to_string());
110 metadata.insert("test_wallet".to_string(), "true".to_string());
111
112 let address = quantum_keypair.to_account_id_ss58check();
114
115 let wallet_data = WalletData {
116 name: name.to_string(),
117 keypair: quantum_keypair,
118 mnemonic: None, metadata,
120 };
121
122 let encrypted_wallet = keystore.encrypt_wallet_data(&wallet_data, "")?;
124 keystore.save_wallet(&encrypted_wallet)?;
125
126 Ok(WalletInfo {
127 name: name.to_string(),
128 address,
129 created_at: encrypted_wallet.created_at,
130 key_type: "Dilithium ML-DSA-87".to_string(),
131 })
132 }
133
134 pub fn export_mnemonic(&self, name: &str, password: Option<&str>) -> Result<String> {
136 let final_password = password::get_wallet_password(name, password.map(String::from), None)?;
137
138 let wallet_data = self.load_wallet(name, &final_password)?;
139
140 wallet_data.mnemonic.ok_or_else(|| WalletError::MnemonicNotAvailable.into())
141 }
142
143 pub fn list_wallets(&self) -> Result<Vec<WalletInfo>> {
145 let keystore = Keystore::new(&self.wallets_dir);
146 let wallet_names = keystore.list_wallets()?;
147
148 let mut wallets = Vec::new();
149 for name in wallet_names {
150 if let Some(encrypted_wallet) = keystore.load_wallet(&name)? {
151 let wallet_info = WalletInfo {
153 name: encrypted_wallet.name,
154 address: encrypted_wallet.address, created_at: encrypted_wallet.created_at,
156 key_type: "Dilithium ML-DSA-87".to_string(),
157 };
158 wallets.push(wallet_info);
159 }
160 }
161
162 wallets.sort_by(|a, b| b.created_at.cmp(&a.created_at));
164 Ok(wallets)
165 }
166
167 pub async fn import_wallet(
169 &self,
170 name: &str,
171 mnemonic: &str,
172 password: Option<&str>,
173 ) -> Result<WalletInfo> {
174 let keystore = Keystore::new(&self.wallets_dir);
176 if keystore.load_wallet(name)?.is_some() {
177 return Err(WalletError::AlreadyExists.into());
178 }
179
180 let lattice =
182 HDLattice::from_mnemonic(mnemonic, None).map_err(|_| WalletError::InvalidMnemonic)?;
183 let dilithium_keypair = lattice.generate_keys();
184 let quantum_keypair = QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
185
186 let mut metadata = std::collections::HashMap::new();
188 metadata.insert("version".to_string(), "1.0.0".to_string());
189 metadata.insert("algorithm".to_string(), "ML-DSA-87".to_string());
190 metadata.insert("imported".to_string(), "true".to_string());
191
192 let address = quantum_keypair.to_account_id_ss58check();
194
195 let wallet_data = WalletData {
196 name: name.to_string(),
197 keypair: quantum_keypair,
198 mnemonic: Some(mnemonic.to_string()),
199 metadata,
200 };
201
202 let password = password.unwrap_or(""); let encrypted_wallet = keystore.encrypt_wallet_data(&wallet_data, password)?;
205 keystore.save_wallet(&encrypted_wallet)?;
206
207 Ok(WalletInfo {
208 name: name.to_string(),
209 address,
210 created_at: encrypted_wallet.created_at,
211 key_type: "Dilithium ML-DSA-87".to_string(),
212 })
213 }
214
215 pub fn get_wallet(&self, name: &str, password: Option<&str>) -> Result<Option<WalletInfo>> {
217 let keystore = Keystore::new(&self.wallets_dir);
218
219 if let Some(encrypted_wallet) = keystore.load_wallet(name)? {
220 if let Some(pwd) = password {
221 match keystore.decrypt_wallet_data(&encrypted_wallet, pwd) {
223 Ok(wallet_data) => {
224 let address = wallet_data.keypair.to_account_id_ss58check();
225 Ok(Some(WalletInfo {
226 name: wallet_data.name,
227 address,
228 created_at: encrypted_wallet.created_at,
229 key_type: "Dilithium ML-DSA-87".to_string(),
230 }))
231 },
232 Err(_) => {
233 Ok(Some(WalletInfo {
235 name: encrypted_wallet.name,
236 address: "[Wrong password]".to_string(),
237 created_at: encrypted_wallet.created_at,
238 key_type: "Dilithium ML-DSA-87".to_string(),
239 }))
240 },
241 }
242 } else {
243 Ok(Some(WalletInfo {
245 name: encrypted_wallet.name,
246 address: encrypted_wallet.address, created_at: encrypted_wallet.created_at,
248 key_type: "Dilithium ML-DSA-87".to_string(),
249 }))
250 }
251 } else {
252 Ok(None)
253 }
254 }
255
256 pub fn load_wallet(&self, name: &str, password: &str) -> Result<WalletData> {
258 let keystore = Keystore::new(&self.wallets_dir);
259
260 let encrypted_wallet = keystore.load_wallet(name)?.ok_or(WalletError::NotFound)?;
262
263 let wallet_data = keystore.decrypt_wallet_data(&encrypted_wallet, password)?;
265
266 Ok(wallet_data)
267 }
268
269 pub fn delete_wallet(&self, name: &str) -> Result<bool> {
271 let keystore = Keystore::new(&self.wallets_dir);
272 keystore.delete_wallet(name)
273 }
274
275 pub fn find_wallet_address(&self, name: &str) -> Result<Option<String>> {
277 let keystore = Keystore::new(&self.wallets_dir);
278
279 if let Some(encrypted_wallet) = keystore.load_wallet(name)? {
280 Ok(Some(encrypted_wallet.address))
282 } else {
283 Ok(None)
284 }
285 }
286}
287
288pub fn load_keypair_from_wallet(
289 wallet_name: &str,
290 password: Option<String>,
291 password_file: Option<String>,
292) -> Result<QuantumKeyPair> {
293 let wallet_manager = WalletManager::new()?;
294 let wallet_password = password::get_wallet_password(wallet_name, password, password_file)?;
295 let wallet_data = wallet_manager.load_wallet(wallet_name, &wallet_password)?;
296 let keypair = wallet_data.keypair;
297 Ok(keypair)
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use std::fs;
304 use tempfile::TempDir;
305
306 async fn create_test_wallet_manager() -> (WalletManager, TempDir) {
307 let temp_dir = TempDir::new().expect("Failed to create temp directory");
308 let wallets_dir = temp_dir.path().join("wallets");
309 fs::create_dir_all(&wallets_dir).expect("Failed to create wallets directory");
310
311 let wallet_manager = WalletManager { wallets_dir };
312
313 (wallet_manager, temp_dir)
314 }
315
316 #[tokio::test]
317 async fn test_wallet_creation() {
318 let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
319
320 let wallet_info = wallet_manager
322 .create_wallet("test-wallet", Some("test-password"))
323 .await
324 .expect("Failed to create wallet");
325
326 assert_eq!(wallet_info.name, "test-wallet");
328 assert!(wallet_info.address.starts_with("5")); assert_eq!(wallet_info.key_type, "Dilithium ML-DSA-87");
330 assert!(wallet_info.created_at <= chrono::Utc::now());
331 }
332
333 #[tokio::test]
334 async fn test_wallet_already_exists() {
335 let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
336
337 wallet_manager
339 .create_wallet("duplicate-wallet", None)
340 .await
341 .expect("Failed to create first wallet");
342
343 let result = wallet_manager.create_wallet("duplicate-wallet", None).await;
345
346 assert!(result.is_err());
347 match result.unwrap_err() {
348 crate::error::QuantusError::Wallet(WalletError::AlreadyExists) => {},
349 _ => panic!("Expected AlreadyExists error"),
350 }
351 }
352
353 #[tokio::test]
354 async fn test_wallet_file_creation() {
355 let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
356
357 let _ = wallet_manager
359 .create_wallet("file-test-wallet", Some("password123"))
360 .await
361 .expect("Failed to create wallet");
362
363 let wallet_file = wallet_manager.wallets_dir.join("file-test-wallet.json");
365 assert!(wallet_file.exists(), "Wallet file should exist");
366
367 let file_size = fs::metadata(&wallet_file).expect("Failed to get file metadata").len();
369 assert!(file_size > 0, "Wallet file should not be empty");
370 }
371
372 #[tokio::test]
373 async fn test_keystore_encryption_decryption() {
374 let temp_dir = TempDir::new().expect("Failed to create temp directory");
375 let keystore = keystore::Keystore::new(temp_dir.path());
376
377 let entropy = [1u8; 32]; let dilithium_keypair =
380 qp_rusty_crystals_dilithium::ml_dsa_87::Keypair::generate(Some(&entropy));
381 let quantum_keypair = keystore::QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
382
383 let mut metadata = std::collections::HashMap::new();
384 metadata.insert("test_key".to_string(), "test_value".to_string());
385
386 let original_wallet_data = keystore::WalletData {
387 name: "test-wallet".to_string(),
388 keypair: quantum_keypair,
389 mnemonic: Some(
390 "test mnemonic phrase with twenty four words here for testing purposes only"
391 .to_string(),
392 ),
393 metadata,
394 };
395
396 let encrypted_wallet = keystore
398 .encrypt_wallet_data(&original_wallet_data, "test-password")
399 .expect("Failed to encrypt wallet data");
400
401 assert_eq!(encrypted_wallet.name, "test-wallet");
402 assert!(!encrypted_wallet.encrypted_data.is_empty());
403 assert!(!encrypted_wallet.argon2_salt.is_empty());
404 assert!(!encrypted_wallet.aes_nonce.is_empty());
405
406 let decrypted_wallet_data = keystore
408 .decrypt_wallet_data(&encrypted_wallet, "test-password")
409 .expect("Failed to decrypt wallet data");
410
411 assert_eq!(decrypted_wallet_data.name, original_wallet_data.name);
413 assert_eq!(decrypted_wallet_data.mnemonic, original_wallet_data.mnemonic);
414 assert_eq!(decrypted_wallet_data.metadata, original_wallet_data.metadata);
415 assert_eq!(
416 decrypted_wallet_data.keypair.public_key,
417 original_wallet_data.keypair.public_key
418 );
419 assert_eq!(
420 decrypted_wallet_data.keypair.private_key,
421 original_wallet_data.keypair.private_key
422 );
423 }
424
425 #[tokio::test]
426 async fn test_quantum_keypair_address_generation() {
427 let entropy = [2u8; 32]; let dilithium_keypair =
430 qp_rusty_crystals_dilithium::ml_dsa_87::Keypair::generate(Some(&entropy));
431 let quantum_keypair = keystore::QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
432
433 let account_id = quantum_keypair.to_account_id_32();
435 let ss58_address = quantum_keypair.to_account_id_ss58check();
436
437 assert!(ss58_address.starts_with("5"), "SS58 address should start with 5");
439 assert!(ss58_address.len() >= 47, "SS58 address should be at least 47 characters");
440
441 let converted_account_bytes = keystore::QuantumKeyPair::ss58_to_account_id(&ss58_address);
443 let account_bytes: &[u8] = account_id.as_ref();
444 assert_eq!(converted_account_bytes, account_bytes);
445 }
446
447 #[tokio::test]
448 async fn test_keystore_save_and_load() {
449 let temp_dir = TempDir::new().expect("Failed to create temp directory");
450 let keystore = keystore::Keystore::new(temp_dir.path());
451
452 let entropy = [3u8; 32]; let dilithium_keypair =
455 qp_rusty_crystals_dilithium::ml_dsa_87::Keypair::generate(Some(&entropy));
456 let quantum_keypair = keystore::QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
457
458 let wallet_data = keystore::WalletData {
459 name: "save-load-test".to_string(),
460 keypair: quantum_keypair,
461 mnemonic: Some("save load test mnemonic phrase".to_string()),
462 metadata: std::collections::HashMap::new(),
463 };
464
465 let encrypted_wallet = keystore
466 .encrypt_wallet_data(&wallet_data, "save-load-password")
467 .expect("Failed to encrypt wallet");
468
469 keystore.save_wallet(&encrypted_wallet).expect("Failed to save wallet");
471
472 let loaded_wallet = keystore
474 .load_wallet("save-load-test")
475 .expect("Failed to load wallet")
476 .expect("Wallet should exist");
477
478 assert_eq!(loaded_wallet.name, encrypted_wallet.name);
480 assert_eq!(loaded_wallet.encrypted_data, encrypted_wallet.encrypted_data);
481 assert_eq!(loaded_wallet.argon2_salt, encrypted_wallet.argon2_salt);
482 assert_eq!(loaded_wallet.aes_nonce, encrypted_wallet.aes_nonce);
483
484 let non_existent = keystore
486 .load_wallet("non-existent-wallet")
487 .expect("Load should succeed but return None");
488 assert!(non_existent.is_none());
489 }
490
491 #[tokio::test]
492 async fn test_mnemonic_generation_and_key_derivation() {
493 let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
494
495 let wallet1 = wallet_manager
497 .create_wallet("mnemonic-test-1", None)
498 .await
499 .expect("Failed to create wallet 1");
500
501 let wallet2 = wallet_manager
502 .create_wallet("mnemonic-test-2", None)
503 .await
504 .expect("Failed to create wallet 2");
505
506 assert_ne!(wallet1.address, wallet2.address);
508
509 assert!(wallet1.address.starts_with("5"));
511 assert!(wallet2.address.starts_with("5"));
512 }
513
514 #[tokio::test]
515 async fn test_wallet_import() {
516 let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
517
518 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";
520
521 let imported_wallet = wallet_manager
523 .import_wallet("imported-test-wallet", test_mnemonic, Some("import-password"))
524 .await
525 .expect("Failed to import wallet");
526
527 assert_eq!(imported_wallet.name, "imported-test-wallet");
529 assert!(imported_wallet.address.starts_with("5"));
530 assert_eq!(imported_wallet.key_type, "Dilithium ML-DSA-87");
531
532 let imported_wallet2 = wallet_manager
534 .import_wallet("imported-test-wallet-2", test_mnemonic, None)
535 .await
536 .expect("Failed to import wallet again");
537
538 assert_eq!(imported_wallet.address, imported_wallet2.address);
539 }
540
541 #[tokio::test]
542 async fn test_wallet_import_invalid_mnemonic() {
543 let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
544
545 let invalid_mnemonic = "invalid mnemonic phrase that should not work";
547
548 let result = wallet_manager.import_wallet("invalid-wallet", invalid_mnemonic, None).await;
549
550 assert!(result.is_err());
551 match result.unwrap_err() {
552 crate::error::QuantusError::Wallet(WalletError::InvalidMnemonic) => {},
553 _ => panic!("Expected InvalidMnemonic error"),
554 }
555 }
556
557 #[tokio::test]
558 async fn test_wallet_import_already_exists() {
559 let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
560
561 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";
562
563 wallet_manager
565 .import_wallet("duplicate-import-wallet", test_mnemonic, None)
566 .await
567 .expect("Failed to import first wallet");
568
569 let result = wallet_manager
571 .import_wallet("duplicate-import-wallet", test_mnemonic, None)
572 .await;
573
574 assert!(result.is_err());
575 match result.unwrap_err() {
576 crate::error::QuantusError::Wallet(WalletError::AlreadyExists) => {},
577 _ => panic!("Expected AlreadyExists error"),
578 }
579 }
580
581 #[tokio::test]
582 async fn test_list_wallets() {
583 let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
584
585 let wallets = wallet_manager.list_wallets().expect("Failed to list wallets");
587 assert_eq!(wallets.len(), 0);
588
589 wallet_manager
591 .create_wallet("wallet-1", Some("password1"))
592 .await
593 .expect("Failed to create wallet 1");
594
595 wallet_manager
596 .create_wallet("wallet-2", None)
597 .await
598 .expect("Failed to create wallet 2");
599
600 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";
601 wallet_manager
602 .import_wallet("imported-wallet", test_mnemonic, Some("password3"))
603 .await
604 .expect("Failed to import wallet");
605
606 let wallets = wallet_manager.list_wallets().expect("Failed to list wallets");
608
609 assert_eq!(wallets.len(), 3);
610
611 let wallet_names: Vec<&String> = wallets.iter().map(|w| &w.name).collect();
613 assert!(wallet_names.contains(&&"wallet-1".to_string()));
614 assert!(wallet_names.contains(&&"wallet-2".to_string()));
615 assert!(wallet_names.contains(&&"imported-wallet".to_string()));
616
617 for wallet in &wallets {
619 assert!(wallet.address.starts_with("5")); assert_eq!(wallet.key_type, "Dilithium ML-DSA-87");
621 }
622
623 assert!(wallets[0].created_at >= wallets[1].created_at);
625 assert!(wallets[1].created_at >= wallets[2].created_at);
626 }
627
628 #[tokio::test]
629 async fn test_get_wallet() {
630 let (wallet_manager, _temp_dir) = create_test_wallet_manager().await;
631
632 let created_wallet = wallet_manager
634 .create_wallet("test-get-wallet", Some("test-password"))
635 .await
636 .expect("Failed to create wallet");
637
638 let wallet_info = wallet_manager
640 .get_wallet("test-get-wallet", None)
641 .expect("Failed to get wallet")
642 .expect("Wallet should exist");
643
644 assert_eq!(wallet_info.name, "test-get-wallet");
645 assert_eq!(wallet_info.address, created_wallet.address); let wallet_info = wallet_manager
650 .get_wallet("test-get-wallet", Some("wrong-password"))
651 .expect("Failed to get wallet")
652 .expect("Wallet should exist");
653
654 assert_eq!(wallet_info.name, "test-get-wallet");
655 assert_eq!(wallet_info.address, "[Wrong password]");
657
658 let wallet_info = wallet_manager
660 .get_wallet("test-get-wallet", Some("test-password"))
661 .expect("Failed to get wallet")
662 .expect("Wallet should exist");
663
664 assert_eq!(wallet_info.name, "test-get-wallet");
665 assert_eq!(wallet_info.address, created_wallet.address);
666 assert!(wallet_info.address.starts_with("5"));
667
668 let result = wallet_manager
670 .get_wallet("non-existent-wallet", None)
671 .expect("Should not error on non-existent wallet");
672
673 assert!(result.is_none());
674 }
675}