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