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 chia::puzzles::cat::CatArgs;
10use chia_wallet_sdk::driver::{Cat, Puzzle};
11use chia_wallet_sdk::prelude::{Allocator, ToClvm, TreeHash};
12use chia_wallet_sdk::types::MAINNET_CONSTANTS;
13use datalayer_driver::{
14 address_to_puzzle_hash, connect_random, get_coin_id, master_public_key_to_first_puzzle_hash,
15 master_public_key_to_wallet_synthetic_key, master_secret_key_to_wallet_synthetic_secret_key,
16 puzzle_hash_to_address, secret_key_to_public_key, sign_message, verify_signature, Bytes,
17 Bytes32, Coin, CoinSpend, NetworkType, Peer, PublicKey, SecretKey, Signature,
18};
19use hex_literal::hex;
20use once_cell::sync::Lazy;
21use serde::{Deserialize, Serialize};
22use std::collections::HashMap;
23use std::env;
24use std::fs;
25use std::path::PathBuf;
26
27pub static DIG_MIN_HEIGHT: u32 = 5777842;
28pub static DIG_COIN_ASSET_ID: Lazy<Bytes32> = Lazy::new(|| {
29 Bytes32::new(hex!(
30 "a406d3a9de984d03c9591c10d917593b434d5263cabe2b42f6b367df16832f81"
31 ))
32});
33const KEYRING_FILE: &str = "keyring.json";
34#[allow(dead_code)]
36const CACHE_DURATION_MS: u64 = 5 * 60 * 1000; pub const DEFAULT_FEE_COIN_COST: u64 = 64_000_000;
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40struct EncryptedData {
41 data: String,
42 nonce: String,
43 salt: String,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47struct KeyringData {
48 wallets: HashMap<String, EncryptedData>,
49}
50
51pub struct Wallet {
52 mnemonic: Option<String>,
53 wallet_name: String,
54}
55
56impl Wallet {
57 fn new(mnemonic: Option<String>, wallet_name: String) -> Self {
59 Self {
60 mnemonic,
61 wallet_name,
62 }
63 }
64
65 pub async fn load(
67 wallet_name: Option<String>,
68 create_on_undefined: bool,
69 ) -> Result<Self, WalletError> {
70 let name = wallet_name.unwrap_or_else(|| "default".to_string());
71
72 if let Some(mnemonic) = Self::get_wallet_from_keyring(&name).await? {
73 return Ok(Self::new(Some(mnemonic), name));
74 }
75
76 if create_on_undefined {
77 let new_mnemonic = Self::create_new_wallet(&name).await?;
80 return Ok(Self::new(Some(new_mnemonic), name));
81 }
82
83 Err(WalletError::WalletNotFound(name))
84 }
85
86 pub fn get_mnemonic(&self) -> Result<&str, WalletError> {
88 self.mnemonic
89 .as_deref()
90 .ok_or(WalletError::MnemonicNotLoaded)
91 }
92
93 pub fn get_wallet_name(&self) -> &str {
95 &self.wallet_name
96 }
97
98 pub async fn create_new_wallet(wallet_name: &str) -> Result<String, WalletError> {
100 let entropy = rand::random::<[u8; 32]>(); let mnemonic = Mnemonic::from_entropy_in(Language::English, &entropy)
102 .map_err(|_| WalletError::CryptoError("Failed to generate mnemonic".to_string()))?;
103 let mnemonic_str = mnemonic.to_string();
104 Self::save_wallet_to_keyring(wallet_name, &mnemonic_str).await?;
105 Ok(mnemonic_str)
106 }
107
108 pub async fn import_wallet(
110 wallet_name: &str,
111 seed: Option<&str>,
112 ) -> Result<String, WalletError> {
113 let mnemonic_str = match seed {
114 Some(s) => s.to_string(),
115 None => {
116 return Err(WalletError::MnemonicRequired);
118 }
119 };
120
121 Mnemonic::parse_in_normalized(Language::English, &mnemonic_str)
123 .map_err(|_| WalletError::InvalidMnemonic)?;
124
125 Self::save_wallet_to_keyring(wallet_name, &mnemonic_str).await?;
126 Ok(mnemonic_str)
127 }
128
129 pub async fn get_master_secret_key(&self) -> Result<SecretKey, WalletError> {
131 let mnemonic_str = self.get_mnemonic()?;
132 let mnemonic = Mnemonic::parse_in_normalized(Language::English, mnemonic_str)
133 .map_err(|_| WalletError::InvalidMnemonic)?;
134
135 let seed = mnemonic.to_seed("");
136 let sk = SecretKey::from_seed(&seed);
137 Ok(sk)
138 }
139
140 pub async fn get_public_synthetic_key(&self) -> Result<PublicKey, WalletError> {
142 let master_sk = self.get_master_secret_key().await?;
143 let master_pk = secret_key_to_public_key(&master_sk);
144 Ok(master_public_key_to_wallet_synthetic_key(&master_pk))
145 }
146
147 pub async fn get_private_synthetic_key(&self) -> Result<SecretKey, WalletError> {
149 let master_sk = self.get_master_secret_key().await?;
150 Ok(master_secret_key_to_wallet_synthetic_secret_key(&master_sk))
151 }
152
153 pub async fn get_owner_puzzle_hash(&self) -> Result<Bytes32, WalletError> {
155 let master_sk = self.get_master_secret_key().await?;
156 let master_pk = secret_key_to_public_key(&master_sk);
157 Ok(master_public_key_to_first_puzzle_hash(&master_pk))
158 }
159
160 pub async fn get_owner_public_key(&self) -> Result<String, WalletError> {
162 let owner_puzzle_hash = self.get_owner_puzzle_hash().await?;
163 puzzle_hash_to_address(owner_puzzle_hash, "xch")
165 .map_err(|e| WalletError::CryptoError(format!("Failed to encode address: {}", e)))
166 }
167
168 pub async fn delete_wallet(wallet_name: &str) -> Result<bool, WalletError> {
170 let keyring_path = Self::get_keyring_path()?;
171
172 if !keyring_path.exists() {
173 return Ok(false);
174 }
175
176 let content = fs::read_to_string(&keyring_path)
177 .map_err(|e| WalletError::FileSystemError(e.to_string()))?;
178
179 let mut keyring: KeyringData = serde_json::from_str(&content)
180 .map_err(|e| WalletError::SerializationError(e.to_string()))?;
181
182 if keyring.wallets.remove(wallet_name).is_some() {
183 let updated_content = serde_json::to_string_pretty(&keyring)
184 .map_err(|e| WalletError::SerializationError(e.to_string()))?;
185
186 fs::write(&keyring_path, updated_content)
187 .map_err(|e| WalletError::FileSystemError(e.to_string()))?;
188
189 Ok(true)
190 } else {
191 Ok(false)
192 }
193 }
194
195 pub async fn list_wallets() -> Result<Vec<String>, WalletError> {
197 let keyring_path = Self::get_keyring_path()?;
198
199 if !keyring_path.exists() {
200 return Ok(vec![]);
201 }
202
203 let content = fs::read_to_string(&keyring_path)
204 .map_err(|e| WalletError::FileSystemError(e.to_string()))?;
205
206 let keyring: KeyringData = serde_json::from_str(&content)
207 .map_err(|e| WalletError::SerializationError(e.to_string()))?;
208
209 Ok(keyring.wallets.keys().cloned().collect())
210 }
211
212 pub async fn create_key_ownership_signature(&self, nonce: &str) -> Result<String, WalletError> {
214 let message = format!(
215 "Signing this message to prove ownership of key.\n\nNonce: {}",
216 nonce
217 );
218 let private_synthetic_key = self.get_private_synthetic_key().await?;
219
220 let signature = sign_message(
221 &Bytes::from(message.as_bytes().to_vec()),
222 &private_synthetic_key,
223 )
224 .map_err(|e| WalletError::CryptoError(e.to_string()))?;
225
226 Ok(hex::encode(signature.to_bytes()))
227 }
228
229 pub async fn verify_key_ownership_signature(
231 nonce: &str,
232 signature: &str,
233 public_key: &str,
234 ) -> Result<bool, WalletError> {
235 let message = format!(
236 "Signing this message to prove ownership of key.\n\nNonce: {}",
237 nonce
238 );
239
240 let sig_bytes =
241 hex::decode(signature).map_err(|e| WalletError::CryptoError(e.to_string()))?;
242
243 let pk_bytes =
244 hex::decode(public_key).map_err(|e| WalletError::CryptoError(e.to_string()))?;
245
246 if pk_bytes.len() != 48 {
247 return Err(WalletError::CryptoError(
248 "Invalid public key length".to_string(),
249 ));
250 }
251
252 let mut pk_array = [0u8; 48];
253 pk_array.copy_from_slice(&pk_bytes);
254
255 let public_key = PublicKey::from_bytes(&pk_array)
256 .map_err(|e| WalletError::CryptoError(e.to_string()))?;
257
258 if sig_bytes.len() != 96 {
259 return Err(WalletError::CryptoError(
260 "Invalid signature length".to_string(),
261 ));
262 }
263
264 let mut sig_array = [0u8; 96];
265 sig_array.copy_from_slice(&sig_bytes);
266
267 let signature = Signature::from_bytes(&sig_array)
268 .map_err(|e| WalletError::CryptoError(e.to_string()))?;
269
270 verify_signature(
271 Bytes::from(message.as_bytes().to_vec()),
272 public_key,
273 signature,
274 )
275 .map_err(|e| WalletError::CryptoError(e.to_string()))
276 }
277
278 pub async fn get_all_unspent_dig_coins(
281 &self,
282 peer: &Peer,
283 omit_coins: Vec<Coin>,
284 verbose: bool,
285 ) -> Result<Vec<Coin>, WalletError> {
286 let p2 = self.get_owner_puzzle_hash().await?;
287 let dig_cat_ph = CatArgs::curry_tree_hash(*DIG_COIN_ASSET_ID, TreeHash::from(p2));
288 let dig_cat_ph_bytes = Bytes32::from(dig_cat_ph.to_bytes());
289
290 let unspent_coin_states = datalayer_driver::async_api::get_all_unspent_coins(
292 peer,
293 dig_cat_ph_bytes,
294 None, datalayer_driver::constants::get_mainnet_genesis_challenge(), )
297 .await
298 .map_err(|e| WalletError::NetworkError(format!("Failed to get unspent coins: {}", e)))?;
299
300 let omit_coin_ids: Vec<Bytes32> = omit_coins.iter().map(get_coin_id).collect();
302
303 let available_coin_states: Vec<CoinState> = unspent_coin_states
304 .coin_states
305 .into_iter()
306 .filter(|coin_state| !omit_coin_ids.contains(&get_coin_id(&coin_state.coin)))
307 .collect();
308
309 let mut proved_dig_token_coins: Vec<Coin> = vec![];
310
311 let mut allocator = Allocator::new();
312
313 for coin_state in &available_coin_states {
314 let coin = &coin_state.coin;
315 let coin_id = coin.coin_id();
316 let coin_created_height = match coin_state.created_height {
317 Some(height) => height,
318 None => {
319 if verbose {
320 eprintln!(
321 "ERROR: coin_id {} | {}",
322 coin_id,
323 WalletError::CoinSetError(
324 "Cannot determine coin creation height".to_string()
325 )
326 );
327 }
328 continue;
329 }
330 };
331
332 let parent_state_result = peer
334 .request_coin_state(
335 vec![coin.parent_coin_info],
336 None,
337 MAINNET_CONSTANTS.genesis_challenge,
338 false,
339 )
340 .await;
341
342 let parent_state_response = match parent_state_result {
343 Ok(response) => response,
344 Err(error) => {
345 if verbose {
346 eprintln!(
347 "ERROR: coin_id {} | {}",
348 coin_id,
349 WalletError::NetworkError(format!(
350 "Failed to get coin state: {}",
351 error
352 ))
353 );
354 }
355 continue;
356 }
357 };
358
359 let parent_state = match parent_state_response {
360 Ok(state) => state,
361 Err(_) => {
362 if verbose {
363 eprintln!(
364 "ERROR: coin_id {} | {}",
365 coin_id,
366 WalletError::CoinSetError("Coin state rejected".to_string())
367 );
368 }
369 continue;
370 }
371 };
372
373 let parent_puzzle_and_solution_result = peer
375 .request_puzzle_and_solution(parent_state.coin_ids[0], coin_created_height)
376 .await;
377
378 let parent_puzzle_and_solution_response = match parent_puzzle_and_solution_result {
379 Ok(response) => response,
380 Err(error) => {
381 if verbose {
382 eprintln!(
383 "ERROR: coin_id {} | {}",
384 coin_id,
385 WalletError::NetworkError(format!(
386 "Failed to get puzzle and solution: {}",
387 error
388 ))
389 );
390 }
391 continue;
392 }
393 };
394
395 let parent_puzzle_and_solution = match parent_puzzle_and_solution_response {
396 Ok(v) => v,
397 Err(_) => {
398 if verbose {
399 eprintln!(
400 "ERROR: coin_id {} | {}",
401 coin_id,
402 WalletError::CoinSetError(
403 "Parent puzzle solution rejected".to_string()
404 )
405 );
406 }
407 continue;
408 }
409 };
410
411 let parent_puzzle_ptr = match parent_puzzle_and_solution.puzzle.to_clvm(&mut allocator)
413 {
414 Ok(ptr) => ptr,
415 Err(error) => {
416 if verbose {
417 eprintln!(
418 "ERROR: coin_id {} | {}",
419 coin_id,
420 WalletError::CoinSetError(format!(
421 "Failed to parse puzzle and solution: {}",
422 error
423 ))
424 );
425 }
426 continue;
427 }
428 };
429
430 let parent_puzzle = Puzzle::parse(&allocator, parent_puzzle_ptr);
431
432 let parent_solution = match parent_puzzle_and_solution.solution.to_clvm(&mut allocator)
434 {
435 Ok(solution) => solution,
436 Err(error) => {
437 if verbose {
438 eprintln!(
439 "ERROR: coin_id {} | {}",
440 coin_id,
441 WalletError::CoinSetError(format!(
442 "Failed to parse puzzle and solution: {}",
443 error
444 ))
445 );
446 }
447 continue;
448 }
449 };
450
451 let cat_parse_result = Cat::parse_children(
453 &mut allocator,
454 parent_state.coin_states[0].coin,
455 parent_puzzle,
456 parent_solution,
457 );
458 match cat_parse_result {
459 Ok(_) => {
460 proved_dig_token_coins.push(*coin);
462 }
463 Err(error) => {
464 if verbose {
465 eprintln!(
466 "ERROR: coin_id {} | {}",
467 coin_id,
468 WalletError::CoinSetError(format!(
469 "Failed to parse CAT and prove lineage: {}",
470 error
471 ))
472 );
473 }
474 continue;
475 }
476 }
477 }
478
479 Ok(proved_dig_token_coins)
480 }
481
482 pub async fn select_unspent_dig_token_coins(
483 &self,
484 peer: &Peer,
485 coin_amount: u64,
486 fee: u64,
487 omit_coins: Vec<Coin>,
488 verbose: bool,
489 ) -> Result<Vec<Coin>, WalletError> {
490 let total_needed = coin_amount + fee;
491 let available_dig_coins = self
492 .get_all_unspent_dig_coins(peer, omit_coins, verbose)
493 .await?;
494
495 let selected_coins = datalayer_driver::select_coins(&available_dig_coins, total_needed)
497 .map_err(|e| WalletError::DataLayerError(format!("Coin selection failed: {}", e)))?;
498
499 if selected_coins.is_empty() {
500 return Err(WalletError::NoUnspentCoins);
501 }
502
503 Ok(selected_coins)
504 }
505
506 pub async fn get_dig_balance(&self, peer: &Peer, verbose: bool) -> Result<u64, WalletError> {
507 let dig_coins = self
508 .get_all_unspent_dig_coins(peer, vec![], verbose)
509 .await?;
510 let dig_balance = dig_coins.iter().map(|c| c.amount).sum::<u64>();
511 Ok(dig_balance)
512 }
513
514 pub async fn get_all_unspent_xch_coins(
515 &self,
516 peer: &Peer,
517 omit_coins: Vec<Coin>,
518 ) -> Result<Vec<Coin>, WalletError> {
519 let owner_puzzle_hash = self.get_owner_puzzle_hash().await?;
520
521 let coin_states = datalayer_driver::async_api::get_all_unspent_coins(
522 peer,
523 owner_puzzle_hash,
524 None, datalayer_driver::constants::get_mainnet_genesis_challenge(), )
527 .await
528 .map_err(|e| WalletError::NetworkError(format!("Failed to get unspent coins: {}", e)))?;
529
530 let omit_coin_ids: Vec<Bytes32> = omit_coins.iter().map(get_coin_id).collect();
532
533 Ok(coin_states
534 .coin_states
535 .into_iter()
536 .map(|cs| cs.coin)
537 .filter(|coin| !omit_coin_ids.contains(&get_coin_id(coin)))
538 .collect())
539 }
540
541 pub async fn select_unspent_coins(
543 &self,
544 peer: &Peer,
545 coin_amount: u64,
546 fee: u64,
547 omit_coins: Vec<Coin>,
548 ) -> Result<Vec<Coin>, WalletError> {
549 let total_needed = coin_amount + fee;
550
551 let available_coins = self.get_all_unspent_xch_coins(peer, omit_coins).await?;
552
553 let selected_coins = datalayer_driver::select_coins(&available_coins, total_needed)
555 .map_err(|e| WalletError::DataLayerError(format!("Coin selection failed: {}", e)))?;
556
557 if selected_coins.is_empty() {
558 return Err(WalletError::NoUnspentCoins);
559 }
560
561 Ok(selected_coins)
562 }
563
564 pub async fn get_xch_balance(&self, peer: &Peer) -> Result<u64, WalletError> {
565 let xch_coins = self.get_all_unspent_xch_coins(peer, vec![]).await?;
566 let xch_balance = xch_coins.iter().map(|c| c.amount).sum::<u64>();
567 Ok(xch_balance)
568 }
569
570 pub async fn calculate_fee_for_coin_spends(
572 _peer: &Peer,
573 _coin_spends: Option<&[CoinSpend]>,
574 ) -> Result<u64, WalletError> {
575 Ok(1_000_000) }
578
579 pub async fn is_coin_spendable(peer: &Peer, coin_id: &Bytes32) -> Result<bool, WalletError> {
581 let is_spent = datalayer_driver::is_coin_spent(
583 peer,
584 *coin_id,
585 None, datalayer_driver::constants::get_mainnet_genesis_challenge(), )
588 .await
589 .map_err(|e| WalletError::NetworkError(format!("Failed to check coin status: {}", e)))?;
590
591 Ok(!is_spent)
593 }
594
595 pub async fn connect_random_peer(
597 network: NetworkType,
598 cert_path: &str,
599 key_path: &str,
600 ) -> Result<Peer, WalletError> {
601 connect_random(network, cert_path, key_path)
602 .await
603 .map_err(|e| WalletError::NetworkError(format!("Failed to connect to peer: {}", e)))
604 }
605
606 pub async fn connect_mainnet_peer() -> Result<Peer, WalletError> {
608 let home_dir = dirs::home_dir().ok_or_else(|| {
609 WalletError::FileSystemError("Could not find home directory".to_string())
610 })?;
611
612 let ssl_dir = home_dir
613 .join(".chia")
614 .join("mainnet")
615 .join("config")
616 .join("ssl")
617 .join("wallet");
618 let cert_path = ssl_dir.join("wallet_node.crt");
619 let key_path = ssl_dir.join("wallet_node.key");
620
621 Self::connect_random_peer(
622 NetworkType::Mainnet,
623 cert_path
624 .to_str()
625 .ok_or_else(|| WalletError::FileSystemError("Invalid cert path".to_string()))?,
626 key_path
627 .to_str()
628 .ok_or_else(|| WalletError::FileSystemError("Invalid key path".to_string()))?,
629 )
630 .await
631 }
632
633 pub async fn connect_testnet_peer() -> Result<Peer, WalletError> {
635 let home_dir = dirs::home_dir().ok_or_else(|| {
636 WalletError::FileSystemError("Could not find home directory".to_string())
637 })?;
638
639 let ssl_dir = home_dir
640 .join(".chia")
641 .join("testnet11")
642 .join("config")
643 .join("ssl")
644 .join("wallet");
645 let cert_path = ssl_dir.join("wallet_node.crt");
646 let key_path = ssl_dir.join("wallet_node.key");
647
648 Self::connect_random_peer(
649 NetworkType::Testnet11,
650 cert_path
651 .to_str()
652 .ok_or_else(|| WalletError::FileSystemError("Invalid cert path".to_string()))?,
653 key_path
654 .to_str()
655 .ok_or_else(|| WalletError::FileSystemError("Invalid key path".to_string()))?,
656 )
657 .await
658 }
659
660 pub fn address_to_puzzle_hash(address: &str) -> Result<Bytes32, WalletError> {
662 address_to_puzzle_hash(address)
663 .map_err(|e| WalletError::CryptoError(format!("Failed to decode address: {}", e)))
664 }
665
666 pub fn puzzle_hash_to_address(
668 puzzle_hash: Bytes32,
669 prefix: &str,
670 ) -> Result<String, WalletError> {
671 puzzle_hash_to_address(puzzle_hash, prefix)
672 .map_err(|e| WalletError::CryptoError(format!("Failed to encode address: {}", e)))
673 }
674
675 async fn get_wallet_from_keyring(wallet_name: &str) -> Result<Option<String>, WalletError> {
678 let keyring_path = Self::get_keyring_path()?;
679
680 if !keyring_path.exists() {
681 return Ok(None);
682 }
683
684 let content = fs::read_to_string(&keyring_path)
685 .map_err(|e| WalletError::FileSystemError(e.to_string()))?;
686
687 let keyring: KeyringData = serde_json::from_str(&content)
688 .map_err(|e| WalletError::SerializationError(e.to_string()))?;
689
690 if let Some(encrypted_data) = keyring.wallets.get(wallet_name) {
691 let decrypted = Self::decrypt_data(encrypted_data)?;
692 Ok(Some(decrypted))
693 } else {
694 Ok(None)
695 }
696 }
697
698 async fn save_wallet_to_keyring(wallet_name: &str, mnemonic: &str) -> Result<(), WalletError> {
699 let keyring_path = Self::get_keyring_path()?;
700
701 if let Some(parent) = keyring_path.parent() {
703 fs::create_dir_all(parent).map_err(|e| WalletError::FileSystemError(e.to_string()))?;
704 }
705
706 let mut keyring = if keyring_path.exists() {
707 let content = fs::read_to_string(&keyring_path)
708 .map_err(|e| WalletError::FileSystemError(e.to_string()))?;
709 serde_json::from_str(&content)
710 .map_err(|e| WalletError::SerializationError(e.to_string()))?
711 } else {
712 KeyringData {
713 wallets: HashMap::new(),
714 }
715 };
716
717 let encrypted_data = Self::encrypt_data(mnemonic)?;
718
719 keyring
720 .wallets
721 .insert(wallet_name.to_string(), encrypted_data);
722
723 let content = serde_json::to_string_pretty(&keyring)
724 .map_err(|e| WalletError::SerializationError(e.to_string()))?;
725
726 fs::write(&keyring_path, content)
727 .map_err(|e| WalletError::FileSystemError(e.to_string()))?;
728
729 Ok(())
730 }
731
732 fn get_keyring_path() -> Result<PathBuf, WalletError> {
733 if let Ok(test_path) = env::var("TEST_KEYRING_PATH") {
735 return Ok(PathBuf::from(test_path));
736 }
737
738 let home_dir = dirs::home_dir().ok_or_else(|| {
739 WalletError::FileSystemError("Could not find home directory".to_string())
740 })?;
741
742 Ok(home_dir.join(".dig").join(KEYRING_FILE))
743 }
744
745 fn encrypt_data(data: &str) -> Result<EncryptedData, WalletError> {
747 let salt = rand::random::<[u8; 16]>();
749
750 let mut key_bytes = [0u8; 32];
753 let password = b"mnemonic-seed"; for i in 0..32 {
757 key_bytes[i] = password[i % password.len()] ^ salt[i % salt.len()];
758 }
759
760 let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
761 let cipher = Aes256Gcm::new(key);
762
763 let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
765
766 let ciphertext = cipher
768 .encrypt(&nonce, data.as_bytes())
769 .map_err(|e| WalletError::CryptoError(format!("Encryption failed: {}", e)))?;
770
771 Ok(EncryptedData {
772 data: general_purpose::STANDARD.encode(&ciphertext),
773 nonce: general_purpose::STANDARD.encode(nonce),
774 salt: general_purpose::STANDARD.encode(salt),
775 })
776 }
777
778 fn decrypt_data(encrypted_data: &EncryptedData) -> Result<String, WalletError> {
780 let ciphertext = general_purpose::STANDARD
781 .decode(&encrypted_data.data)
782 .map_err(|e| WalletError::CryptoError(format!("Failed to decode ciphertext: {}", e)))?;
783
784 let nonce_bytes = general_purpose::STANDARD
785 .decode(&encrypted_data.nonce)
786 .map_err(|e| WalletError::CryptoError(format!("Failed to decode nonce: {}", e)))?;
787
788 let salt = general_purpose::STANDARD
789 .decode(&encrypted_data.salt)
790 .map_err(|e| WalletError::CryptoError(format!("Failed to decode salt: {}", e)))?;
791
792 let mut key_bytes = [0u8; 32];
794 let password = b"mnemonic-seed";
795
796 for i in 0..32 {
797 key_bytes[i] = password[i % password.len()] ^ salt[i % salt.len()];
798 }
799
800 let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
801 let cipher = Aes256Gcm::new(key);
802
803 let nonce = Nonce::from_slice(&nonce_bytes);
804
805 let plaintext = cipher
807 .decrypt(nonce, ciphertext.as_ref())
808 .map_err(|e| WalletError::CryptoError(format!("Decryption failed: {}", e)))?;
809
810 String::from_utf8(plaintext).map_err(|e| {
811 WalletError::CryptoError(format!("Failed to convert decrypted data to string: {}", e))
812 })
813 }
814}
815
816#[cfg(test)]
817mod tests {
818 use super::*;
819 use std::env;
820 use tempfile::TempDir;
821
822 fn setup_test_env() -> TempDir {
824 let temp_dir = TempDir::new().unwrap();
825
826 let keyring_path = temp_dir.path().join("test_keyring.json");
828 env::set_var(
829 "TEST_KEYRING_PATH",
830 keyring_path.to_string_lossy().to_string(),
831 );
832
833 env::set_var("HOME", temp_dir.path());
835
836 temp_dir
837 }
838
839 #[tokio::test]
840 async fn test_wallet_creation() {
841 let _temp_dir = setup_test_env();
842
843 let mnemonic = Wallet::create_new_wallet("test_wallet").await.unwrap();
845
846 assert!(bip39::Mnemonic::parse_in_normalized(Language::English, &mnemonic).is_ok());
848
849 assert_eq!(mnemonic.split_whitespace().count(), 24);
851
852 let wallets = Wallet::list_wallets().await.unwrap();
854 assert!(wallets.contains(&"test_wallet".to_string()));
855 }
856
857 #[tokio::test]
858 async fn test_wallet_import() {
859 let _temp_dir = setup_test_env();
860
861 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";
863
864 let imported_mnemonic = Wallet::import_wallet("imported_wallet", Some(test_mnemonic))
866 .await
867 .unwrap();
868
869 assert_eq!(imported_mnemonic, test_mnemonic);
871
872 let wallet = Wallet::load(Some("imported_wallet".to_string()), false)
874 .await
875 .unwrap();
876 assert_eq!(wallet.get_mnemonic().unwrap(), test_mnemonic);
877 }
878
879 #[tokio::test]
880 async fn test_wallet_import_invalid_mnemonic() {
881 let _temp_dir = setup_test_env();
882
883 let invalid_mnemonic = "invalid mnemonic phrase that should fail validation";
885
886 let result = Wallet::import_wallet("invalid_wallet", Some(invalid_mnemonic)).await;
888 assert!(matches!(result, Err(WalletError::InvalidMnemonic)));
889 }
890
891 #[tokio::test]
892 async fn test_wallet_load_nonexistent() {
893 let _temp_dir = setup_test_env();
894
895 let result = Wallet::load(Some("nonexistent".to_string()), false).await;
897 assert!(matches!(result, Err(WalletError::WalletNotFound(_))));
898 }
899
900 #[tokio::test]
901 async fn test_wallet_load_with_creation() {
902 let _temp_dir = setup_test_env();
903
904 let wallet = Wallet::load(Some("auto_created".to_string()), true)
906 .await
907 .unwrap();
908
909 let mnemonic = wallet.get_mnemonic().unwrap();
911 assert!(bip39::Mnemonic::parse_in_normalized(Language::English, mnemonic).is_ok());
912
913 assert_eq!(wallet.get_wallet_name(), "auto_created");
915 }
916
917 #[tokio::test]
918 async fn test_key_derivation() {
919 let _temp_dir = setup_test_env();
920
921 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";
923
924 Wallet::import_wallet("key_test", Some(test_mnemonic))
925 .await
926 .unwrap();
927 let wallet = Wallet::load(Some("key_test".to_string()), false)
928 .await
929 .unwrap();
930
931 let master_sk = wallet.get_master_secret_key().await.unwrap();
933 let public_synthetic_key = wallet.get_public_synthetic_key().await.unwrap();
934 let private_synthetic_key = wallet.get_private_synthetic_key().await.unwrap();
935 let puzzle_hash = wallet.get_owner_puzzle_hash().await.unwrap();
936
937 assert_eq!(
939 secret_key_to_public_key(&private_synthetic_key),
940 public_synthetic_key
941 );
942
943 assert_eq!(puzzle_hash.as_ref().len(), 32);
945
946 let wallet2 = Wallet::load(Some("key_test".to_string()), false)
948 .await
949 .unwrap();
950 let master_sk2 = wallet2.get_master_secret_key().await.unwrap();
951 assert_eq!(master_sk.to_bytes(), master_sk2.to_bytes());
952 }
953
954 #[tokio::test]
955 async fn test_address_generation() {
956 let _temp_dir = setup_test_env();
957
958 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";
959
960 Wallet::import_wallet("address_test", Some(test_mnemonic))
961 .await
962 .unwrap();
963 let wallet = Wallet::load(Some("address_test".to_string()), false)
964 .await
965 .unwrap();
966
967 let address = wallet.get_owner_public_key().await.unwrap();
969
970 assert!(address.starts_with("xch1"));
972
973 assert!(address.len() >= 60 && address.len() <= 65);
975
976 let puzzle_hash = Wallet::address_to_puzzle_hash(&address).unwrap();
978 let converted_address = Wallet::puzzle_hash_to_address(puzzle_hash, "xch").unwrap();
979 assert_eq!(address, converted_address);
980 }
981
982 #[tokio::test]
983 async fn test_signature_creation_and_verification() {
984 let _temp_dir = setup_test_env();
985
986 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";
987
988 Wallet::import_wallet("sig_test", Some(test_mnemonic))
989 .await
990 .unwrap();
991 let wallet = Wallet::load(Some("sig_test".to_string()), false)
992 .await
993 .unwrap();
994
995 let nonce = "test_nonce_12345";
997 let signature = wallet.create_key_ownership_signature(nonce).await.unwrap();
998
999 assert!(hex::decode(&signature).is_ok());
1001
1002 let public_key = wallet.get_public_synthetic_key().await.unwrap();
1004 let public_key_hex = hex::encode(public_key.to_bytes());
1005
1006 let is_valid = Wallet::verify_key_ownership_signature(nonce, &signature, &public_key_hex)
1008 .await
1009 .unwrap();
1010 assert!(is_valid);
1011
1012 let is_valid_wrong =
1014 Wallet::verify_key_ownership_signature("wrong_nonce", &signature, &public_key_hex)
1015 .await
1016 .unwrap();
1017 assert!(!is_valid_wrong);
1018 }
1019
1020 #[tokio::test]
1021 async fn test_wallet_deletion() {
1022 let _temp_dir = setup_test_env();
1023
1024 Wallet::create_new_wallet("delete_test").await.unwrap();
1026
1027 let wallets_before = Wallet::list_wallets().await.unwrap();
1029 assert!(wallets_before.contains(&"delete_test".to_string()));
1030
1031 let deleted = Wallet::delete_wallet("delete_test").await.unwrap();
1033 assert!(deleted);
1034
1035 let wallets_after = Wallet::list_wallets().await.unwrap();
1037 assert!(!wallets_after.contains(&"delete_test".to_string()));
1038
1039 let not_deleted = Wallet::delete_wallet("nonexistent").await.unwrap();
1041 assert!(!not_deleted);
1042 }
1043
1044 #[tokio::test]
1045 async fn test_multiple_wallets() {
1046 let _temp_dir = setup_test_env();
1047
1048 Wallet::create_new_wallet("wallet1").await.unwrap();
1050 Wallet::create_new_wallet("wallet2").await.unwrap();
1051 Wallet::create_new_wallet("wallet3").await.unwrap();
1052
1053 let mut wallets = Wallet::list_wallets().await.unwrap();
1055 wallets.sort(); assert_eq!(wallets.len(), 3);
1058 assert!(wallets.contains(&"wallet1".to_string()));
1059 assert!(wallets.contains(&"wallet2".to_string()));
1060 assert!(wallets.contains(&"wallet3".to_string()));
1061
1062 let w1 = Wallet::load(Some("wallet1".to_string()), false)
1064 .await
1065 .unwrap();
1066 let w2 = Wallet::load(Some("wallet2".to_string()), false)
1067 .await
1068 .unwrap();
1069 let w3 = Wallet::load(Some("wallet3".to_string()), false)
1070 .await
1071 .unwrap();
1072
1073 assert_ne!(w1.get_mnemonic().unwrap(), w2.get_mnemonic().unwrap());
1074 assert_ne!(w2.get_mnemonic().unwrap(), w3.get_mnemonic().unwrap());
1075 assert_ne!(w1.get_mnemonic().unwrap(), w3.get_mnemonic().unwrap());
1076 }
1077
1078 #[tokio::test]
1079 async fn test_encryption_decryption() {
1080 let test_data = "test mnemonic phrase for encryption";
1082
1083 let encrypted = Wallet::encrypt_data(test_data).unwrap();
1084
1085 assert_ne!(encrypted.data, test_data);
1087 assert!(!encrypted.nonce.is_empty());
1088 assert!(!encrypted.salt.is_empty());
1089
1090 let decrypted = Wallet::decrypt_data(&encrypted).unwrap();
1092 assert_eq!(decrypted, test_data);
1093 }
1094
1095 #[tokio::test]
1096 async fn test_encryption_with_different_salts() {
1097 let test_data = "same data";
1098
1099 let encrypted1 = Wallet::encrypt_data(test_data).unwrap();
1101 let encrypted2 = Wallet::encrypt_data(test_data).unwrap();
1102
1103 assert_ne!(encrypted1.data, encrypted2.data);
1105 assert_ne!(encrypted1.salt, encrypted2.salt);
1106 assert_ne!(encrypted1.nonce, encrypted2.nonce);
1107
1108 let decrypted1 = Wallet::decrypt_data(&encrypted1).unwrap();
1110 let decrypted2 = Wallet::decrypt_data(&encrypted2).unwrap();
1111 assert_eq!(decrypted1, test_data);
1112 assert_eq!(decrypted2, test_data);
1113 }
1114
1115 #[tokio::test]
1116 async fn test_invalid_signature_verification() {
1117 let _temp_dir = setup_test_env();
1118
1119 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";
1121 Wallet::import_wallet("invalid_sig_test", Some(test_mnemonic))
1122 .await
1123 .unwrap();
1124 let wallet = Wallet::load(Some("invalid_sig_test".to_string()), false)
1125 .await
1126 .unwrap();
1127
1128 let public_key = wallet.get_public_synthetic_key().await.unwrap();
1129 let public_key_hex = hex::encode(public_key.to_bytes());
1130
1131 let result =
1133 Wallet::verify_key_ownership_signature("nonce", "invalid_hex", &public_key_hex).await;
1134 assert!(result.is_err());
1135
1136 let short_sig = "deadbeef";
1138 let result =
1139 Wallet::verify_key_ownership_signature("nonce", short_sig, &public_key_hex).await;
1140 assert!(result.is_err());
1141
1142 let result =
1144 Wallet::verify_key_ownership_signature("nonce", &"a".repeat(192), "invalid_key").await;
1145 assert!(result.is_err());
1146 }
1147
1148 #[tokio::test]
1149 async fn test_address_conversion_errors() {
1150 let result = Wallet::address_to_puzzle_hash("invalid_address");
1152 assert!(result.is_err());
1153
1154 let result = Wallet::address_to_puzzle_hash("");
1156 assert!(result.is_err());
1157 }
1158
1159 #[tokio::test]
1160 async fn test_mnemonic_not_loaded_error() {
1161 let wallet = Wallet::new(None, "empty_wallet".to_string());
1163
1164 let result = wallet.get_mnemonic();
1166 assert!(matches!(result, Err(WalletError::MnemonicNotLoaded)));
1167
1168 let result = wallet.get_master_secret_key().await;
1170 assert!(matches!(result, Err(WalletError::MnemonicNotLoaded)));
1171 }
1172
1173 #[tokio::test]
1174 async fn test_default_wallet_name() {
1175 let _temp_dir = setup_test_env();
1176
1177 let wallet = Wallet::load(None, true).await.unwrap();
1179 assert_eq!(wallet.get_wallet_name(), "default");
1180
1181 let wallets = Wallet::list_wallets().await.unwrap();
1183 assert!(wallets.contains(&"default".to_string()));
1184 }
1185}