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 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// Cache duration constant - keeping for potential future use
35#[allow(dead_code)]
36const CACHE_DURATION_MS: u64 = 5 * 60 * 1000; // 5 minutes
37pub 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    /// Create a new Wallet instance
58    fn new(mnemonic: Option<String>, wallet_name: String) -> Self {
59        Self {
60            mnemonic,
61            wallet_name,
62        }
63    }
64
65    /// Load a wallet by name, optionally creating one if it doesn't exist
66    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            // In a real implementation, you'd prompt the user for input
78            // For now, we'll generate a new wallet
79            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    /// Get the mnemonic seed phrase
87    pub fn get_mnemonic(&self) -> Result<&str, WalletError> {
88        self.mnemonic
89            .as_deref()
90            .ok_or(WalletError::MnemonicNotLoaded)
91    }
92
93    /// Get the wallet name
94    pub fn get_wallet_name(&self) -> &str {
95        &self.wallet_name
96    }
97
98    /// Create a new wallet with a generated mnemonic
99    pub async fn create_new_wallet(wallet_name: &str) -> Result<String, WalletError> {
100        let entropy = rand::random::<[u8; 32]>(); // 32 bytes = 256 bits for 24 words
101        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    /// Import a wallet from a provided mnemonic
109    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                // In a real implementation, you'd prompt for input
117                return Err(WalletError::MnemonicRequired);
118            }
119        };
120
121        // Validate the mnemonic
122        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    /// Get the master secret key from the mnemonic
130    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    /// Get the public synthetic key
141    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    /// Get the private synthetic key
148    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    /// Get the owner puzzle hash
154    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    /// Get the owner public key as an address
161    pub async fn get_owner_public_key(&self) -> Result<String, WalletError> {
162        let owner_puzzle_hash = self.get_owner_puzzle_hash().await?;
163        // Convert puzzle hash to address (xch format) using DataLayer-Driver
164        puzzle_hash_to_address(owner_puzzle_hash, "xch")
165            .map_err(|e| WalletError::CryptoError(format!("Failed to encode address: {}", e)))
166    }
167
168    /// Delete a wallet from the keyring
169    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    /// List all wallets in the keyring
196    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    /// Create a key ownership signature
213    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    /// Verify a key ownership signature
230    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    /// Get all unspent DIG Token coins
279    // todo: this should be moved to the driver
280    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        // Get unspent coin states from the DataLayer-Driver async API
291        let unspent_coin_states = datalayer_driver::async_api::get_all_unspent_coins(
292            peer,
293            dig_cat_ph_bytes,
294            None, // previous_height - start from genesis
295            datalayer_driver::constants::get_mainnet_genesis_challenge(), // Use mainnet for now
296        )
297        .await
298        .map_err(|e| WalletError::NetworkError(format!("Failed to get unspent coins: {}", e)))?;
299
300        // Convert coin states to coins and filter out omitted coins
301        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            // 1) Request parent coin state
333            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            // 2) Request parent puzzle and solution
374            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            // 3) Convert puzzle to CLVM
412            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            // 4) Convert solution to CLVM
433            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            // 5) Parse CAT to prove lineage
452            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                    // lineage proved. append coin in question
461                    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        // Use the DataLayer-Driver's select_coins function
496        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, // previous_height - start from genesis
525            datalayer_driver::constants::get_mainnet_genesis_challenge(), // Use mainnet for now
526        )
527        .await
528        .map_err(|e| WalletError::NetworkError(format!("Failed to get unspent coins: {}", e)))?;
529
530        // Convert coin states to coins and filter out omitted coins
531        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    /// Select unspent coins for spending
542    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        // Use the DataLayer-Driver's select_coins function
554        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    /// Calculate fee for coin spends
571    pub async fn calculate_fee_for_coin_spends(
572        _peer: &Peer,
573        _coin_spends: Option<&[CoinSpend]>,
574    ) -> Result<u64, WalletError> {
575        // Simplified fee calculation - in practice this would be more complex
576        Ok(1_000_000) // 1 million mojos
577    }
578
579    /// Check if a coin is spendable
580    pub async fn is_coin_spendable(peer: &Peer, coin_id: &Bytes32) -> Result<bool, WalletError> {
581        // Check if coin is spent using the DataLayer-Driver API
582        let is_spent = datalayer_driver::is_coin_spent(
583            peer,
584            *coin_id,
585            None,                                                         // last_height
586            datalayer_driver::constants::get_mainnet_genesis_challenge(), // Use mainnet for now
587        )
588        .await
589        .map_err(|e| WalletError::NetworkError(format!("Failed to check coin status: {}", e)))?;
590
591        // Return true if coin is NOT spent (i.e., is spendable)
592        Ok(!is_spent)
593    }
594
595    /// Connect to a random peer on the specified network
596    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    /// Connect to a random mainnet peer using default Chia SSL paths
607    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    /// Connect to a random testnet peer using default Chia SSL paths
634    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    /// Convert an address to a puzzle hash
661    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    /// Convert a puzzle hash to an address
667    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    // Private helper methods
676
677    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        // Ensure the directory exists
702        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        // Check if we're in test mode by looking for TEST_KEYRING_PATH env var
734        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    /// Encrypt data using AES-256-GCM
746    fn encrypt_data(data: &str) -> Result<EncryptedData, WalletError> {
747        // Generate a random salt
748        let salt = rand::random::<[u8; 16]>();
749
750        // Derive key from a fixed password and salt using a simple method
751        // In production, you'd want to use a proper key derivation function like PBKDF2
752        let mut key_bytes = [0u8; 32];
753        let password = b"mnemonic-seed"; // This should be derived from user input in practice
754
755        // Simple key derivation (not cryptographically secure - use PBKDF2 in production)
756        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        // Generate a random nonce
764        let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
765
766        // Encrypt the data
767        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    /// Decrypt data using AES-256-GCM
779    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        // Derive the same key using the salt
793        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        // Decrypt the data
806        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    // Test helper to set up a temporary directory for tests
823    fn setup_test_env() -> TempDir {
824        let temp_dir = TempDir::new().unwrap();
825
826        // Set up isolated keyring path for this test
827        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        // Also set HOME for any other path operations
834        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        // Create a new wallet
844        let mnemonic = Wallet::create_new_wallet("test_wallet").await.unwrap();
845
846        // Verify mnemonic is valid BIP39
847        assert!(bip39::Mnemonic::parse_in_normalized(Language::English, &mnemonic).is_ok());
848
849        // Verify mnemonic has 24 words
850        assert_eq!(mnemonic.split_whitespace().count(), 24);
851
852        // Verify wallet appears in list
853        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        // Known valid 24-word mnemonic
862        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        // Import the wallet
865        let imported_mnemonic = Wallet::import_wallet("imported_wallet", Some(test_mnemonic))
866            .await
867            .unwrap();
868
869        // Verify the mnemonic matches
870        assert_eq!(imported_mnemonic, test_mnemonic);
871
872        // Load the wallet and verify mnemonic
873        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        // Invalid mnemonic
884        let invalid_mnemonic = "invalid mnemonic phrase that should fail validation";
885
886        // Should fail with InvalidMnemonic error
887        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        // Try to load non-existent wallet without creating
896        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        // Load wallet with auto-creation
905        let wallet = Wallet::load(Some("auto_created".to_string()), true)
906            .await
907            .unwrap();
908
909        // Verify wallet was created and has valid mnemonic
910        let mnemonic = wallet.get_mnemonic().unwrap();
911        assert!(bip39::Mnemonic::parse_in_normalized(Language::English, mnemonic).is_ok());
912
913        // Verify wallet name
914        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        // Use known mnemonic for deterministic testing
922        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        // Test key derivation
932        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        // Verify keys are consistent
938        assert_eq!(
939            secret_key_to_public_key(&private_synthetic_key),
940            public_synthetic_key
941        );
942
943        // Verify puzzle hash is 32 bytes
944        assert_eq!(puzzle_hash.as_ref().len(), 32);
945
946        // Test that keys are deterministic (same mnemonic = same keys)
947        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        // Generate address
968        let address = wallet.get_owner_public_key().await.unwrap();
969
970        // Verify address format (should start with "xch1")
971        assert!(address.starts_with("xch1"));
972
973        // Verify address length (Chia addresses are typically 62 characters)
974        assert!(address.len() >= 60 && address.len() <= 65);
975
976        // Test address conversion roundtrip
977        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        // Create signature
996        let nonce = "test_nonce_12345";
997        let signature = wallet.create_key_ownership_signature(nonce).await.unwrap();
998
999        // Verify signature format (should be hex string)
1000        assert!(hex::decode(&signature).is_ok());
1001
1002        // Get public key for verification
1003        let public_key = wallet.get_public_synthetic_key().await.unwrap();
1004        let public_key_hex = hex::encode(public_key.to_bytes());
1005
1006        // Verify signature
1007        let is_valid = Wallet::verify_key_ownership_signature(nonce, &signature, &public_key_hex)
1008            .await
1009            .unwrap();
1010        assert!(is_valid);
1011
1012        // Test with wrong nonce (should fail)
1013        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        // Create wallet
1025        Wallet::create_new_wallet("delete_test").await.unwrap();
1026
1027        // Verify it exists
1028        let wallets_before = Wallet::list_wallets().await.unwrap();
1029        assert!(wallets_before.contains(&"delete_test".to_string()));
1030
1031        // Delete wallet
1032        let deleted = Wallet::delete_wallet("delete_test").await.unwrap();
1033        assert!(deleted);
1034
1035        // Verify it's gone
1036        let wallets_after = Wallet::list_wallets().await.unwrap();
1037        assert!(!wallets_after.contains(&"delete_test".to_string()));
1038
1039        // Try to delete non-existent wallet
1040        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        // Create multiple wallets
1049        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        // List wallets
1054        let mut wallets = Wallet::list_wallets().await.unwrap();
1055        wallets.sort(); // Sort for consistent testing
1056
1057        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        // Load each wallet and verify they have different mnemonics
1063        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        // Test encryption/decryption directly
1081        let test_data = "test mnemonic phrase for encryption";
1082
1083        let encrypted = Wallet::encrypt_data(test_data).unwrap();
1084
1085        // Verify encrypted data is different from original
1086        assert_ne!(encrypted.data, test_data);
1087        assert!(!encrypted.nonce.is_empty());
1088        assert!(!encrypted.salt.is_empty());
1089
1090        // Decrypt and verify
1091        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        // Encrypt same data twice
1100        let encrypted1 = Wallet::encrypt_data(test_data).unwrap();
1101        let encrypted2 = Wallet::encrypt_data(test_data).unwrap();
1102
1103        // Should produce different ciphertexts due to random salt/nonce
1104        assert_ne!(encrypted1.data, encrypted2.data);
1105        assert_ne!(encrypted1.salt, encrypted2.salt);
1106        assert_ne!(encrypted1.nonce, encrypted2.nonce);
1107
1108        // But both should decrypt to same data
1109        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        // Create wallet
1120        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        // Test with invalid signature format
1132        let result =
1133            Wallet::verify_key_ownership_signature("nonce", "invalid_hex", &public_key_hex).await;
1134        assert!(result.is_err());
1135
1136        // Test with wrong signature length
1137        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        // Test with invalid public key
1143        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        // Test invalid address
1151        let result = Wallet::address_to_puzzle_hash("invalid_address");
1152        assert!(result.is_err());
1153
1154        // Test empty address
1155        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        // Create wallet without mnemonic
1162        let wallet = Wallet::new(None, "empty_wallet".to_string());
1163
1164        // Should fail when trying to get mnemonic
1165        let result = wallet.get_mnemonic();
1166        assert!(matches!(result, Err(WalletError::MnemonicNotLoaded)));
1167
1168        // Should fail when trying to derive keys
1169        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        // Load wallet without specifying name (should use "default")
1178        let wallet = Wallet::load(None, true).await.unwrap();
1179        assert_eq!(wallet.get_wallet_name(), "default");
1180
1181        // Verify it appears in wallet list
1182        let wallets = Wallet::list_wallets().await.unwrap();
1183        assert!(wallets.contains(&"default".to_string()));
1184    }
1185}