1use crate::error::WalletError;
2use aes_gcm::{
3 aead::{Aead, AeadCore, KeyInit, OsRng},
4 Aes256Gcm, Key, Nonce,
5};
6use base64::{engine::general_purpose, Engine as _};
7use bip39::{Language, Mnemonic};
8use chia::protocol::CoinState;
9use datalayer_driver::{
10 address_to_puzzle_hash, connect_random, get_coin_id, master_public_key_to_first_puzzle_hash,
11 master_public_key_to_wallet_synthetic_key, master_secret_key_to_wallet_synthetic_secret_key,
12 puzzle_hash_to_address, secret_key_to_public_key, sign_message, verify_signature, Bytes,
13 Bytes32, Coin, CoinSpend, DigCoin, NetworkType, Peer, PublicKey, SecretKey, Signature,
14};
15use serde::{Deserialize, Serialize};
16use std::collections::{HashMap, HashSet};
17use std::env;
18use std::fs;
19use std::path::PathBuf;
20
21const KEYRING_FILE: &str = "keyring.json";
22#[allow(dead_code)]
24const CACHE_DURATION_MS: u64 = 5 * 60 * 1000; pub const DEFAULT_FEE_COIN_COST: u64 = 64_000_000;
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28struct EncryptedData {
29 data: String,
30 nonce: String,
31 salt: String,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35struct KeyringData {
36 wallets: HashMap<String, EncryptedData>,
37}
38
39#[derive(Debug, Clone)]
40pub struct Wallet {
41 mnemonic: Option<String>,
42 wallet_name: String,
43}
44
45impl Wallet {
46 fn new(mnemonic: Option<String>, wallet_name: String) -> Self {
48 Self {
49 mnemonic,
50 wallet_name,
51 }
52 }
53
54 pub async fn load(
56 wallet_name: Option<String>,
57 create_on_undefined: bool,
58 ) -> Result<Self, WalletError> {
59 let name = wallet_name.unwrap_or_else(|| "default".to_string());
60
61 if let Some(mnemonic) = Self::get_wallet_from_keyring(&name).await? {
62 return Ok(Self::new(Some(mnemonic), name));
63 }
64
65 if create_on_undefined {
66 let new_mnemonic = Self::create_new_wallet(&name).await?;
69 return Ok(Self::new(Some(new_mnemonic), name));
70 }
71
72 Err(WalletError::WalletNotFound(name))
73 }
74
75 pub fn get_mnemonic(&self) -> Result<&str, WalletError> {
77 self.mnemonic
78 .as_deref()
79 .ok_or(WalletError::MnemonicNotLoaded)
80 }
81
82 pub fn get_wallet_name(&self) -> &str {
84 &self.wallet_name
85 }
86
87 pub async fn create_new_wallet(wallet_name: &str) -> Result<String, WalletError> {
89 let entropy = rand::random::<[u8; 32]>(); let mnemonic = Mnemonic::from_entropy_in(Language::English, &entropy)
91 .map_err(|_| WalletError::CryptoError("Failed to generate mnemonic".to_string()))?;
92 let mnemonic_str = mnemonic.to_string();
93 Self::save_wallet_to_keyring(wallet_name, &mnemonic_str).await?;
94 Ok(mnemonic_str)
95 }
96
97 pub async fn import_wallet(
99 wallet_name: &str,
100 seed: Option<&str>,
101 ) -> Result<String, WalletError> {
102 let mnemonic_str = match seed {
103 Some(s) => s.to_string(),
104 None => {
105 return Err(WalletError::MnemonicRequired);
107 }
108 };
109
110 Mnemonic::parse_in_normalized(Language::English, &mnemonic_str)
112 .map_err(|_| WalletError::InvalidMnemonic)?;
113
114 Self::save_wallet_to_keyring(wallet_name, &mnemonic_str).await?;
115 Ok(mnemonic_str)
116 }
117
118 pub async fn get_master_secret_key(&self) -> Result<SecretKey, WalletError> {
120 let mnemonic_str = self.get_mnemonic()?;
121 let mnemonic = Mnemonic::parse_in_normalized(Language::English, mnemonic_str)
122 .map_err(|_| WalletError::InvalidMnemonic)?;
123
124 let seed = mnemonic.to_seed("");
125 let sk = SecretKey::from_seed(&seed);
126 Ok(sk)
127 }
128
129 pub async fn get_public_synthetic_key(&self) -> Result<PublicKey, WalletError> {
131 let master_sk = self.get_master_secret_key().await?;
132 let master_pk = secret_key_to_public_key(&master_sk);
133 Ok(master_public_key_to_wallet_synthetic_key(&master_pk))
134 }
135
136 pub async fn get_private_synthetic_key(&self) -> Result<SecretKey, WalletError> {
138 let master_sk = self.get_master_secret_key().await?;
139 Ok(master_secret_key_to_wallet_synthetic_secret_key(&master_sk))
140 }
141
142 pub async fn get_owner_puzzle_hash(&self) -> Result<Bytes32, WalletError> {
144 let master_sk = self.get_master_secret_key().await?;
145 let master_pk = secret_key_to_public_key(&master_sk);
146 Ok(master_public_key_to_first_puzzle_hash(&master_pk))
147 }
148
149 pub async fn get_owner_public_key(&self) -> Result<String, WalletError> {
151 let owner_puzzle_hash = self.get_owner_puzzle_hash().await?;
152 puzzle_hash_to_address(owner_puzzle_hash, "xch")
154 .map_err(|e| WalletError::CryptoError(format!("Failed to encode address: {}", e)))
155 }
156
157 pub async fn delete_wallet(wallet_name: &str) -> Result<bool, WalletError> {
159 let keyring_path = Self::get_keyring_path()?;
160
161 if !keyring_path.exists() {
162 return Ok(false);
163 }
164
165 let content = fs::read_to_string(&keyring_path)
166 .map_err(|e| WalletError::FileSystemError(e.to_string()))?;
167
168 let mut keyring: KeyringData = serde_json::from_str(&content)
169 .map_err(|e| WalletError::SerializationError(e.to_string()))?;
170
171 if keyring.wallets.remove(wallet_name).is_some() {
172 let updated_content = serde_json::to_string_pretty(&keyring)
173 .map_err(|e| WalletError::SerializationError(e.to_string()))?;
174
175 fs::write(&keyring_path, updated_content)
176 .map_err(|e| WalletError::FileSystemError(e.to_string()))?;
177
178 Ok(true)
179 } else {
180 Ok(false)
181 }
182 }
183
184 pub async fn list_wallets() -> Result<Vec<String>, WalletError> {
186 let keyring_path = Self::get_keyring_path()?;
187
188 if !keyring_path.exists() {
189 return Ok(vec![]);
190 }
191
192 let content = fs::read_to_string(&keyring_path)
193 .map_err(|e| WalletError::FileSystemError(e.to_string()))?;
194
195 let keyring: KeyringData = serde_json::from_str(&content)
196 .map_err(|e| WalletError::SerializationError(e.to_string()))?;
197
198 Ok(keyring.wallets.keys().cloned().collect())
199 }
200
201 pub async fn create_key_ownership_signature(&self, nonce: &str) -> Result<String, WalletError> {
203 let message = format!(
204 "Signing this message to prove ownership of key.\n\nNonce: {}",
205 nonce
206 );
207 let private_synthetic_key = self.get_private_synthetic_key().await?;
208
209 let signature = sign_message(
210 &Bytes::from(message.as_bytes().to_vec()),
211 &private_synthetic_key,
212 )
213 .map_err(|e| WalletError::CryptoError(e.to_string()))?;
214
215 Ok(hex::encode(signature.to_bytes()))
216 }
217
218 pub async fn verify_key_ownership_signature(
220 nonce: &str,
221 signature: &str,
222 public_key: &str,
223 ) -> Result<bool, WalletError> {
224 let message = format!(
225 "Signing this message to prove ownership of key.\n\nNonce: {}",
226 nonce
227 );
228
229 let sig_bytes =
230 hex::decode(signature).map_err(|e| WalletError::CryptoError(e.to_string()))?;
231
232 let pk_bytes =
233 hex::decode(public_key).map_err(|e| WalletError::CryptoError(e.to_string()))?;
234
235 if pk_bytes.len() != 48 {
236 return Err(WalletError::CryptoError(
237 "Invalid public key length".to_string(),
238 ));
239 }
240
241 let mut pk_array = [0u8; 48];
242 pk_array.copy_from_slice(&pk_bytes);
243
244 let public_key = PublicKey::from_bytes(&pk_array)
245 .map_err(|e| WalletError::CryptoError(e.to_string()))?;
246
247 if sig_bytes.len() != 96 {
248 return Err(WalletError::CryptoError(
249 "Invalid signature length".to_string(),
250 ));
251 }
252
253 let mut sig_array = [0u8; 96];
254 sig_array.copy_from_slice(&sig_bytes);
255
256 let signature = Signature::from_bytes(&sig_array)
257 .map_err(|e| WalletError::CryptoError(e.to_string()))?;
258
259 verify_signature(
260 Bytes::from(message.as_bytes().to_vec()),
261 public_key,
262 signature,
263 )
264 .map_err(|e| WalletError::CryptoError(e.to_string()))
265 }
266
267 pub async fn get_all_unspent_dig_coins(
269 &self,
270 peer: &Peer,
271 omit_coins: Vec<Coin>,
272 verbose: bool,
273 ) -> Result<Vec<DigCoin>, WalletError> {
274 let owner_puzzle_hash = self.get_owner_puzzle_hash().await?;
275 let dig_ph = DigCoin::puzzle_hash(owner_puzzle_hash);
276
277 let unspent_coin_states = datalayer_driver::async_api::get_all_unspent_coins(
279 peer,
280 dig_ph,
281 None, datalayer_driver::constants::get_mainnet_genesis_challenge(), )
284 .await
285 .map_err(|e| WalletError::NetworkError(format!("Failed to get unspent coins: {}", e)))?;
286
287 let omit_coin_ids: Vec<Bytes32> = omit_coins.iter().map(get_coin_id).collect();
289 let available_coin_states: Vec<CoinState> = unspent_coin_states
290 .coin_states
291 .into_iter()
292 .filter(|coin_state| !omit_coin_ids.contains(&get_coin_id(&coin_state.coin)))
293 .collect();
294
295 let mut proved_dig_cats: Vec<DigCoin> = vec![];
296
297 for coin_state in &available_coin_states {
298 let cat_parse_result = DigCoin::from_coin_state(peer, coin_state).await;
300 match cat_parse_result {
301 Ok(parsed_cat) => {
302 proved_dig_cats.push(parsed_cat);
304 }
305 Err(error) => {
306 if verbose {
307 eprintln!(
308 "ERROR: coin_id {} | {}",
309 coin_state.coin.coin_id(),
310 WalletError::CoinSetError(format!(
311 "Failed to parse CAT and prove lineage: {}",
312 error
313 ))
314 );
315 }
316 continue;
317 }
318 }
319 }
320
321 Ok(proved_dig_cats)
322 }
323
324 pub async fn select_unspent_dig_coins(
325 &self,
326 peer: &Peer,
327 coin_amount: u64,
328 omit_coins: Vec<Coin>,
329 verbose: bool,
330 ) -> Result<Vec<DigCoin>, WalletError> {
331 let available_dig_cats = self
332 .get_all_unspent_dig_coins(peer, omit_coins, verbose)
333 .await?;
334
335 let dig_coins = available_dig_cats
336 .iter()
337 .map(|dig_coin| dig_coin.cat().coin)
338 .collect::<Vec<_>>();
339
340 let selected_coins = datalayer_driver::select_coins(&dig_coins, coin_amount)
342 .map_err(|e| WalletError::DataLayerError(format!("Coin selection failed: {}", e)))?;
343
344 if selected_coins.is_empty() {
345 return Err(WalletError::NoUnspentCoins);
346 }
347
348 let selected_coins_ids: HashSet<Bytes32> = selected_coins.iter().map(get_coin_id).collect();
349 let dig_coin = available_dig_cats
350 .into_iter()
351 .filter(|dig_coin| selected_coins_ids.contains(&dig_coin.cat().coin.coin_id()))
352 .collect::<Vec<_>>();
353
354 Ok(dig_coin)
355 }
356
357 pub async fn get_dig_balance(&self, peer: &Peer, verbose: bool) -> Result<u64, WalletError> {
358 let dig_cats = self
359 .get_all_unspent_dig_coins(peer, vec![], verbose)
360 .await?;
361 let dig_balance = dig_cats
362 .iter()
363 .map(|dig_coin| dig_coin.cat().coin.amount)
364 .sum::<u64>();
365 Ok(dig_balance)
366 }
367
368 pub async fn get_all_unspent_xch_coins(
369 &self,
370 peer: &Peer,
371 omit_coins: Vec<Coin>,
372 ) -> Result<Vec<Coin>, WalletError> {
373 let owner_puzzle_hash = self.get_owner_puzzle_hash().await?;
374
375 let coin_states = datalayer_driver::async_api::get_all_unspent_coins(
376 peer,
377 owner_puzzle_hash,
378 None, datalayer_driver::constants::get_mainnet_genesis_challenge(), )
381 .await
382 .map_err(|e| WalletError::NetworkError(format!("Failed to get unspent coins: {}", e)))?;
383
384 let omit_coin_ids: Vec<Bytes32> = omit_coins.iter().map(get_coin_id).collect();
386
387 Ok(coin_states
388 .coin_states
389 .into_iter()
390 .map(|cs| cs.coin)
391 .filter(|coin| !omit_coin_ids.contains(&get_coin_id(coin)))
392 .collect())
393 }
394
395 pub async fn select_unspent_coins(
397 &self,
398 peer: &Peer,
399 coin_amount: u64,
400 fee: u64,
401 omit_coins: Vec<Coin>,
402 ) -> Result<Vec<Coin>, WalletError> {
403 let total_needed = coin_amount + fee;
404
405 let available_coins = self.get_all_unspent_xch_coins(peer, omit_coins).await?;
406
407 let selected_coins = datalayer_driver::select_coins(&available_coins, total_needed)
409 .map_err(|e| WalletError::DataLayerError(format!("Coin selection failed: {}", e)))?;
410
411 if selected_coins.is_empty() {
412 return Err(WalletError::NoUnspentCoins);
413 }
414
415 Ok(selected_coins)
416 }
417
418 pub async fn get_xch_balance(&self, peer: &Peer) -> Result<u64, WalletError> {
419 let xch_coins = self.get_all_unspent_xch_coins(peer, vec![]).await?;
420 let xch_balance = xch_coins.iter().map(|c| c.amount).sum::<u64>();
421 Ok(xch_balance)
422 }
423
424 pub async fn calculate_fee_for_coin_spends(
426 _peer: &Peer,
427 _coin_spends: Option<&[CoinSpend]>,
428 ) -> Result<u64, WalletError> {
429 Ok(1_000_000) }
432
433 pub async fn is_coin_spendable(peer: &Peer, coin_id: &Bytes32) -> Result<bool, WalletError> {
435 let is_spent = datalayer_driver::is_coin_spent(
437 peer,
438 *coin_id,
439 None, datalayer_driver::constants::get_mainnet_genesis_challenge(), )
442 .await
443 .map_err(|e| WalletError::NetworkError(format!("Failed to check coin status: {}", e)))?;
444
445 Ok(!is_spent)
447 }
448
449 pub async fn connect_random_peer(
451 network: NetworkType,
452 cert_path: &str,
453 key_path: &str,
454 ) -> Result<Peer, WalletError> {
455 connect_random(network, cert_path, key_path)
456 .await
457 .map_err(|e| WalletError::NetworkError(format!("Failed to connect to peer: {}", e)))
458 }
459
460 pub async fn connect_mainnet_peer() -> Result<Peer, WalletError> {
462 let home_dir = dirs::home_dir().ok_or_else(|| {
463 WalletError::FileSystemError("Could not find home directory".to_string())
464 })?;
465
466 let ssl_dir = home_dir
467 .join(".chia")
468 .join("mainnet")
469 .join("config")
470 .join("ssl")
471 .join("wallet");
472 let cert_path = ssl_dir.join("wallet_node.crt");
473 let key_path = ssl_dir.join("wallet_node.key");
474
475 Self::connect_random_peer(
476 NetworkType::Mainnet,
477 cert_path
478 .to_str()
479 .ok_or_else(|| WalletError::FileSystemError("Invalid cert path".to_string()))?,
480 key_path
481 .to_str()
482 .ok_or_else(|| WalletError::FileSystemError("Invalid key path".to_string()))?,
483 )
484 .await
485 }
486
487 pub async fn connect_testnet_peer() -> Result<Peer, WalletError> {
489 let home_dir = dirs::home_dir().ok_or_else(|| {
490 WalletError::FileSystemError("Could not find home directory".to_string())
491 })?;
492
493 let ssl_dir = home_dir
494 .join(".chia")
495 .join("testnet11")
496 .join("config")
497 .join("ssl")
498 .join("wallet");
499 let cert_path = ssl_dir.join("wallet_node.crt");
500 let key_path = ssl_dir.join("wallet_node.key");
501
502 Self::connect_random_peer(
503 NetworkType::Testnet11,
504 cert_path
505 .to_str()
506 .ok_or_else(|| WalletError::FileSystemError("Invalid cert path".to_string()))?,
507 key_path
508 .to_str()
509 .ok_or_else(|| WalletError::FileSystemError("Invalid key path".to_string()))?,
510 )
511 .await
512 }
513
514 pub fn address_to_puzzle_hash(address: &str) -> Result<Bytes32, WalletError> {
516 address_to_puzzle_hash(address)
517 .map_err(|e| WalletError::CryptoError(format!("Failed to decode address: {}", e)))
518 }
519
520 pub fn puzzle_hash_to_address(
522 puzzle_hash: Bytes32,
523 prefix: &str,
524 ) -> Result<String, WalletError> {
525 puzzle_hash_to_address(puzzle_hash, prefix)
526 .map_err(|e| WalletError::CryptoError(format!("Failed to encode address: {}", e)))
527 }
528
529 async fn get_wallet_from_keyring(wallet_name: &str) -> Result<Option<String>, WalletError> {
532 let keyring_path = Self::get_keyring_path()?;
533
534 if !keyring_path.exists() {
535 return Ok(None);
536 }
537
538 let content = fs::read_to_string(&keyring_path)
539 .map_err(|e| WalletError::FileSystemError(e.to_string()))?;
540
541 let keyring: KeyringData = serde_json::from_str(&content)
542 .map_err(|e| WalletError::SerializationError(e.to_string()))?;
543
544 if let Some(encrypted_data) = keyring.wallets.get(wallet_name) {
545 let decrypted = Self::decrypt_data(encrypted_data)?;
546 Ok(Some(decrypted))
547 } else {
548 Ok(None)
549 }
550 }
551
552 async fn save_wallet_to_keyring(wallet_name: &str, mnemonic: &str) -> Result<(), WalletError> {
553 let keyring_path = Self::get_keyring_path()?;
554
555 if let Some(parent) = keyring_path.parent() {
557 fs::create_dir_all(parent).map_err(|e| WalletError::FileSystemError(e.to_string()))?;
558 }
559
560 let mut keyring = if keyring_path.exists() {
561 let content = fs::read_to_string(&keyring_path)
562 .map_err(|e| WalletError::FileSystemError(e.to_string()))?;
563 serde_json::from_str(&content)
564 .map_err(|e| WalletError::SerializationError(e.to_string()))?
565 } else {
566 KeyringData {
567 wallets: HashMap::new(),
568 }
569 };
570
571 let encrypted_data = Self::encrypt_data(mnemonic)?;
572
573 keyring
574 .wallets
575 .insert(wallet_name.to_string(), encrypted_data);
576
577 let content = serde_json::to_string_pretty(&keyring)
578 .map_err(|e| WalletError::SerializationError(e.to_string()))?;
579
580 fs::write(&keyring_path, content)
581 .map_err(|e| WalletError::FileSystemError(e.to_string()))?;
582
583 Ok(())
584 }
585
586 fn get_keyring_path() -> Result<PathBuf, WalletError> {
587 if let Ok(test_path) = env::var("TEST_KEYRING_PATH") {
589 return Ok(PathBuf::from(test_path));
590 }
591
592 let home_dir = dirs::home_dir().ok_or_else(|| {
593 WalletError::FileSystemError("Could not find home directory".to_string())
594 })?;
595
596 Ok(home_dir.join(".dig").join(KEYRING_FILE))
597 }
598
599 fn encrypt_data(data: &str) -> Result<EncryptedData, WalletError> {
601 let salt = rand::random::<[u8; 16]>();
603
604 let mut key_bytes = [0u8; 32];
607 let password = b"mnemonic-seed"; for i in 0..32 {
611 key_bytes[i] = password[i % password.len()] ^ salt[i % salt.len()];
612 }
613
614 let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
615 let cipher = Aes256Gcm::new(key);
616
617 let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
619
620 let ciphertext = cipher
622 .encrypt(&nonce, data.as_bytes())
623 .map_err(|e| WalletError::CryptoError(format!("Encryption failed: {}", e)))?;
624
625 Ok(EncryptedData {
626 data: general_purpose::STANDARD.encode(&ciphertext),
627 nonce: general_purpose::STANDARD.encode(nonce),
628 salt: general_purpose::STANDARD.encode(salt),
629 })
630 }
631
632 fn decrypt_data(encrypted_data: &EncryptedData) -> Result<String, WalletError> {
634 let ciphertext = general_purpose::STANDARD
635 .decode(&encrypted_data.data)
636 .map_err(|e| WalletError::CryptoError(format!("Failed to decode ciphertext: {}", e)))?;
637
638 let nonce_bytes = general_purpose::STANDARD
639 .decode(&encrypted_data.nonce)
640 .map_err(|e| WalletError::CryptoError(format!("Failed to decode nonce: {}", e)))?;
641
642 let salt = general_purpose::STANDARD
643 .decode(&encrypted_data.salt)
644 .map_err(|e| WalletError::CryptoError(format!("Failed to decode salt: {}", e)))?;
645
646 let mut key_bytes = [0u8; 32];
648 let password = b"mnemonic-seed";
649
650 for i in 0..32 {
651 key_bytes[i] = password[i % password.len()] ^ salt[i % salt.len()];
652 }
653
654 let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
655 let cipher = Aes256Gcm::new(key);
656
657 let nonce = Nonce::from_slice(&nonce_bytes);
658
659 let plaintext = cipher
661 .decrypt(nonce, ciphertext.as_ref())
662 .map_err(|e| WalletError::CryptoError(format!("Decryption failed: {}", e)))?;
663
664 String::from_utf8(plaintext).map_err(|e| {
665 WalletError::CryptoError(format!("Failed to convert decrypted data to string: {}", e))
666 })
667 }
668}
669
670#[cfg(test)]
671mod tests {
672 use super::*;
673 use std::env;
674 use tempfile::TempDir;
675
676 fn setup_test_env() -> TempDir {
678 let temp_dir = TempDir::new().unwrap();
679
680 let keyring_path = temp_dir.path().join("test_keyring.json");
682 env::set_var(
683 "TEST_KEYRING_PATH",
684 keyring_path.to_string_lossy().to_string(),
685 );
686
687 env::set_var("HOME", temp_dir.path());
689
690 temp_dir
691 }
692
693 #[tokio::test]
694 async fn test_wallet_creation() {
695 let _temp_dir = setup_test_env();
696
697 let mnemonic = Wallet::create_new_wallet("test_wallet").await.unwrap();
699
700 assert!(bip39::Mnemonic::parse_in_normalized(Language::English, &mnemonic).is_ok());
702
703 assert_eq!(mnemonic.split_whitespace().count(), 24);
705
706 let wallets = Wallet::list_wallets().await.unwrap();
708 assert!(wallets.contains(&"test_wallet".to_string()));
709 }
710
711 #[tokio::test]
712 async fn test_wallet_import() {
713 let _temp_dir = setup_test_env();
714
715 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";
717
718 let imported_mnemonic = Wallet::import_wallet("imported_wallet", Some(test_mnemonic))
720 .await
721 .unwrap();
722
723 assert_eq!(imported_mnemonic, test_mnemonic);
725
726 let wallet = Wallet::load(Some("imported_wallet".to_string()), false)
728 .await
729 .unwrap();
730 assert_eq!(wallet.get_mnemonic().unwrap(), test_mnemonic);
731 }
732
733 #[tokio::test]
734 async fn test_wallet_import_invalid_mnemonic() {
735 let _temp_dir = setup_test_env();
736
737 let invalid_mnemonic = "invalid mnemonic phrase that should fail validation";
739
740 let result = Wallet::import_wallet("invalid_wallet", Some(invalid_mnemonic)).await;
742 assert!(matches!(result, Err(WalletError::InvalidMnemonic)));
743 }
744
745 #[tokio::test]
746 async fn test_wallet_load_nonexistent() {
747 let _temp_dir = setup_test_env();
748
749 let result = Wallet::load(Some("nonexistent".to_string()), false).await;
751 assert!(matches!(result, Err(WalletError::WalletNotFound(_))));
752 }
753
754 #[tokio::test]
755 async fn test_wallet_load_with_creation() {
756 let _temp_dir = setup_test_env();
757
758 let wallet = Wallet::load(Some("auto_created".to_string()), true)
760 .await
761 .unwrap();
762
763 let mnemonic = wallet.get_mnemonic().unwrap();
765 assert!(bip39::Mnemonic::parse_in_normalized(Language::English, mnemonic).is_ok());
766
767 assert_eq!(wallet.get_wallet_name(), "auto_created");
769 }
770
771 #[tokio::test]
772 async fn test_key_derivation() {
773 let _temp_dir = setup_test_env();
774
775 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";
777
778 Wallet::import_wallet("key_test", Some(test_mnemonic))
779 .await
780 .unwrap();
781 let wallet = Wallet::load(Some("key_test".to_string()), false)
782 .await
783 .unwrap();
784
785 let master_sk = wallet.get_master_secret_key().await.unwrap();
787 let public_synthetic_key = wallet.get_public_synthetic_key().await.unwrap();
788 let private_synthetic_key = wallet.get_private_synthetic_key().await.unwrap();
789 let puzzle_hash = wallet.get_owner_puzzle_hash().await.unwrap();
790
791 assert_eq!(
793 secret_key_to_public_key(&private_synthetic_key),
794 public_synthetic_key
795 );
796
797 assert_eq!(puzzle_hash.as_ref().len(), 32);
799
800 let wallet2 = Wallet::load(Some("key_test".to_string()), false)
802 .await
803 .unwrap();
804 let master_sk2 = wallet2.get_master_secret_key().await.unwrap();
805 assert_eq!(master_sk.to_bytes(), master_sk2.to_bytes());
806 }
807
808 #[tokio::test]
809 async fn test_address_generation() {
810 let _temp_dir = setup_test_env();
811
812 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";
813
814 Wallet::import_wallet("address_test", Some(test_mnemonic))
815 .await
816 .unwrap();
817 let wallet = Wallet::load(Some("address_test".to_string()), false)
818 .await
819 .unwrap();
820
821 let address = wallet.get_owner_public_key().await.unwrap();
823
824 assert!(address.starts_with("xch1"));
826
827 assert!(address.len() >= 60 && address.len() <= 65);
829
830 let puzzle_hash = Wallet::address_to_puzzle_hash(&address).unwrap();
832 let converted_address = Wallet::puzzle_hash_to_address(puzzle_hash, "xch").unwrap();
833 assert_eq!(address, converted_address);
834 }
835
836 #[tokio::test]
837 async fn test_signature_creation_and_verification() {
838 let _temp_dir = setup_test_env();
839
840 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";
841
842 Wallet::import_wallet("sig_test", Some(test_mnemonic))
843 .await
844 .unwrap();
845 let wallet = Wallet::load(Some("sig_test".to_string()), false)
846 .await
847 .unwrap();
848
849 let nonce = "test_nonce_12345";
851 let signature = wallet.create_key_ownership_signature(nonce).await.unwrap();
852
853 assert!(hex::decode(&signature).is_ok());
855
856 let public_key = wallet.get_public_synthetic_key().await.unwrap();
858 let public_key_hex = hex::encode(public_key.to_bytes());
859
860 let is_valid = Wallet::verify_key_ownership_signature(nonce, &signature, &public_key_hex)
862 .await
863 .unwrap();
864 assert!(is_valid);
865
866 let is_valid_wrong =
868 Wallet::verify_key_ownership_signature("wrong_nonce", &signature, &public_key_hex)
869 .await
870 .unwrap();
871 assert!(!is_valid_wrong);
872 }
873
874 #[tokio::test]
875 async fn test_wallet_deletion() {
876 let _temp_dir = setup_test_env();
877
878 Wallet::create_new_wallet("delete_test").await.unwrap();
880
881 let wallets_before = Wallet::list_wallets().await.unwrap();
883 assert!(wallets_before.contains(&"delete_test".to_string()));
884
885 let deleted = Wallet::delete_wallet("delete_test").await.unwrap();
887 assert!(deleted);
888
889 let wallets_after = Wallet::list_wallets().await.unwrap();
891 assert!(!wallets_after.contains(&"delete_test".to_string()));
892
893 let not_deleted = Wallet::delete_wallet("nonexistent").await.unwrap();
895 assert!(!not_deleted);
896 }
897
898 #[tokio::test]
899 async fn test_multiple_wallets() {
900 let _temp_dir = setup_test_env();
901
902 Wallet::create_new_wallet("wallet1").await.unwrap();
904 Wallet::create_new_wallet("wallet2").await.unwrap();
905 Wallet::create_new_wallet("wallet3").await.unwrap();
906
907 let mut wallets = Wallet::list_wallets().await.unwrap();
909 wallets.sort(); assert_eq!(wallets.len(), 3);
912 assert!(wallets.contains(&"wallet1".to_string()));
913 assert!(wallets.contains(&"wallet2".to_string()));
914 assert!(wallets.contains(&"wallet3".to_string()));
915
916 let w1 = Wallet::load(Some("wallet1".to_string()), false)
918 .await
919 .unwrap();
920 let w2 = Wallet::load(Some("wallet2".to_string()), false)
921 .await
922 .unwrap();
923 let w3 = Wallet::load(Some("wallet3".to_string()), false)
924 .await
925 .unwrap();
926
927 assert_ne!(w1.get_mnemonic().unwrap(), w2.get_mnemonic().unwrap());
928 assert_ne!(w2.get_mnemonic().unwrap(), w3.get_mnemonic().unwrap());
929 assert_ne!(w1.get_mnemonic().unwrap(), w3.get_mnemonic().unwrap());
930 }
931
932 #[tokio::test]
933 async fn test_encryption_decryption() {
934 let test_data = "test mnemonic phrase for encryption";
936
937 let encrypted = Wallet::encrypt_data(test_data).unwrap();
938
939 assert_ne!(encrypted.data, test_data);
941 assert!(!encrypted.nonce.is_empty());
942 assert!(!encrypted.salt.is_empty());
943
944 let decrypted = Wallet::decrypt_data(&encrypted).unwrap();
946 assert_eq!(decrypted, test_data);
947 }
948
949 #[tokio::test]
950 async fn test_encryption_with_different_salts() {
951 let test_data = "same data";
952
953 let encrypted1 = Wallet::encrypt_data(test_data).unwrap();
955 let encrypted2 = Wallet::encrypt_data(test_data).unwrap();
956
957 assert_ne!(encrypted1.data, encrypted2.data);
959 assert_ne!(encrypted1.salt, encrypted2.salt);
960 assert_ne!(encrypted1.nonce, encrypted2.nonce);
961
962 let decrypted1 = Wallet::decrypt_data(&encrypted1).unwrap();
964 let decrypted2 = Wallet::decrypt_data(&encrypted2).unwrap();
965 assert_eq!(decrypted1, test_data);
966 assert_eq!(decrypted2, test_data);
967 }
968
969 #[tokio::test]
970 async fn test_invalid_signature_verification() {
971 let _temp_dir = setup_test_env();
972
973 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";
975 Wallet::import_wallet("invalid_sig_test", Some(test_mnemonic))
976 .await
977 .unwrap();
978 let wallet = Wallet::load(Some("invalid_sig_test".to_string()), false)
979 .await
980 .unwrap();
981
982 let public_key = wallet.get_public_synthetic_key().await.unwrap();
983 let public_key_hex = hex::encode(public_key.to_bytes());
984
985 let result =
987 Wallet::verify_key_ownership_signature("nonce", "invalid_hex", &public_key_hex).await;
988 assert!(result.is_err());
989
990 let short_sig = "deadbeef";
992 let result =
993 Wallet::verify_key_ownership_signature("nonce", short_sig, &public_key_hex).await;
994 assert!(result.is_err());
995
996 let result =
998 Wallet::verify_key_ownership_signature("nonce", &"a".repeat(192), "invalid_key").await;
999 assert!(result.is_err());
1000 }
1001
1002 #[tokio::test]
1003 async fn test_address_conversion_errors() {
1004 let result = Wallet::address_to_puzzle_hash("invalid_address");
1006 assert!(result.is_err());
1007
1008 let result = Wallet::address_to_puzzle_hash("");
1010 assert!(result.is_err());
1011 }
1012
1013 #[tokio::test]
1014 async fn test_mnemonic_not_loaded_error() {
1015 let wallet = Wallet::new(None, "empty_wallet".to_string());
1017
1018 let result = wallet.get_mnemonic();
1020 assert!(matches!(result, Err(WalletError::MnemonicNotLoaded)));
1021
1022 let result = wallet.get_master_secret_key().await;
1024 assert!(matches!(result, Err(WalletError::MnemonicNotLoaded)));
1025 }
1026
1027 #[tokio::test]
1028 async fn test_default_wallet_name() {
1029 let _temp_dir = setup_test_env();
1030
1031 let wallet = Wallet::load(None, true).await.unwrap();
1033 assert_eq!(wallet.get_wallet_name(), "default");
1034
1035 let wallets = Wallet::list_wallets().await.unwrap();
1037 assert!(wallets.contains(&"default".to_string()));
1038 }
1039}