apex_sdk_substrate/
wallet.rs

1//! Substrate wallet and account management
2//!
3//! This module provides comprehensive wallet functionality including:
4//! - Key pair generation (SR25519, ED25519)
5//! - Mnemonic phrase support (BIP-39)
6//! - SS58 address encoding
7//! - Message and transaction signing
8//! - Multi-wallet management
9//!
10//! # Security
11//!
12//! This module handles sensitive cryptographic material (private keys).
13//! The underlying `sp_core::Pair` types implement secure memory handling.
14//! For additional safety:
15//! - Avoid unnecessary cloning of wallet instances
16//! - Use `Arc<Wallet>` for shared access
17//! - Ensure wallets are dropped when no longer needed
18
19use crate::{Error, Result};
20use parking_lot::RwLock;
21use sp_core::crypto::{Ss58AddressFormat, Ss58Codec};
22use sp_core::{ed25519, sr25519, Pair as PairTrait};
23use std::collections::HashMap;
24use std::sync::Arc;
25use tracing::{debug, info};
26
27/// Supported key pair types
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum KeyPairType {
30    /// SR25519 (Schnorrkel) - Default for Substrate
31    #[default]
32    Sr25519,
33    /// ED25519 - Alternative signing algorithm
34    Ed25519,
35}
36
37/// A unified wallet that can hold either SR25519 or ED25519 keys
38///
39/// # Security
40///
41/// This struct handles sensitive cryptographic material and implements
42/// secure memory practices:
43///
44/// - **Cloning**: This struct implements `Clone` to support wallet management.
45///   Cloning duplicates private key material in memory. For shared access
46///   without duplication, wrap in `Arc<Wallet>`.
47///
48/// - **Memory Zeroing**: The underlying `sp_core::Pair` types (SR25519 and ED25519)
49///   implement secure memory clearing on drop. However, cloned instances each
50///   maintain their own copy until dropped.
51///
52/// - **Best Practices**:
53///   - Minimize the lifetime of wallet instances
54///   - Use `Arc<Wallet>` for shared access
55///   - Avoid logging or printing wallet contents
56///   - Drop wallets explicitly when no longer needed
57///
58/// # Example
59///
60/// ```rust
61/// use apex_sdk_substrate::wallet::{Wallet, KeyPairType};
62/// use std::sync::Arc;
63///
64/// // For single ownership
65/// let wallet = Wallet::new_random();
66/// // Use wallet...
67/// drop(wallet); // Explicitly drop when done
68///
69/// // For shared access (recommended)
70/// let wallet = Arc::new(Wallet::new_random());
71/// let wallet_clone = Arc::clone(&wallet); // Increments reference count, no key duplication
72/// ```
73#[derive(Clone)]
74pub struct Wallet {
75    /// The key pair type
76    key_type: KeyPairType,
77    /// SR25519 pair (if applicable)
78    sr25519_pair: Option<sr25519::Pair>,
79    /// ED25519 pair (if applicable)
80    ed25519_pair: Option<ed25519::Pair>,
81    /// SS58 address format (network prefix)
82    ss58_format: Ss58AddressFormat,
83}
84
85impl Wallet {
86    /// Create a new random wallet with SR25519 keys
87    pub fn new_random() -> Self {
88        Self::new_random_with_type(KeyPairType::Sr25519)
89    }
90
91    /// Create a new random wallet with specified key type
92    pub fn new_random_with_type(key_type: KeyPairType) -> Self {
93        info!("Creating new random {:?} wallet", key_type);
94
95        match key_type {
96            KeyPairType::Sr25519 => {
97                let (pair, _seed) = sr25519::Pair::generate();
98                Self {
99                    key_type,
100                    sr25519_pair: Some(pair),
101                    ed25519_pair: None,
102                    ss58_format: Ss58AddressFormat::custom(42), // Default to generic
103                }
104            }
105            KeyPairType::Ed25519 => {
106                let (pair, _seed) = ed25519::Pair::generate();
107                Self {
108                    key_type,
109                    sr25519_pair: None,
110                    ed25519_pair: Some(pair),
111                    ss58_format: Ss58AddressFormat::custom(42),
112                }
113            }
114        }
115    }
116
117    /// Create wallet from mnemonic phrase
118    pub fn from_mnemonic(mnemonic: &str, key_type: KeyPairType) -> Result<Self> {
119        Self::from_mnemonic_with_path(mnemonic, None, key_type)
120    }
121
122    /// Create wallet from mnemonic phrase with derivation path
123    pub fn from_mnemonic_with_path(
124        mnemonic: &str,
125        path: Option<&str>,
126        key_type: KeyPairType,
127    ) -> Result<Self> {
128        info!("Creating wallet from mnemonic with {:?} keys", key_type);
129
130        // Validate mnemonic
131        let _ = bip39::Mnemonic::parse(mnemonic)
132            .map_err(|e| Error::Wallet(format!("Invalid mnemonic: {}", e)))?;
133
134        // Create derivation path string
135        let full_path = if let Some(p) = path {
136            format!("{}//{}", mnemonic, p)
137        } else {
138            mnemonic.to_string()
139        };
140
141        match key_type {
142            KeyPairType::Sr25519 => {
143                let pair = sr25519::Pair::from_string(&full_path, None)
144                    .map_err(|e| Error::Wallet(format!("Failed to derive key: {:?}", e)))?;
145
146                Ok(Self {
147                    key_type,
148                    sr25519_pair: Some(pair),
149                    ed25519_pair: None,
150                    ss58_format: Ss58AddressFormat::custom(42),
151                })
152            }
153            KeyPairType::Ed25519 => {
154                let pair = ed25519::Pair::from_string(&full_path, None)
155                    .map_err(|e| Error::Wallet(format!("Failed to derive key: {:?}", e)))?;
156
157                Ok(Self {
158                    key_type,
159                    sr25519_pair: None,
160                    ed25519_pair: Some(pair),
161                    ss58_format: Ss58AddressFormat::custom(42),
162                })
163            }
164        }
165    }
166
167    /// Create wallet from private key (seed)
168    pub fn from_seed(seed: &[u8], key_type: KeyPairType) -> Result<Self> {
169        info!("Creating wallet from seed with {:?} keys", key_type);
170
171        if seed.len() != 32 {
172            return Err(Error::Wallet("Seed must be 32 bytes".to_string()));
173        }
174
175        let mut seed_array = [0u8; 32];
176        seed_array.copy_from_slice(seed);
177
178        match key_type {
179            KeyPairType::Sr25519 => {
180                let pair = sr25519::Pair::from_seed(&seed_array);
181                Ok(Self {
182                    key_type,
183                    sr25519_pair: Some(pair),
184                    ed25519_pair: None,
185                    ss58_format: Ss58AddressFormat::custom(42),
186                })
187            }
188            KeyPairType::Ed25519 => {
189                let pair = ed25519::Pair::from_seed(&seed_array);
190                Ok(Self {
191                    key_type,
192                    sr25519_pair: None,
193                    ed25519_pair: Some(pair),
194                    ss58_format: Ss58AddressFormat::custom(42),
195                })
196            }
197        }
198    }
199
200    /// Generate a new mnemonic phrase
201    pub fn generate_mnemonic() -> String {
202        use bip39::{Language, Mnemonic};
203        use rand::RngCore;
204
205        let mut entropy = [0u8; 32];
206        rand::rng().fill_bytes(&mut entropy);
207
208        Mnemonic::from_entropy_in(Language::English, &entropy)
209            .expect("Failed to generate mnemonic")
210            .to_string()
211    }
212
213    /// Set the SS58 address format (network prefix)
214    pub fn with_ss58_format(mut self, format: u16) -> Self {
215        self.ss58_format = Ss58AddressFormat::custom(format);
216        self
217    }
218
219    /// Get the public key as bytes
220    pub fn public_key(&self) -> Vec<u8> {
221        match self.key_type {
222            KeyPairType::Sr25519 => self.sr25519_pair.as_ref().unwrap().public().0.to_vec(),
223            KeyPairType::Ed25519 => self.ed25519_pair.as_ref().unwrap().public().0.to_vec(),
224        }
225    }
226
227    /// Get the SS58-encoded address
228    pub fn address(&self) -> String {
229        match self.key_type {
230            KeyPairType::Sr25519 => {
231                let public = self.sr25519_pair.as_ref().unwrap().public();
232                public.to_ss58check_with_version(self.ss58_format)
233            }
234            KeyPairType::Ed25519 => {
235                let public = self.ed25519_pair.as_ref().unwrap().public();
236                public.to_ss58check_with_version(self.ss58_format)
237            }
238        }
239    }
240
241    /// Get the key pair type
242    pub fn key_type(&self) -> KeyPairType {
243        self.key_type
244    }
245
246    /// Sign a message
247    pub fn sign(&self, message: &[u8]) -> Vec<u8> {
248        match self.key_type {
249            KeyPairType::Sr25519 => {
250                let pair = self.sr25519_pair.as_ref().unwrap();
251                pair.sign(message).0.to_vec()
252            }
253            KeyPairType::Ed25519 => {
254                let pair = self.ed25519_pair.as_ref().unwrap();
255                pair.sign(message).0.to_vec()
256            }
257        }
258    }
259
260    /// Verify a signature
261    pub fn verify(&self, message: &[u8], signature: &[u8]) -> bool {
262        match self.key_type {
263            KeyPairType::Sr25519 => {
264                if signature.len() != 64 {
265                    return false;
266                }
267                let mut sig_array = [0u8; 64];
268                sig_array.copy_from_slice(signature);
269                let sig = sr25519::Signature::from_raw(sig_array);
270                let public = self.sr25519_pair.as_ref().unwrap().public();
271                sr25519::Pair::verify(&sig, message, &public)
272            }
273            KeyPairType::Ed25519 => {
274                if signature.len() != 64 {
275                    return false;
276                }
277                let mut sig_array = [0u8; 64];
278                sig_array.copy_from_slice(signature);
279                let sig = ed25519::Signature::from_raw(sig_array);
280                let public = self.ed25519_pair.as_ref().unwrap().public();
281                ed25519::Pair::verify(&sig, message, &public)
282            }
283        }
284    }
285
286    /// Get the seed/private key (if available)
287    /// Note: This should be kept secure and not exposed in production
288    pub fn seed(&self) -> Option<[u8; 32]> {
289        match self.key_type {
290            KeyPairType::Sr25519 => {
291                // SR25519 doesn't expose seed directly in a simple way
292                None
293            }
294            KeyPairType::Ed25519 => {
295                // ED25519 also doesn't expose seed directly
296                None
297            }
298        }
299    }
300
301    /// Get the SR25519 pair for signing (if this is an SR25519 wallet)
302    pub fn sr25519_pair(&self) -> Option<&sr25519::Pair> {
303        self.sr25519_pair.as_ref()
304    }
305
306    /// Get the ED25519 pair for signing (if this is an ED25519 wallet)
307    pub fn ed25519_pair(&self) -> Option<&ed25519::Pair> {
308        self.ed25519_pair.as_ref()
309    }
310}
311
312impl std::fmt::Debug for Wallet {
313    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314        f.debug_struct("Wallet")
315            .field("key_type", &self.key_type)
316            .field("address", &self.address())
317            .field("ss58_format", &self.ss58_format)
318            .finish()
319    }
320}
321
322impl Drop for Wallet {
323    fn drop(&mut self) {
324        // Log wallet cleanup for security auditing
325        // Note: sp_core::Pair types handle actual memory zeroing internally
326        debug!(
327            "Dropping wallet of type {:?} at address {}",
328            self.key_type,
329            self.address()
330        );
331    }
332}
333
334/// Manager for multiple wallets
335pub struct WalletManager {
336    wallets: Arc<RwLock<HashMap<String, Wallet>>>,
337    default_key_type: KeyPairType,
338}
339
340impl WalletManager {
341    /// Create a new wallet manager
342    pub fn new() -> Self {
343        Self {
344            wallets: Arc::new(RwLock::new(HashMap::new())),
345            default_key_type: KeyPairType::Sr25519,
346        }
347    }
348
349    /// Create a new wallet manager with default key type
350    pub fn with_key_type(key_type: KeyPairType) -> Self {
351        Self {
352            wallets: Arc::new(RwLock::new(HashMap::new())),
353            default_key_type: key_type,
354        }
355    }
356
357    /// Create and add a new random wallet
358    pub fn create_wallet(&self, name: impl Into<String>) -> Wallet {
359        let wallet = Wallet::new_random_with_type(self.default_key_type);
360        let name = name.into();
361
362        debug!("Creating wallet '{}' at address {}", name, wallet.address());
363
364        self.wallets.write().insert(name.clone(), wallet.clone());
365        wallet
366    }
367
368    /// Add an existing wallet
369    pub fn add_wallet(&self, name: impl Into<String>, wallet: Wallet) {
370        let name = name.into();
371        debug!("Adding wallet '{}' at address {}", name, wallet.address());
372        self.wallets.write().insert(name, wallet);
373    }
374
375    /// Get a wallet by name
376    pub fn get_wallet(&self, name: &str) -> Option<Wallet> {
377        self.wallets.read().get(name).cloned()
378    }
379
380    /// Remove a wallet
381    pub fn remove_wallet(&self, name: &str) -> Option<Wallet> {
382        debug!("Removing wallet '{}'", name);
383        self.wallets.write().remove(name)
384    }
385
386    /// List all wallet names
387    pub fn list_wallets(&self) -> Vec<String> {
388        self.wallets.read().keys().cloned().collect()
389    }
390
391    /// Get number of wallets
392    pub fn wallet_count(&self) -> usize {
393        self.wallets.read().len()
394    }
395
396    /// Clear all wallets
397    pub fn clear(&self) {
398        debug!("Clearing all wallets");
399        self.wallets.write().clear();
400    }
401}
402
403impl Default for WalletManager {
404    fn default() -> Self {
405        Self::new()
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412
413    #[test]
414    fn test_create_random_wallet() {
415        let wallet = Wallet::new_random();
416        assert_eq!(wallet.key_type(), KeyPairType::Sr25519);
417        assert!(!wallet.address().is_empty());
418        assert!(!wallet.public_key().is_empty());
419    }
420
421    #[test]
422    fn test_create_wallet_types() {
423        let sr25519_wallet = Wallet::new_random_with_type(KeyPairType::Sr25519);
424        assert_eq!(sr25519_wallet.key_type(), KeyPairType::Sr25519);
425
426        let ed25519_wallet = Wallet::new_random_with_type(KeyPairType::Ed25519);
427        assert_eq!(ed25519_wallet.key_type(), KeyPairType::Ed25519);
428    }
429
430    #[test]
431    fn test_sign_and_verify() {
432        let wallet = Wallet::new_random();
433        let message = b"Hello, Substrate!";
434
435        let signature = wallet.sign(message);
436        assert_eq!(signature.len(), 64);
437
438        assert!(wallet.verify(message, &signature));
439        assert!(!wallet.verify(b"Different message", &signature));
440    }
441
442    #[test]
443    fn test_generate_mnemonic() {
444        let mnemonic = Wallet::generate_mnemonic();
445        assert!(!mnemonic.is_empty());
446
447        // Should be able to create a wallet from it
448        let wallet = Wallet::from_mnemonic(&mnemonic, KeyPairType::Sr25519);
449        assert!(wallet.is_ok());
450    }
451
452    #[test]
453    fn test_wallet_from_mnemonic() {
454        let mnemonic = "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
455
456        let wallet1 = Wallet::from_mnemonic(mnemonic, KeyPairType::Sr25519).unwrap();
457        let wallet2 = Wallet::from_mnemonic(mnemonic, KeyPairType::Sr25519).unwrap();
458
459        // Same mnemonic should produce same address
460        assert_eq!(wallet1.address(), wallet2.address());
461    }
462
463    #[test]
464    fn test_wallet_with_ss58_format() {
465        let wallet = Wallet::new_random().with_ss58_format(0); // Polkadot
466        let address = wallet.address();
467
468        // Polkadot addresses start with '1'
469        assert!(address.starts_with('1'));
470    }
471
472    #[test]
473    fn test_wallet_from_seed() {
474        let seed = [42u8; 32];
475        let wallet1 = Wallet::from_seed(&seed, KeyPairType::Sr25519).unwrap();
476        let wallet2 = Wallet::from_seed(&seed, KeyPairType::Sr25519).unwrap();
477
478        // Same seed should produce same address
479        assert_eq!(wallet1.address(), wallet2.address());
480    }
481
482    #[test]
483    fn test_wallet_manager() {
484        let manager = WalletManager::new();
485
486        let wallet1 = manager.create_wallet("wallet1");
487        assert_eq!(manager.wallet_count(), 1);
488
489        let wallet2 = Wallet::new_random();
490        manager.add_wallet("wallet2", wallet2.clone());
491        assert_eq!(manager.wallet_count(), 2);
492
493        let retrieved = manager.get_wallet("wallet1").unwrap();
494        assert_eq!(retrieved.address(), wallet1.address());
495
496        let names = manager.list_wallets();
497        assert_eq!(names.len(), 2);
498        assert!(names.contains(&"wallet1".to_string()));
499        assert!(names.contains(&"wallet2".to_string()));
500
501        manager.remove_wallet("wallet1");
502        assert_eq!(manager.wallet_count(), 1);
503
504        manager.clear();
505        assert_eq!(manager.wallet_count(), 0);
506    }
507
508    #[test]
509    fn test_different_key_types_produce_different_addresses() {
510        let seed = [42u8; 32];
511        let sr25519_wallet = Wallet::from_seed(&seed, KeyPairType::Sr25519).unwrap();
512        let ed25519_wallet = Wallet::from_seed(&seed, KeyPairType::Ed25519).unwrap();
513
514        // Different cryptographic algorithms (SR25519 vs ED25519) interpret the same seed differently,
515        // resulting in distinct key pairs and thus different addresses.
516        assert_ne!(sr25519_wallet.address(), ed25519_wallet.address());
517    }
518}