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 datalayer_driver::{
9    address_to_puzzle_hash, connect_random, get_coin_id, master_public_key_to_first_puzzle_hash,
10    master_public_key_to_wallet_synthetic_key, master_secret_key_to_wallet_synthetic_secret_key,
11    puzzle_hash_to_address, secret_key_to_public_key, sign_message, verify_signature, Bytes,
12    Bytes32, Coin, CoinSpend, NetworkType, Peer, PublicKey, SecretKey, Signature,
13};
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::env;
17use std::fs;
18use std::path::PathBuf;
19
20const KEYRING_FILE: &str = "keyring.json";
21// Cache duration constant - keeping for potential future use
22#[allow(dead_code)]
23const CACHE_DURATION_MS: u64 = 5 * 60 * 1000; // 5 minutes
24pub const DEFAULT_FEE_COIN_COST: u64 = 64_000_000;
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27struct EncryptedData {
28    data: String,
29    nonce: String,
30    salt: String,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34struct KeyringData {
35    wallets: HashMap<String, EncryptedData>,
36}
37
38pub struct Wallet {
39    mnemonic: Option<String>,
40    wallet_name: String,
41}
42
43impl Wallet {
44    /// Create a new Wallet instance
45    fn new(mnemonic: Option<String>, wallet_name: String) -> Self {
46        Self {
47            mnemonic,
48            wallet_name,
49        }
50    }
51
52    /// Load a wallet by name, optionally creating one if it doesn't exist
53    pub async fn load(
54        wallet_name: Option<String>,
55        create_on_undefined: bool,
56    ) -> Result<Self, WalletError> {
57        let name = wallet_name.unwrap_or_else(|| "default".to_string());
58
59        if let Some(mnemonic) = Self::get_wallet_from_keyring(&name).await? {
60            return Ok(Self::new(Some(mnemonic), name));
61        }
62
63        if create_on_undefined {
64            // In a real implementation, you'd prompt the user for input
65            // For now, we'll generate a new wallet
66            let new_mnemonic = Self::create_new_wallet(&name).await?;
67            return Ok(Self::new(Some(new_mnemonic), name));
68        }
69
70        Err(WalletError::WalletNotFound(name))
71    }
72
73    /// Get the mnemonic seed phrase
74    pub fn get_mnemonic(&self) -> Result<&str, WalletError> {
75        self.mnemonic
76            .as_deref()
77            .ok_or(WalletError::MnemonicNotLoaded)
78    }
79
80    /// Get the wallet name
81    pub fn get_wallet_name(&self) -> &str {
82        &self.wallet_name
83    }
84
85    /// Create a new wallet with a generated mnemonic
86    pub async fn create_new_wallet(wallet_name: &str) -> Result<String, WalletError> {
87        let entropy = rand::random::<[u8; 32]>(); // 32 bytes = 256 bits for 24 words
88        let mnemonic = Mnemonic::from_entropy_in(Language::English, &entropy)
89            .map_err(|_| WalletError::CryptoError("Failed to generate mnemonic".to_string()))?;
90        let mnemonic_str = mnemonic.to_string();
91        Self::save_wallet_to_keyring(wallet_name, &mnemonic_str).await?;
92        Ok(mnemonic_str)
93    }
94
95    /// Import a wallet from a provided mnemonic
96    pub async fn import_wallet(
97        wallet_name: &str,
98        seed: Option<&str>,
99    ) -> Result<String, WalletError> {
100        let mnemonic_str = match seed {
101            Some(s) => s.to_string(),
102            None => {
103                // In a real implementation, you'd prompt for input
104                return Err(WalletError::MnemonicRequired);
105            }
106        };
107
108        // Validate the mnemonic
109        Mnemonic::parse_in_normalized(Language::English, &mnemonic_str)
110            .map_err(|_| WalletError::InvalidMnemonic)?;
111
112        Self::save_wallet_to_keyring(wallet_name, &mnemonic_str).await?;
113        Ok(mnemonic_str)
114    }
115
116    /// Get the master secret key from the mnemonic
117    pub async fn get_master_secret_key(&self) -> Result<SecretKey, WalletError> {
118        let mnemonic_str = self.get_mnemonic()?;
119        let mnemonic = Mnemonic::parse_in_normalized(Language::English, mnemonic_str)
120            .map_err(|_| WalletError::InvalidMnemonic)?;
121
122        let seed = mnemonic.to_seed("");
123        let sk = SecretKey::from_seed(&seed);
124        Ok(sk)
125    }
126
127    /// Get the public synthetic key
128    pub async fn get_public_synthetic_key(&self) -> Result<PublicKey, WalletError> {
129        let master_sk = self.get_master_secret_key().await?;
130        let master_pk = secret_key_to_public_key(&master_sk);
131        Ok(master_public_key_to_wallet_synthetic_key(&master_pk))
132    }
133
134    /// Get the private synthetic key
135    pub async fn get_private_synthetic_key(&self) -> Result<SecretKey, WalletError> {
136        let master_sk = self.get_master_secret_key().await?;
137        Ok(master_secret_key_to_wallet_synthetic_secret_key(&master_sk))
138    }
139
140    /// Get the owner puzzle hash
141    pub async fn get_owner_puzzle_hash(&self) -> Result<Bytes32, 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_first_puzzle_hash(&master_pk))
145    }
146
147    /// Get the owner public key as an address
148    pub async fn get_owner_public_key(&self) -> Result<String, WalletError> {
149        let owner_puzzle_hash = self.get_owner_puzzle_hash().await?;
150        // Convert puzzle hash to address (xch format) using DataLayer-Driver
151        puzzle_hash_to_address(owner_puzzle_hash, "xch")
152            .map_err(|e| WalletError::CryptoError(format!("Failed to encode address: {}", e)))
153    }
154
155    /// Delete a wallet from the keyring
156    pub async fn delete_wallet(wallet_name: &str) -> Result<bool, WalletError> {
157        let keyring_path = Self::get_keyring_path()?;
158
159        if !keyring_path.exists() {
160            return Ok(false);
161        }
162
163        let content = fs::read_to_string(&keyring_path)
164            .map_err(|e| WalletError::FileSystemError(e.to_string()))?;
165
166        let mut keyring: KeyringData = serde_json::from_str(&content)
167            .map_err(|e| WalletError::SerializationError(e.to_string()))?;
168
169        if keyring.wallets.remove(wallet_name).is_some() {
170            let updated_content = serde_json::to_string_pretty(&keyring)
171                .map_err(|e| WalletError::SerializationError(e.to_string()))?;
172
173            fs::write(&keyring_path, updated_content)
174                .map_err(|e| WalletError::FileSystemError(e.to_string()))?;
175
176            Ok(true)
177        } else {
178            Ok(false)
179        }
180    }
181
182    /// List all wallets in the keyring
183    pub async fn list_wallets() -> Result<Vec<String>, WalletError> {
184        let keyring_path = Self::get_keyring_path()?;
185
186        if !keyring_path.exists() {
187            return Ok(vec![]);
188        }
189
190        let content = fs::read_to_string(&keyring_path)
191            .map_err(|e| WalletError::FileSystemError(e.to_string()))?;
192
193        let keyring: KeyringData = serde_json::from_str(&content)
194            .map_err(|e| WalletError::SerializationError(e.to_string()))?;
195
196        Ok(keyring.wallets.keys().cloned().collect())
197    }
198
199    /// Create a key ownership signature
200    pub async fn create_key_ownership_signature(&self, nonce: &str) -> Result<String, WalletError> {
201        let message = format!(
202            "Signing this message to prove ownership of key.\n\nNonce: {}",
203            nonce
204        );
205        let private_synthetic_key = self.get_private_synthetic_key().await?;
206
207        let signature = sign_message(
208            Bytes::from(message.as_bytes().to_vec()),
209            private_synthetic_key,
210        )
211        .map_err(|e| WalletError::CryptoError(e.to_string()))?;
212
213        Ok(hex::encode(signature.to_bytes()))
214    }
215
216    /// Verify a key ownership signature
217    pub async fn verify_key_ownership_signature(
218        nonce: &str,
219        signature: &str,
220        public_key: &str,
221    ) -> Result<bool, WalletError> {
222        let message = format!(
223            "Signing this message to prove ownership of key.\n\nNonce: {}",
224            nonce
225        );
226
227        let sig_bytes =
228            hex::decode(signature).map_err(|e| WalletError::CryptoError(e.to_string()))?;
229
230        let pk_bytes =
231            hex::decode(public_key).map_err(|e| WalletError::CryptoError(e.to_string()))?;
232
233        if pk_bytes.len() != 48 {
234            return Err(WalletError::CryptoError(
235                "Invalid public key length".to_string(),
236            ));
237        }
238
239        let mut pk_array = [0u8; 48];
240        pk_array.copy_from_slice(&pk_bytes);
241
242        let public_key = PublicKey::from_bytes(&pk_array)
243            .map_err(|e| WalletError::CryptoError(e.to_string()))?;
244
245        if sig_bytes.len() != 96 {
246            return Err(WalletError::CryptoError(
247                "Invalid signature length".to_string(),
248            ));
249        }
250
251        let mut sig_array = [0u8; 96];
252        sig_array.copy_from_slice(&sig_bytes);
253
254        let signature = Signature::from_bytes(&sig_array)
255            .map_err(|e| WalletError::CryptoError(e.to_string()))?;
256
257        verify_signature(
258            Bytes::from(message.as_bytes().to_vec()),
259            public_key,
260            signature,
261        )
262        .map_err(|e| WalletError::CryptoError(e.to_string()))
263    }
264
265    /// Select unspent coins for spending
266    pub async fn select_unspent_coins(
267        &self,
268        peer: &Peer,
269        coin_amount: u64,
270        fee: u64,
271        omit_coins: Vec<Coin>,
272    ) -> Result<Vec<Coin>, WalletError> {
273        let owner_puzzle_hash = self.get_owner_puzzle_hash().await?;
274        let total_needed = coin_amount + fee;
275
276        // Get unspent coin states from the DataLayer-Driver async API
277        let coin_states = datalayer_driver::async_api::get_all_unspent_coins_rust(
278            peer,
279            owner_puzzle_hash,
280            None, // previous_height - start from genesis
281            datalayer_driver::constants::get_mainnet_genesis_challenge(), // Use mainnet for now
282        )
283        .await
284        .map_err(|e| WalletError::NetworkError(format!("Failed to get unspent coins: {}", e)))?;
285
286        // Convert coin states to coins and filter out omitted coins
287        let omit_coin_ids: Vec<Bytes32> = omit_coins.iter().map(get_coin_id).collect();
288
289        let available_coins: Vec<Coin> = coin_states
290            .coin_states
291            .into_iter()
292            .map(|cs| cs.coin)
293            .filter(|coin| !omit_coin_ids.contains(&get_coin_id(coin)))
294            .collect();
295
296        // Use the DataLayer-Driver's select_coins function
297        let selected_coins = datalayer_driver::select_coins_rust(&available_coins, total_needed)
298            .map_err(|e| WalletError::DataLayerError(format!("Coin selection failed: {}", e)))?;
299
300        if selected_coins.is_empty() {
301            return Err(WalletError::NoUnspentCoins);
302        }
303
304        Ok(selected_coins)
305    }
306
307    /// Calculate fee for coin spends
308    pub async fn calculate_fee_for_coin_spends(
309        _peer: &Peer,
310        _coin_spends: Option<&[CoinSpend]>,
311    ) -> Result<u64, WalletError> {
312        // Simplified fee calculation - in practice this would be more complex
313        Ok(1_000_000) // 1 million mojos
314    }
315
316    /// Check if a coin is spendable
317    pub async fn is_coin_spendable(peer: &Peer, coin_id: &Bytes32) -> Result<bool, WalletError> {
318        use datalayer_driver::async_api::is_coin_spent_rust;
319
320        // Check if coin is spent using the DataLayer-Driver API
321        let is_spent = is_coin_spent_rust(
322            peer,
323            *coin_id,
324            None,                                                         // last_height
325            datalayer_driver::constants::get_mainnet_genesis_challenge(), // Use mainnet for now
326        )
327        .await
328        .map_err(|e| WalletError::NetworkError(format!("Failed to check coin status: {}", e)))?;
329
330        // Return true if coin is NOT spent (i.e., is spendable)
331        Ok(!is_spent)
332    }
333
334    /// Connect to a random peer on the specified network
335    pub async fn connect_random_peer(
336        network: NetworkType,
337        cert_path: &str,
338        key_path: &str,
339    ) -> Result<Peer, WalletError> {
340        connect_random(network, cert_path, key_path)
341            .await
342            .map_err(|e| WalletError::NetworkError(format!("Failed to connect to peer: {}", e)))
343    }
344
345    /// Connect to a random mainnet peer using default Chia SSL paths
346    pub async fn connect_mainnet_peer() -> Result<Peer, WalletError> {
347        let home_dir = dirs::home_dir().ok_or_else(|| {
348            WalletError::FileSystemError("Could not find home directory".to_string())
349        })?;
350
351        let ssl_dir = home_dir
352            .join(".chia")
353            .join("mainnet")
354            .join("config")
355            .join("ssl")
356            .join("wallet");
357        let cert_path = ssl_dir.join("wallet_node.crt");
358        let key_path = ssl_dir.join("wallet_node.key");
359
360        Self::connect_random_peer(
361            NetworkType::Mainnet,
362            cert_path
363                .to_str()
364                .ok_or_else(|| WalletError::FileSystemError("Invalid cert path".to_string()))?,
365            key_path
366                .to_str()
367                .ok_or_else(|| WalletError::FileSystemError("Invalid key path".to_string()))?,
368        )
369        .await
370    }
371
372    /// Connect to a random testnet peer using default Chia SSL paths
373    pub async fn connect_testnet_peer() -> Result<Peer, WalletError> {
374        let home_dir = dirs::home_dir().ok_or_else(|| {
375            WalletError::FileSystemError("Could not find home directory".to_string())
376        })?;
377
378        let ssl_dir = home_dir
379            .join(".chia")
380            .join("testnet11")
381            .join("config")
382            .join("ssl")
383            .join("wallet");
384        let cert_path = ssl_dir.join("wallet_node.crt");
385        let key_path = ssl_dir.join("wallet_node.key");
386
387        Self::connect_random_peer(
388            NetworkType::Testnet11,
389            cert_path
390                .to_str()
391                .ok_or_else(|| WalletError::FileSystemError("Invalid cert path".to_string()))?,
392            key_path
393                .to_str()
394                .ok_or_else(|| WalletError::FileSystemError("Invalid key path".to_string()))?,
395        )
396        .await
397    }
398
399    /// Convert an address to a puzzle hash
400    pub fn address_to_puzzle_hash(address: &str) -> Result<Bytes32, WalletError> {
401        address_to_puzzle_hash(address)
402            .map_err(|e| WalletError::CryptoError(format!("Failed to decode address: {}", e)))
403    }
404
405    /// Convert a puzzle hash to an address
406    pub fn puzzle_hash_to_address(
407        puzzle_hash: Bytes32,
408        prefix: &str,
409    ) -> Result<String, WalletError> {
410        puzzle_hash_to_address(puzzle_hash, prefix)
411            .map_err(|e| WalletError::CryptoError(format!("Failed to encode address: {}", e)))
412    }
413
414    // Private helper methods
415
416    async fn get_wallet_from_keyring(wallet_name: &str) -> Result<Option<String>, WalletError> {
417        let keyring_path = Self::get_keyring_path()?;
418
419        if !keyring_path.exists() {
420            return Ok(None);
421        }
422
423        let content = fs::read_to_string(&keyring_path)
424            .map_err(|e| WalletError::FileSystemError(e.to_string()))?;
425
426        let keyring: KeyringData = serde_json::from_str(&content)
427            .map_err(|e| WalletError::SerializationError(e.to_string()))?;
428
429        if let Some(encrypted_data) = keyring.wallets.get(wallet_name) {
430            let decrypted = Self::decrypt_data(encrypted_data)?;
431            Ok(Some(decrypted))
432        } else {
433            Ok(None)
434        }
435    }
436
437    async fn save_wallet_to_keyring(wallet_name: &str, mnemonic: &str) -> Result<(), WalletError> {
438        let keyring_path = Self::get_keyring_path()?;
439
440        // Ensure the directory exists
441        if let Some(parent) = keyring_path.parent() {
442            fs::create_dir_all(parent).map_err(|e| WalletError::FileSystemError(e.to_string()))?;
443        }
444
445        let mut keyring = if keyring_path.exists() {
446            let content = fs::read_to_string(&keyring_path)
447                .map_err(|e| WalletError::FileSystemError(e.to_string()))?;
448            serde_json::from_str(&content)
449                .map_err(|e| WalletError::SerializationError(e.to_string()))?
450        } else {
451            KeyringData {
452                wallets: HashMap::new(),
453            }
454        };
455
456        let encrypted_data = Self::encrypt_data(mnemonic)?;
457
458        keyring
459            .wallets
460            .insert(wallet_name.to_string(), encrypted_data);
461
462        let content = serde_json::to_string_pretty(&keyring)
463            .map_err(|e| WalletError::SerializationError(e.to_string()))?;
464
465        fs::write(&keyring_path, content)
466            .map_err(|e| WalletError::FileSystemError(e.to_string()))?;
467
468        Ok(())
469    }
470
471    fn get_keyring_path() -> Result<PathBuf, WalletError> {
472        // Check if we're in test mode by looking for TEST_KEYRING_PATH env var
473        if let Ok(test_path) = env::var("TEST_KEYRING_PATH") {
474            return Ok(PathBuf::from(test_path));
475        }
476
477        let home_dir = dirs::home_dir().ok_or_else(|| {
478            WalletError::FileSystemError("Could not find home directory".to_string())
479        })?;
480
481        Ok(home_dir.join(".dig").join(KEYRING_FILE))
482    }
483
484    /// Encrypt data using AES-256-GCM
485    fn encrypt_data(data: &str) -> Result<EncryptedData, WalletError> {
486        // Generate a random salt
487        let salt = rand::random::<[u8; 16]>();
488
489        // Derive key from a fixed password and salt using a simple method
490        // In production, you'd want to use a proper key derivation function like PBKDF2
491        let mut key_bytes = [0u8; 32];
492        let password = b"mnemonic-seed"; // This should be derived from user input in practice
493
494        // Simple key derivation (not cryptographically secure - use PBKDF2 in production)
495        for i in 0..32 {
496            key_bytes[i] = password[i % password.len()] ^ salt[i % salt.len()];
497        }
498
499        let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
500        let cipher = Aes256Gcm::new(key);
501
502        // Generate a random nonce
503        let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
504
505        // Encrypt the data
506        let ciphertext = cipher
507            .encrypt(&nonce, data.as_bytes())
508            .map_err(|e| WalletError::CryptoError(format!("Encryption failed: {}", e)))?;
509
510        Ok(EncryptedData {
511            data: general_purpose::STANDARD.encode(&ciphertext),
512            nonce: general_purpose::STANDARD.encode(nonce),
513            salt: general_purpose::STANDARD.encode(salt),
514        })
515    }
516
517    /// Decrypt data using AES-256-GCM
518    fn decrypt_data(encrypted_data: &EncryptedData) -> Result<String, WalletError> {
519        let ciphertext = general_purpose::STANDARD
520            .decode(&encrypted_data.data)
521            .map_err(|e| WalletError::CryptoError(format!("Failed to decode ciphertext: {}", e)))?;
522
523        let nonce_bytes = general_purpose::STANDARD
524            .decode(&encrypted_data.nonce)
525            .map_err(|e| WalletError::CryptoError(format!("Failed to decode nonce: {}", e)))?;
526
527        let salt = general_purpose::STANDARD
528            .decode(&encrypted_data.salt)
529            .map_err(|e| WalletError::CryptoError(format!("Failed to decode salt: {}", e)))?;
530
531        // Derive the same key using the salt
532        let mut key_bytes = [0u8; 32];
533        let password = b"mnemonic-seed";
534
535        for i in 0..32 {
536            key_bytes[i] = password[i % password.len()] ^ salt[i % salt.len()];
537        }
538
539        let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
540        let cipher = Aes256Gcm::new(key);
541
542        let nonce = Nonce::from_slice(&nonce_bytes);
543
544        // Decrypt the data
545        let plaintext = cipher
546            .decrypt(nonce, ciphertext.as_ref())
547            .map_err(|e| WalletError::CryptoError(format!("Decryption failed: {}", e)))?;
548
549        String::from_utf8(plaintext).map_err(|e| {
550            WalletError::CryptoError(format!("Failed to convert decrypted data to string: {}", e))
551        })
552    }
553}
554
555#[cfg(test)]
556mod tests {
557    use super::*;
558    use std::env;
559    use tempfile::TempDir;
560
561    // Test helper to set up a temporary directory for tests
562    fn setup_test_env() -> TempDir {
563        let temp_dir = TempDir::new().unwrap();
564
565        // Set up isolated keyring path for this test
566        let keyring_path = temp_dir.path().join("test_keyring.json");
567        env::set_var(
568            "TEST_KEYRING_PATH",
569            keyring_path.to_string_lossy().to_string(),
570        );
571
572        // Also set HOME for any other path operations
573        env::set_var("HOME", temp_dir.path());
574
575        temp_dir
576    }
577
578    #[tokio::test]
579    async fn test_wallet_creation() {
580        let _temp_dir = setup_test_env();
581
582        // Create a new wallet
583        let mnemonic = Wallet::create_new_wallet("test_wallet").await.unwrap();
584
585        // Verify mnemonic is valid BIP39
586        assert!(bip39::Mnemonic::parse_in_normalized(Language::English, &mnemonic).is_ok());
587
588        // Verify mnemonic has 24 words
589        assert_eq!(mnemonic.split_whitespace().count(), 24);
590
591        // Verify wallet appears in list
592        let wallets = Wallet::list_wallets().await.unwrap();
593        assert!(wallets.contains(&"test_wallet".to_string()));
594    }
595
596    #[tokio::test]
597    async fn test_wallet_import() {
598        let _temp_dir = setup_test_env();
599
600        // Known valid 24-word mnemonic
601        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";
602
603        // Import the wallet
604        let imported_mnemonic = Wallet::import_wallet("imported_wallet", Some(test_mnemonic))
605            .await
606            .unwrap();
607
608        // Verify the mnemonic matches
609        assert_eq!(imported_mnemonic, test_mnemonic);
610
611        // Load the wallet and verify mnemonic
612        let wallet = Wallet::load(Some("imported_wallet".to_string()), false)
613            .await
614            .unwrap();
615        assert_eq!(wallet.get_mnemonic().unwrap(), test_mnemonic);
616    }
617
618    #[tokio::test]
619    async fn test_wallet_import_invalid_mnemonic() {
620        let _temp_dir = setup_test_env();
621
622        // Invalid mnemonic
623        let invalid_mnemonic = "invalid mnemonic phrase that should fail validation";
624
625        // Should fail with InvalidMnemonic error
626        let result = Wallet::import_wallet("invalid_wallet", Some(invalid_mnemonic)).await;
627        assert!(matches!(result, Err(WalletError::InvalidMnemonic)));
628    }
629
630    #[tokio::test]
631    async fn test_wallet_load_nonexistent() {
632        let _temp_dir = setup_test_env();
633
634        // Try to load non-existent wallet without creating
635        let result = Wallet::load(Some("nonexistent".to_string()), false).await;
636        assert!(matches!(result, Err(WalletError::WalletNotFound(_))));
637    }
638
639    #[tokio::test]
640    async fn test_wallet_load_with_creation() {
641        let _temp_dir = setup_test_env();
642
643        // Load wallet with auto-creation
644        let wallet = Wallet::load(Some("auto_created".to_string()), true)
645            .await
646            .unwrap();
647
648        // Verify wallet was created and has valid mnemonic
649        let mnemonic = wallet.get_mnemonic().unwrap();
650        assert!(bip39::Mnemonic::parse_in_normalized(Language::English, mnemonic).is_ok());
651
652        // Verify wallet name
653        assert_eq!(wallet.get_wallet_name(), "auto_created");
654    }
655
656    #[tokio::test]
657    async fn test_key_derivation() {
658        let _temp_dir = setup_test_env();
659
660        // Use known mnemonic for deterministic testing
661        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";
662
663        Wallet::import_wallet("key_test", Some(test_mnemonic))
664            .await
665            .unwrap();
666        let wallet = Wallet::load(Some("key_test".to_string()), false)
667            .await
668            .unwrap();
669
670        // Test key derivation
671        let master_sk = wallet.get_master_secret_key().await.unwrap();
672        let public_synthetic_key = wallet.get_public_synthetic_key().await.unwrap();
673        let private_synthetic_key = wallet.get_private_synthetic_key().await.unwrap();
674        let puzzle_hash = wallet.get_owner_puzzle_hash().await.unwrap();
675
676        // Verify keys are consistent
677        assert_eq!(
678            secret_key_to_public_key(&private_synthetic_key),
679            public_synthetic_key
680        );
681
682        // Verify puzzle hash is 32 bytes
683        assert_eq!(puzzle_hash.as_ref().len(), 32);
684
685        // Test that keys are deterministic (same mnemonic = same keys)
686        let wallet2 = Wallet::load(Some("key_test".to_string()), false)
687            .await
688            .unwrap();
689        let master_sk2 = wallet2.get_master_secret_key().await.unwrap();
690        assert_eq!(master_sk.to_bytes(), master_sk2.to_bytes());
691    }
692
693    #[tokio::test]
694    async fn test_address_generation() {
695        let _temp_dir = setup_test_env();
696
697        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";
698
699        Wallet::import_wallet("address_test", Some(test_mnemonic))
700            .await
701            .unwrap();
702        let wallet = Wallet::load(Some("address_test".to_string()), false)
703            .await
704            .unwrap();
705
706        // Generate address
707        let address = wallet.get_owner_public_key().await.unwrap();
708
709        // Verify address format (should start with "xch1")
710        assert!(address.starts_with("xch1"));
711
712        // Verify address length (Chia addresses are typically 62 characters)
713        assert!(address.len() >= 60 && address.len() <= 65);
714
715        // Test address conversion roundtrip
716        let puzzle_hash = Wallet::address_to_puzzle_hash(&address).unwrap();
717        let converted_address = Wallet::puzzle_hash_to_address(puzzle_hash, "xch").unwrap();
718        assert_eq!(address, converted_address);
719    }
720
721    #[tokio::test]
722    async fn test_signature_creation_and_verification() {
723        let _temp_dir = setup_test_env();
724
725        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";
726
727        Wallet::import_wallet("sig_test", Some(test_mnemonic))
728            .await
729            .unwrap();
730        let wallet = Wallet::load(Some("sig_test".to_string()), false)
731            .await
732            .unwrap();
733
734        // Create signature
735        let nonce = "test_nonce_12345";
736        let signature = wallet.create_key_ownership_signature(nonce).await.unwrap();
737
738        // Verify signature format (should be hex string)
739        assert!(hex::decode(&signature).is_ok());
740
741        // Get public key for verification
742        let public_key = wallet.get_public_synthetic_key().await.unwrap();
743        let public_key_hex = hex::encode(public_key.to_bytes());
744
745        // Verify signature
746        let is_valid = Wallet::verify_key_ownership_signature(nonce, &signature, &public_key_hex)
747            .await
748            .unwrap();
749        assert!(is_valid);
750
751        // Test with wrong nonce (should fail)
752        let is_valid_wrong =
753            Wallet::verify_key_ownership_signature("wrong_nonce", &signature, &public_key_hex)
754                .await
755                .unwrap();
756        assert!(!is_valid_wrong);
757    }
758
759    #[tokio::test]
760    async fn test_wallet_deletion() {
761        let _temp_dir = setup_test_env();
762
763        // Create wallet
764        Wallet::create_new_wallet("delete_test").await.unwrap();
765
766        // Verify it exists
767        let wallets_before = Wallet::list_wallets().await.unwrap();
768        assert!(wallets_before.contains(&"delete_test".to_string()));
769
770        // Delete wallet
771        let deleted = Wallet::delete_wallet("delete_test").await.unwrap();
772        assert!(deleted);
773
774        // Verify it's gone
775        let wallets_after = Wallet::list_wallets().await.unwrap();
776        assert!(!wallets_after.contains(&"delete_test".to_string()));
777
778        // Try to delete non-existent wallet
779        let not_deleted = Wallet::delete_wallet("nonexistent").await.unwrap();
780        assert!(!not_deleted);
781    }
782
783    #[tokio::test]
784    async fn test_multiple_wallets() {
785        let _temp_dir = setup_test_env();
786
787        // Create multiple wallets
788        Wallet::create_new_wallet("wallet1").await.unwrap();
789        Wallet::create_new_wallet("wallet2").await.unwrap();
790        Wallet::create_new_wallet("wallet3").await.unwrap();
791
792        // List wallets
793        let mut wallets = Wallet::list_wallets().await.unwrap();
794        wallets.sort(); // Sort for consistent testing
795
796        assert_eq!(wallets.len(), 3);
797        assert!(wallets.contains(&"wallet1".to_string()));
798        assert!(wallets.contains(&"wallet2".to_string()));
799        assert!(wallets.contains(&"wallet3".to_string()));
800
801        // Load each wallet and verify they have different mnemonics
802        let w1 = Wallet::load(Some("wallet1".to_string()), false)
803            .await
804            .unwrap();
805        let w2 = Wallet::load(Some("wallet2".to_string()), false)
806            .await
807            .unwrap();
808        let w3 = Wallet::load(Some("wallet3".to_string()), false)
809            .await
810            .unwrap();
811
812        assert_ne!(w1.get_mnemonic().unwrap(), w2.get_mnemonic().unwrap());
813        assert_ne!(w2.get_mnemonic().unwrap(), w3.get_mnemonic().unwrap());
814        assert_ne!(w1.get_mnemonic().unwrap(), w3.get_mnemonic().unwrap());
815    }
816
817    #[tokio::test]
818    async fn test_encryption_decryption() {
819        // Test encryption/decryption directly
820        let test_data = "test mnemonic phrase for encryption";
821
822        let encrypted = Wallet::encrypt_data(test_data).unwrap();
823
824        // Verify encrypted data is different from original
825        assert_ne!(encrypted.data, test_data);
826        assert!(!encrypted.nonce.is_empty());
827        assert!(!encrypted.salt.is_empty());
828
829        // Decrypt and verify
830        let decrypted = Wallet::decrypt_data(&encrypted).unwrap();
831        assert_eq!(decrypted, test_data);
832    }
833
834    #[tokio::test]
835    async fn test_encryption_with_different_salts() {
836        let test_data = "same data";
837
838        // Encrypt same data twice
839        let encrypted1 = Wallet::encrypt_data(test_data).unwrap();
840        let encrypted2 = Wallet::encrypt_data(test_data).unwrap();
841
842        // Should produce different ciphertexts due to random salt/nonce
843        assert_ne!(encrypted1.data, encrypted2.data);
844        assert_ne!(encrypted1.salt, encrypted2.salt);
845        assert_ne!(encrypted1.nonce, encrypted2.nonce);
846
847        // But both should decrypt to same data
848        let decrypted1 = Wallet::decrypt_data(&encrypted1).unwrap();
849        let decrypted2 = Wallet::decrypt_data(&encrypted2).unwrap();
850        assert_eq!(decrypted1, test_data);
851        assert_eq!(decrypted2, test_data);
852    }
853
854    #[tokio::test]
855    async fn test_invalid_signature_verification() {
856        let _temp_dir = setup_test_env();
857
858        // Create wallet
859        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";
860        Wallet::import_wallet("invalid_sig_test", Some(test_mnemonic))
861            .await
862            .unwrap();
863        let wallet = Wallet::load(Some("invalid_sig_test".to_string()), false)
864            .await
865            .unwrap();
866
867        let public_key = wallet.get_public_synthetic_key().await.unwrap();
868        let public_key_hex = hex::encode(public_key.to_bytes());
869
870        // Test with invalid signature format
871        let result =
872            Wallet::verify_key_ownership_signature("nonce", "invalid_hex", &public_key_hex).await;
873        assert!(result.is_err());
874
875        // Test with wrong signature length
876        let short_sig = "deadbeef";
877        let result =
878            Wallet::verify_key_ownership_signature("nonce", short_sig, &public_key_hex).await;
879        assert!(result.is_err());
880
881        // Test with invalid public key
882        let result =
883            Wallet::verify_key_ownership_signature("nonce", &"a".repeat(192), "invalid_key").await;
884        assert!(result.is_err());
885    }
886
887    #[tokio::test]
888    async fn test_address_conversion_errors() {
889        // Test invalid address
890        let result = Wallet::address_to_puzzle_hash("invalid_address");
891        assert!(result.is_err());
892
893        // Test empty address
894        let result = Wallet::address_to_puzzle_hash("");
895        assert!(result.is_err());
896    }
897
898    #[tokio::test]
899    async fn test_mnemonic_not_loaded_error() {
900        // Create wallet without mnemonic
901        let wallet = Wallet::new(None, "empty_wallet".to_string());
902
903        // Should fail when trying to get mnemonic
904        let result = wallet.get_mnemonic();
905        assert!(matches!(result, Err(WalletError::MnemonicNotLoaded)));
906
907        // Should fail when trying to derive keys
908        let result = wallet.get_master_secret_key().await;
909        assert!(matches!(result, Err(WalletError::MnemonicNotLoaded)));
910    }
911
912    #[tokio::test]
913    async fn test_default_wallet_name() {
914        let _temp_dir = setup_test_env();
915
916        // Load wallet without specifying name (should use "default")
917        let wallet = Wallet::load(None, true).await.unwrap();
918        assert_eq!(wallet.get_wallet_name(), "default");
919
920        // Verify it appears in wallet list
921        let wallets = Wallet::list_wallets().await.unwrap();
922        assert!(wallets.contains(&"default".to_string()));
923    }
924}