Skip to main content

mx_core/
wallet.rs

1//! Wallet derivation utilities for `MultiversX`.
2//!
3//! This module provides shared functionality for deriving Ed25519 signing keys
4//! and bech32 addresses from BIP39 mnemonics using the `MultiversX` wallet derivation path.
5
6use crate::error::CoreError;
7use crate::normalize_mnemonic;
8use bip39::Mnemonic;
9use ed25519_dalek::SigningKey;
10use multiversx_sdk::wallet;
11use std::convert::TryInto;
12
13/// Parses and normalizes a mnemonic string into a validated `Mnemonic`.
14///
15/// Handles commas, extra whitespace, and validates the BIP39 mnemonic.
16fn parse_mnemonic(mnemonic_str: &str) -> Result<Mnemonic, CoreError> {
17    let mnemonic_phrase = normalize_mnemonic(mnemonic_str);
18    Mnemonic::parse(&mnemonic_phrase).map_err(|e| CoreError::InvalidMnemonic(e.to_string()))
19}
20
21/// Derives an Ed25519 signing key from a BIP39 mnemonic at the given account and index.
22///
23/// Uses the `MultiversX` wallet derivation path: m/44'/508'/{account}'/0'/{index}
24///
25/// # Arguments
26/// * `mnemonic_str` - BIP39 mnemonic phrase (12 or 24 words)
27/// * `account` - Account number in the derivation path (typically 0 for relayer, 1+ for stress testing)
28/// * `index` - Address index within the account
29///
30/// # Returns
31/// The Ed25519 signing key, or an error if derivation fails.
32///
33/// # Examples
34/// ```
35/// use mx_core::derive_signing_key;
36///
37/// let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
38/// let key = derive_signing_key(mnemonic, 0, 0).unwrap();
39/// ```
40pub fn derive_signing_key(
41    mnemonic_str: &str,
42    account: u32,
43    index: u32,
44) -> Result<SigningKey, CoreError> {
45    let mnemonic = parse_mnemonic(mnemonic_str)?;
46
47    // Use MultiversX SDK to derive the private key
48    let private_key = wallet::Wallet::get_private_key_from_mnemonic(mnemonic, account, index);
49
50    // Convert to hex string and then to ed25519 signing key
51    let private_key_hex = private_key.to_string();
52    let seed_bytes = hex::decode(&private_key_hex)
53        .map_err(|e| CoreError::InvalidPrivateKey(format!("invalid private key hex: {e}")))?;
54
55    // Ensure we have exactly 32 bytes
56    let seed: [u8; 32] = seed_bytes.as_slice().try_into().map_err(|_| {
57        CoreError::InvalidPrivateKey(format!("expected 32-byte seed, got {}", seed_bytes.len()))
58    })?;
59
60    Ok(SigningKey::from_bytes(&seed))
61}
62
63/// Derives a bech32-encoded address from a BIP39 mnemonic at the given account and index.
64///
65/// Uses the `MultiversX` wallet derivation path: m/44'/508'/{account}'/0'/{index}
66///
67/// # Arguments
68/// * `mnemonic_str` - BIP39 mnemonic phrase (12 or 24 words)
69/// * `account` - Account number in the derivation path (typically 0 for relayer, 1+ for stress testing)
70/// * `index` - Address index within the account
71///
72/// # Returns
73/// The bech32-encoded address (e.g., "erd1..."), or an error if derivation fails.
74///
75/// # Examples
76/// ```
77/// use mx_core::derive_address;
78///
79/// let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
80/// let address = derive_address(mnemonic, 0, 0).unwrap();
81/// assert!(address.starts_with("erd1"));
82/// ```
83pub fn derive_address(mnemonic_str: &str, account: u32, index: u32) -> Result<String, CoreError> {
84    let mnemonic = parse_mnemonic(mnemonic_str)?;
85
86    // Use MultiversX SDK to derive the private key
87    let private_key = wallet::Wallet::get_private_key_from_mnemonic(mnemonic, account, index);
88
89    // Convert to hex and create wallet to get the address
90    let private_key_hex = private_key.to_string();
91    let wallet_obj = wallet::Wallet::from_private_key(&private_key_hex)
92        .map_err(|e| CoreError::WalletCreation(e.to_string()))?;
93
94    Ok(wallet_obj.to_address().to_bech32_default().to_string())
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    // Test mnemonic for deterministic testing (DO NOT use in production)
102    const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
103
104    #[test]
105    fn test_derive_signing_key_success() {
106        let result = derive_signing_key(TEST_MNEMONIC, 0, 0);
107        assert!(result.is_ok());
108
109        // Derive twice to ensure determinism
110        let key1 = derive_signing_key(TEST_MNEMONIC, 0, 0).unwrap();
111        let key2 = derive_signing_key(TEST_MNEMONIC, 0, 0).unwrap();
112        assert_eq!(key1.to_bytes(), key2.to_bytes());
113    }
114
115    #[test]
116    fn test_derive_signing_key_invalid_mnemonic() {
117        let result = derive_signing_key("invalid mnemonic words that dont exist", 0, 0);
118        assert!(result.is_err());
119        assert!(result.unwrap_err().to_string().contains("invalid mnemonic"));
120    }
121
122    #[test]
123    fn test_derive_signing_key_different_indices() {
124        let key0 = derive_signing_key(TEST_MNEMONIC, 0, 0).unwrap();
125        let key1 = derive_signing_key(TEST_MNEMONIC, 0, 1).unwrap();
126        let key2 = derive_signing_key(TEST_MNEMONIC, 1, 0).unwrap();
127
128        // Different indices should produce different keys
129        assert_ne!(key0.to_bytes(), key1.to_bytes());
130        assert_ne!(key0.to_bytes(), key2.to_bytes());
131        assert_ne!(key1.to_bytes(), key2.to_bytes());
132    }
133
134    #[test]
135    fn test_derive_address_success() {
136        let result = derive_address(TEST_MNEMONIC, 0, 0);
137        assert!(result.is_ok());
138
139        let address = result.unwrap();
140        assert!(address.starts_with("erd1"));
141        assert_eq!(address.len(), 62); // MultiversX addresses are 62 chars
142    }
143
144    #[test]
145    fn test_derive_address_invalid_mnemonic() {
146        let result = derive_address("invalid mnemonic words that dont exist", 0, 0);
147        assert!(result.is_err());
148        assert!(result.unwrap_err().to_string().contains("invalid mnemonic"));
149    }
150
151    #[test]
152    fn test_derive_address_different_indices() {
153        let addr0 = derive_address(TEST_MNEMONIC, 0, 0).unwrap();
154        let addr1 = derive_address(TEST_MNEMONIC, 0, 1).unwrap();
155        let addr2 = derive_address(TEST_MNEMONIC, 1, 0).unwrap();
156
157        // Different indices should produce different addresses
158        assert_ne!(addr0, addr1);
159        assert_ne!(addr0, addr2);
160        assert_ne!(addr1, addr2);
161    }
162
163    #[test]
164    fn test_derive_address_deterministic() {
165        let addr1 = derive_address(TEST_MNEMONIC, 0, 0).unwrap();
166        let addr2 = derive_address(TEST_MNEMONIC, 0, 0).unwrap();
167        assert_eq!(addr1, addr2);
168    }
169
170    #[test]
171    fn test_normalize_mnemonic_in_derivation() {
172        // Test that normalization works with commas and extra spaces
173        let mnemonic_commas = "abandon,abandon,abandon,abandon,abandon,abandon,abandon,abandon,abandon,abandon,abandon,about";
174        let mnemonic_spaces = "abandon  abandon  abandon  abandon  abandon  abandon  abandon  abandon  abandon  abandon  abandon  about";
175        let mnemonic_normal = TEST_MNEMONIC;
176
177        let addr_commas = derive_address(mnemonic_commas, 0, 0).unwrap();
178        let addr_spaces = derive_address(mnemonic_spaces, 0, 0).unwrap();
179        let addr_normal = derive_address(mnemonic_normal, 0, 0).unwrap();
180
181        // All should produce the same address
182        assert_eq!(addr_commas, addr_normal);
183        assert_eq!(addr_spaces, addr_normal);
184    }
185
186    #[test]
187    fn test_signing_key_and_address_match() {
188        // Ensure that the signing key and address derived for the same index correspond
189        let signing_key = derive_signing_key(TEST_MNEMONIC, 0, 0).unwrap();
190        let address = derive_address(TEST_MNEMONIC, 0, 0).unwrap();
191
192        // Derive the address from the signing key's public key
193        let verifying_key = signing_key.verifying_key();
194        let pubkey_bytes = verifying_key.to_bytes();
195
196        // Decode the bech32 address to get the raw bytes
197        let (_hrp, addr_bytes) = bech32::decode(&address).unwrap();
198        let addr_bytes_array: [u8; 32] = addr_bytes.as_slice().try_into().unwrap();
199
200        // The address bytes should match the public key
201        assert_eq!(pubkey_bytes, addr_bytes_array);
202    }
203}