dig_wallet/
wallet.rs

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// Cache duration constant - keeping for potential future use
23#[allow(dead_code)]
24const CACHE_DURATION_MS: u64 = 5 * 60 * 1000; // 5 minutes
25pub 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    /// Create a new Wallet instance
47    fn new(mnemonic: Option<String>, wallet_name: String) -> Self {
48        Self {
49            mnemonic,
50            wallet_name,
51        }
52    }
53
54    /// Load a wallet by name, optionally creating one if it doesn't exist
55    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            // In a real implementation, you'd prompt the user for input
67            // For now, we'll generate a new wallet
68            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    /// Get the mnemonic seed phrase
76    pub fn get_mnemonic(&self) -> Result<&str, WalletError> {
77        self.mnemonic
78            .as_deref()
79            .ok_or(WalletError::MnemonicNotLoaded)
80    }
81
82    /// Get the wallet name
83    pub fn get_wallet_name(&self) -> &str {
84        &self.wallet_name
85    }
86
87    /// Create a new wallet with a generated mnemonic
88    pub async fn create_new_wallet(wallet_name: &str) -> Result<String, WalletError> {
89        let entropy = rand::random::<[u8; 32]>(); // 32 bytes = 256 bits for 24 words
90        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    /// Import a wallet from a provided mnemonic
98    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                // In a real implementation, you'd prompt for input
106                return Err(WalletError::MnemonicRequired);
107            }
108        };
109
110        // Validate the mnemonic
111        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    /// Get the master secret key from the mnemonic
119    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    /// Get the public synthetic key
130    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    /// Get the private synthetic key
137    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    /// Get the owner puzzle hash
143    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    /// Get the owner public key as an address
150    pub async fn get_owner_public_key(&self) -> Result<String, WalletError> {
151        let owner_puzzle_hash = self.get_owner_puzzle_hash().await?;
152        // Convert puzzle hash to address (xch format) using DataLayer-Driver
153        puzzle_hash_to_address(owner_puzzle_hash, "xch")
154            .map_err(|e| WalletError::CryptoError(format!("Failed to encode address: {}", e)))
155    }
156
157    /// Delete a wallet from the keyring
158    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    /// List all wallets in the keyring
185    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    /// Create a key ownership signature
202    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    /// Verify a key ownership signature
219    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    /// Get all unspent DIG Token coins
268    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        // Get unspent coin states from the DataLayer-Driver async API
278        let unspent_coin_states = datalayer_driver::async_api::get_all_unspent_coins(
279            peer,
280            dig_ph,
281            None, // previous_height - start from genesis
282            datalayer_driver::constants::get_mainnet_genesis_challenge(), // Use mainnet for now
283        )
284        .await
285        .map_err(|e| WalletError::NetworkError(format!("Failed to get unspent coins: {}", e)))?;
286
287        // Convert coin states to coins and filter out omitted coins
288        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            //Parse CAT to prove lineage
299            let cat_parse_result = DigCoin::from_coin_state(peer, coin_state).await;
300            match cat_parse_result {
301                Ok(parsed_cat) => {
302                    // lineage proved. append coin in question
303                    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        // Use the DataLayer-Driver's select_coins function
341        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, // previous_height - start from genesis
379            datalayer_driver::constants::get_mainnet_genesis_challenge(), // Use mainnet for now
380        )
381        .await
382        .map_err(|e| WalletError::NetworkError(format!("Failed to get unspent coins: {}", e)))?;
383
384        // Convert coin states to coins and filter out omitted coins
385        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    /// Select unspent coins for spending
396    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        // Use the DataLayer-Driver's select_coins function
408        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    /// Calculate fee for coin spends
425    pub async fn calculate_fee_for_coin_spends(
426        _peer: &Peer,
427        _coin_spends: Option<&[CoinSpend]>,
428    ) -> Result<u64, WalletError> {
429        // Simplified fee calculation - in practice this would be more complex
430        Ok(1_000_000) // 1 million mojos
431    }
432
433    /// Check if a coin is spendable
434    pub async fn is_coin_spendable(peer: &Peer, coin_id: &Bytes32) -> Result<bool, WalletError> {
435        // Check if coin is spent using the DataLayer-Driver API
436        let is_spent = datalayer_driver::is_coin_spent(
437            peer,
438            *coin_id,
439            None,                                                         // last_height
440            datalayer_driver::constants::get_mainnet_genesis_challenge(), // Use mainnet for now
441        )
442        .await
443        .map_err(|e| WalletError::NetworkError(format!("Failed to check coin status: {}", e)))?;
444
445        // Return true if coin is NOT spent (i.e., is spendable)
446        Ok(!is_spent)
447    }
448
449    /// Connect to a random peer on the specified network
450    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    /// Connect to a random mainnet peer using default Chia SSL paths
461    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    /// Connect to a random testnet peer using default Chia SSL paths
488    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    /// Convert an address to a puzzle hash
515    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    /// Convert a puzzle hash to an address
521    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    // Private helper methods
530
531    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        // Ensure the directory exists
556        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        // Check if we're in test mode by looking for TEST_KEYRING_PATH env var
588        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    /// Encrypt data using AES-256-GCM
600    fn encrypt_data(data: &str) -> Result<EncryptedData, WalletError> {
601        // Generate a random salt
602        let salt = rand::random::<[u8; 16]>();
603
604        // Derive key from a fixed password and salt using a simple method
605        // In production, you'd want to use a proper key derivation function like PBKDF2
606        let mut key_bytes = [0u8; 32];
607        let password = b"mnemonic-seed"; // This should be derived from user input in practice
608
609        // Simple key derivation (not cryptographically secure - use PBKDF2 in production)
610        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        // Generate a random nonce
618        let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
619
620        // Encrypt the data
621        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    /// Decrypt data using AES-256-GCM
633    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        // Derive the same key using the salt
647        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        // Decrypt the data
660        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    // Test helper to set up a temporary directory for tests
677    fn setup_test_env() -> TempDir {
678        let temp_dir = TempDir::new().unwrap();
679
680        // Set up isolated keyring path for this test
681        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        // Also set HOME for any other path operations
688        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        // Create a new wallet
698        let mnemonic = Wallet::create_new_wallet("test_wallet").await.unwrap();
699
700        // Verify mnemonic is valid BIP39
701        assert!(bip39::Mnemonic::parse_in_normalized(Language::English, &mnemonic).is_ok());
702
703        // Verify mnemonic has 24 words
704        assert_eq!(mnemonic.split_whitespace().count(), 24);
705
706        // Verify wallet appears in list
707        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        // Known valid 24-word mnemonic
716        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        // Import the wallet
719        let imported_mnemonic = Wallet::import_wallet("imported_wallet", Some(test_mnemonic))
720            .await
721            .unwrap();
722
723        // Verify the mnemonic matches
724        assert_eq!(imported_mnemonic, test_mnemonic);
725
726        // Load the wallet and verify mnemonic
727        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        // Invalid mnemonic
738        let invalid_mnemonic = "invalid mnemonic phrase that should fail validation";
739
740        // Should fail with InvalidMnemonic error
741        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        // Try to load non-existent wallet without creating
750        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        // Load wallet with auto-creation
759        let wallet = Wallet::load(Some("auto_created".to_string()), true)
760            .await
761            .unwrap();
762
763        // Verify wallet was created and has valid mnemonic
764        let mnemonic = wallet.get_mnemonic().unwrap();
765        assert!(bip39::Mnemonic::parse_in_normalized(Language::English, mnemonic).is_ok());
766
767        // Verify wallet name
768        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        // Use known mnemonic for deterministic testing
776        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        // Test key derivation
786        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        // Verify keys are consistent
792        assert_eq!(
793            secret_key_to_public_key(&private_synthetic_key),
794            public_synthetic_key
795        );
796
797        // Verify puzzle hash is 32 bytes
798        assert_eq!(puzzle_hash.as_ref().len(), 32);
799
800        // Test that keys are deterministic (same mnemonic = same keys)
801        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        // Generate address
822        let address = wallet.get_owner_public_key().await.unwrap();
823
824        // Verify address format (should start with "xch1")
825        assert!(address.starts_with("xch1"));
826
827        // Verify address length (Chia addresses are typically 62 characters)
828        assert!(address.len() >= 60 && address.len() <= 65);
829
830        // Test address conversion roundtrip
831        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        // Create signature
850        let nonce = "test_nonce_12345";
851        let signature = wallet.create_key_ownership_signature(nonce).await.unwrap();
852
853        // Verify signature format (should be hex string)
854        assert!(hex::decode(&signature).is_ok());
855
856        // Get public key for verification
857        let public_key = wallet.get_public_synthetic_key().await.unwrap();
858        let public_key_hex = hex::encode(public_key.to_bytes());
859
860        // Verify signature
861        let is_valid = Wallet::verify_key_ownership_signature(nonce, &signature, &public_key_hex)
862            .await
863            .unwrap();
864        assert!(is_valid);
865
866        // Test with wrong nonce (should fail)
867        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        // Create wallet
879        Wallet::create_new_wallet("delete_test").await.unwrap();
880
881        // Verify it exists
882        let wallets_before = Wallet::list_wallets().await.unwrap();
883        assert!(wallets_before.contains(&"delete_test".to_string()));
884
885        // Delete wallet
886        let deleted = Wallet::delete_wallet("delete_test").await.unwrap();
887        assert!(deleted);
888
889        // Verify it's gone
890        let wallets_after = Wallet::list_wallets().await.unwrap();
891        assert!(!wallets_after.contains(&"delete_test".to_string()));
892
893        // Try to delete non-existent wallet
894        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        // Create multiple wallets
903        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        // List wallets
908        let mut wallets = Wallet::list_wallets().await.unwrap();
909        wallets.sort(); // Sort for consistent testing
910
911        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        // Load each wallet and verify they have different mnemonics
917        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        // Test encryption/decryption directly
935        let test_data = "test mnemonic phrase for encryption";
936
937        let encrypted = Wallet::encrypt_data(test_data).unwrap();
938
939        // Verify encrypted data is different from original
940        assert_ne!(encrypted.data, test_data);
941        assert!(!encrypted.nonce.is_empty());
942        assert!(!encrypted.salt.is_empty());
943
944        // Decrypt and verify
945        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        // Encrypt same data twice
954        let encrypted1 = Wallet::encrypt_data(test_data).unwrap();
955        let encrypted2 = Wallet::encrypt_data(test_data).unwrap();
956
957        // Should produce different ciphertexts due to random salt/nonce
958        assert_ne!(encrypted1.data, encrypted2.data);
959        assert_ne!(encrypted1.salt, encrypted2.salt);
960        assert_ne!(encrypted1.nonce, encrypted2.nonce);
961
962        // But both should decrypt to same data
963        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        // Create wallet
974        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        // Test with invalid signature format
986        let result =
987            Wallet::verify_key_ownership_signature("nonce", "invalid_hex", &public_key_hex).await;
988        assert!(result.is_err());
989
990        // Test with wrong signature length
991        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        // Test with invalid public key
997        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        // Test invalid address
1005        let result = Wallet::address_to_puzzle_hash("invalid_address");
1006        assert!(result.is_err());
1007
1008        // Test empty address
1009        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        // Create wallet without mnemonic
1016        let wallet = Wallet::new(None, "empty_wallet".to_string());
1017
1018        // Should fail when trying to get mnemonic
1019        let result = wallet.get_mnemonic();
1020        assert!(matches!(result, Err(WalletError::MnemonicNotLoaded)));
1021
1022        // Should fail when trying to derive keys
1023        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        // Load wallet without specifying name (should use "default")
1032        let wallet = Wallet::load(None, true).await.unwrap();
1033        assert_eq!(wallet.get_wallet_name(), "default");
1034
1035        // Verify it appears in wallet list
1036        let wallets = Wallet::list_wallets().await.unwrap();
1037        assert!(wallets.contains(&"default".to_string()));
1038    }
1039}