apex_sdk_evm/
wallet.rs

1//! Wallet management for EVM chains
2//!
3//! This module provides secure wallet management including:
4//! - Wallet creation and recovery
5//! - Private key management
6//! - Transaction signing
7//! - Message signing (EIP-191, EIP-712)
8
9use crate::Error;
10use ethers::prelude::*;
11use ethers::signers::{coins_bip39::English, LocalWallet, Signer};
12use ethers::types::{
13    transaction::eip2718::TypedTransaction, transaction::eip712::Eip712, Address as EthAddress,
14    Signature,
15};
16use std::str::FromStr;
17
18/// Wallet for managing EVM accounts and signing transactions
19#[derive(Clone)]
20pub struct Wallet {
21    /// The underlying ethers wallet
22    inner: LocalWallet,
23    /// The address of this wallet
24    address: EthAddress,
25}
26
27impl Wallet {
28    /// Create a new random wallet
29    ///
30    /// # Example
31    /// ```no_run
32    /// use apex_sdk_evm::wallet::Wallet;
33    ///
34    /// let wallet = Wallet::new_random();
35    /// println!("Address: {}", wallet.address());
36    /// ```
37    pub fn new_random() -> Self {
38        let inner = LocalWallet::new(&mut rand::thread_rng());
39        let address = inner.address();
40
41        tracing::info!("Created new random wallet: {}", address);
42
43        Self { inner, address }
44    }
45
46    /// Create a wallet from a private key (hex string with or without 0x prefix)
47    ///
48    /// # Arguments
49    /// * `private_key` - The private key as a hex string
50    ///
51    /// # Example
52    /// ```no_run
53    /// use apex_sdk_evm::wallet::Wallet;
54    ///
55    /// let wallet = Wallet::from_private_key(
56    ///     "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
57    /// ).unwrap();
58    /// ```
59    pub fn from_private_key(private_key: &str) -> Result<Self, Error> {
60        let key = private_key.trim_start_matches("0x");
61
62        let inner = LocalWallet::from_str(key)
63            .map_err(|e| Error::Other(format!("Invalid private key: {}", e)))?;
64
65        let address = inner.address();
66
67        tracing::info!("Loaded wallet from private key: {}", address);
68
69        Ok(Self { inner, address })
70    }
71
72    /// Create a wallet from a mnemonic phrase
73    ///
74    /// # Arguments
75    /// * `mnemonic` - The BIP-39 mnemonic phrase
76    /// * `index` - The account index (default 0 for first account)
77    ///
78    /// # Example
79    /// ```no_run
80    /// use apex_sdk_evm::wallet::Wallet;
81    ///
82    /// let wallet = Wallet::from_mnemonic(
83    ///     "test test test test test test test test test test test junk",
84    ///     0
85    /// ).unwrap();
86    /// ```
87    pub fn from_mnemonic(mnemonic: &str, index: u32) -> Result<Self, Error> {
88        let wallet = MnemonicBuilder::<English>::default()
89            .phrase(mnemonic)
90            .index(index)
91            .map_err(|e| Error::Other(format!("Invalid index: {}", e)))?
92            .build()
93            .map_err(|e| Error::Other(format!("Failed to build wallet from mnemonic: {}", e)))?;
94
95        let address = wallet.address();
96
97        tracing::info!(
98            "Loaded wallet from mnemonic at index {}: {}",
99            index,
100            address
101        );
102
103        Ok(Self {
104            inner: wallet,
105            address,
106        })
107    }
108
109    /// Create a wallet with a specific chain ID
110    ///
111    /// This is important for EIP-155 replay protection
112    pub fn with_chain_id(mut self, chain_id: u64) -> Self {
113        self.inner = self.inner.with_chain_id(chain_id);
114        tracing::debug!("Set wallet chain ID to {}", chain_id);
115        self
116    }
117
118    /// Get the wallet's address
119    pub fn address(&self) -> String {
120        format!("{:?}", self.address)
121    }
122
123    /// Get the wallet's address as EthAddress
124    pub fn eth_address(&self) -> EthAddress {
125        self.address
126    }
127
128    /// Sign a transaction
129    ///
130    /// # Arguments
131    /// * `tx` - The transaction to sign
132    ///
133    /// # Returns
134    /// The signature as bytes
135    pub async fn sign_transaction(&self, tx: &TypedTransaction) -> Result<Signature, Error> {
136        let signature = self
137            .inner
138            .sign_transaction(tx)
139            .await
140            .map_err(|e| Error::Transaction(format!("Failed to sign transaction: {}", e)))?;
141
142        tracing::debug!("Signed transaction");
143
144        Ok(signature)
145    }
146
147    /// Sign a message (EIP-191)
148    ///
149    /// # Arguments
150    /// * `message` - The message to sign
151    ///
152    /// # Returns
153    /// The signature as bytes
154    pub async fn sign_message<S: AsRef<[u8]> + Send + Sync>(
155        &self,
156        message: S,
157    ) -> Result<Signature, Error> {
158        let signature = self
159            .inner
160            .sign_message(message)
161            .await
162            .map_err(|e| Error::Transaction(format!("Failed to sign message: {}", e)))?;
163
164        tracing::debug!("Signed message");
165
166        Ok(signature)
167    }
168
169    /// Sign typed data (EIP-712)
170    ///
171    /// # Arguments
172    /// * `data` - The typed data to sign
173    ///
174    /// # Returns
175    /// The signature as bytes
176    pub async fn sign_typed_data<T: Eip712 + Send + Sync>(
177        &self,
178        data: &T,
179    ) -> Result<Signature, Error> {
180        let signature = self
181            .inner
182            .sign_typed_data(data)
183            .await
184            .map_err(|e| Error::Transaction(format!("Failed to sign typed data: {}", e)))?;
185
186        tracing::debug!("Signed typed data");
187
188        Ok(signature)
189    }
190
191    /// Get the chain ID configured for this wallet
192    pub fn chain_id(&self) -> Option<u64> {
193        Some(self.inner.chain_id())
194    }
195
196    /// Export private key (WARNING: Handle with extreme care!)
197    ///
198    /// # Security Warning
199    /// This exposes the private key. Only use in secure contexts.
200    pub fn export_private_key(&self) -> String {
201        tracing::warn!("Private key exported - ensure secure handling!");
202        format!("0x{}", hex::encode(self.inner.signer().to_bytes()))
203    }
204}
205
206impl std::fmt::Debug for Wallet {
207    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208        f.debug_struct("Wallet")
209            .field("address", &self.address())
210            .field("chain_id", &self.chain_id())
211            .finish()
212    }
213}
214
215/// Wallet manager for handling multiple accounts
216pub struct WalletManager {
217    wallets: Vec<Wallet>,
218    active_index: usize,
219}
220
221impl WalletManager {
222    /// Create a new wallet manager
223    pub fn new() -> Self {
224        Self {
225            wallets: Vec::new(),
226            active_index: 0,
227        }
228    }
229
230    /// Add a wallet to the manager
231    pub fn add_wallet(&mut self, wallet: Wallet) -> usize {
232        self.wallets.push(wallet);
233        self.wallets.len() - 1
234    }
235
236    /// Get the active wallet
237    pub fn active_wallet(&self) -> Option<&Wallet> {
238        self.wallets.get(self.active_index)
239    }
240
241    /// Get a wallet by index
242    pub fn wallet(&self, index: usize) -> Option<&Wallet> {
243        self.wallets.get(index)
244    }
245
246    /// Set the active wallet
247    pub fn set_active(&mut self, index: usize) -> Result<(), Error> {
248        if index >= self.wallets.len() {
249            return Err(Error::Other("Invalid wallet index".to_string()));
250        }
251        self.active_index = index;
252        Ok(())
253    }
254
255    /// Get the number of wallets
256    pub fn wallet_count(&self) -> usize {
257        self.wallets.len()
258    }
259
260    /// List all wallet addresses
261    pub fn list_addresses(&self) -> Vec<String> {
262        self.wallets.iter().map(|w| w.address()).collect()
263    }
264
265    /// Create and add a new random wallet
266    pub fn create_wallet(&mut self) -> usize {
267        let wallet = Wallet::new_random();
268        self.add_wallet(wallet)
269    }
270
271    /// Import a wallet from private key and add it
272    pub fn import_wallet(&mut self, private_key: &str) -> Result<usize, Error> {
273        let wallet = Wallet::from_private_key(private_key)?;
274        Ok(self.add_wallet(wallet))
275    }
276
277    /// Import a wallet from mnemonic and add it
278    pub fn import_from_mnemonic(&mut self, mnemonic: &str, index: u32) -> Result<usize, Error> {
279        let wallet = Wallet::from_mnemonic(mnemonic, index)?;
280        Ok(self.add_wallet(wallet))
281    }
282}
283
284impl Default for WalletManager {
285    fn default() -> Self {
286        Self::new()
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_new_random_wallet() {
296        let wallet = Wallet::new_random();
297        assert!(wallet.address().starts_with("0x"));
298        assert_eq!(wallet.address().len(), 42);
299    }
300
301    #[test]
302    fn test_from_private_key() {
303        // Test private key (from hardhat default)
304        let private_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
305        let wallet = Wallet::from_private_key(private_key).unwrap();
306
307        // Expected address for this key
308        let expected_address = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266";
309        assert_eq!(wallet.address().to_lowercase(), expected_address);
310    }
311
312    #[test]
313    fn test_from_mnemonic() {
314        // Test mnemonic (common test phrase)
315        let mnemonic = "test test test test test test test test test test test junk";
316        let wallet = Wallet::from_mnemonic(mnemonic, 0).unwrap();
317
318        // Should create a valid address
319        assert!(wallet.address().starts_with("0x"));
320        assert_eq!(wallet.address().len(), 42);
321    }
322
323    #[test]
324    fn test_wallet_with_chain_id() {
325        let wallet = Wallet::new_random().with_chain_id(1);
326        assert_eq!(wallet.chain_id(), Some(1));
327    }
328
329    #[tokio::test]
330    async fn test_sign_message() {
331        let wallet = Wallet::new_random();
332        let message = "Hello, Ethereum!";
333
334        let signature = wallet.sign_message(message).await.unwrap();
335
336        // Signature should be 65 bytes (r: 32, s: 32, v: 1)
337        let sig_bytes = signature.to_vec();
338        assert_eq!(sig_bytes.len(), 65);
339    }
340
341    #[test]
342    fn test_wallet_manager() {
343        let mut manager = WalletManager::new();
344
345        // Create wallets
346        let idx1 = manager.create_wallet();
347        let idx2 = manager.create_wallet();
348
349        assert_eq!(manager.wallet_count(), 2);
350        assert_eq!(idx1, 0);
351        assert_eq!(idx2, 1);
352
353        // Test active wallet
354        assert!(manager.active_wallet().is_some());
355
356        // Change active wallet
357        manager.set_active(1).unwrap();
358        assert!(manager.active_wallet().is_some());
359
360        // List addresses
361        let addresses = manager.list_addresses();
362        assert_eq!(addresses.len(), 2);
363    }
364
365    #[test]
366    fn test_wallet_manager_import() {
367        let mut manager = WalletManager::new();
368
369        let private_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
370        let idx = manager.import_wallet(private_key).unwrap();
371
372        assert_eq!(idx, 0);
373        assert_eq!(manager.wallet_count(), 1);
374
375        let wallet = manager.wallet(0).unwrap();
376        assert_eq!(
377            wallet.address().to_lowercase(),
378            "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"
379        );
380    }
381}