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