rustywallet_import/
mnemonic_import.rs

1//! Mnemonic (BIP39) importer.
2
3use crate::error::{ImportError, Result};
4use crate::types::{ImportMetadata, ImportResult, ImportFormat};
5use rustywallet_mnemonic::Mnemonic;
6use rustywallet_hd::{ExtendedPrivateKey, DerivationPath, Network};
7
8/// Configuration for mnemonic import.
9#[derive(Debug, Clone)]
10pub struct MnemonicImport {
11    /// The mnemonic phrase (12-24 words)
12    pub mnemonic: String,
13    /// Optional passphrase (BIP39)
14    pub passphrase: Option<String>,
15    /// Derivation path (default: m/44'/0'/0'/0/0)
16    pub path: Option<String>,
17    /// Network (default: Mainnet)
18    pub network: Option<Network>,
19}
20
21impl MnemonicImport {
22    /// Create a new mnemonic import config.
23    pub fn new(mnemonic: impl Into<String>) -> Self {
24        Self {
25            mnemonic: mnemonic.into(),
26            passphrase: None,
27            path: None,
28            network: None,
29        }
30    }
31    
32    /// Set passphrase.
33    pub fn with_passphrase(mut self, passphrase: impl Into<String>) -> Self {
34        self.passphrase = Some(passphrase.into());
35        self
36    }
37    
38    /// Set derivation path.
39    pub fn with_path(mut self, path: impl Into<String>) -> Self {
40        self.path = Some(path.into());
41        self
42    }
43    
44    /// Set network.
45    pub fn with_network(mut self, network: Network) -> Self {
46        self.network = Some(network);
47        self
48    }
49}
50
51/// Default derivation paths for different address types.
52pub mod paths {
53    /// BIP44 path for legacy addresses (P2PKH)
54    pub const BIP44: &str = "m/44'/0'/0'/0/0";
55    /// BIP49 path for SegWit-compatible addresses (P2SH-P2WPKH)
56    pub const BIP49: &str = "m/49'/0'/0'/0/0";
57    /// BIP84 path for native SegWit addresses (P2WPKH)
58    pub const BIP84: &str = "m/84'/0'/0'/0/0";
59}
60
61/// Import a private key from a mnemonic phrase.
62///
63/// # Example
64///
65/// ```rust
66/// use rustywallet_import::{import_mnemonic, MnemonicImport};
67///
68/// let config = MnemonicImport::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about")
69///     .with_path("m/44'/0'/0'/0/0");
70///
71/// let result = import_mnemonic(config).unwrap();
72/// println!("Imported key from {} word mnemonic", result.metadata.word_count.unwrap());
73/// ```
74pub fn import_mnemonic(config: MnemonicImport) -> Result<ImportResult> {
75    let mnemonic_str = config.mnemonic.trim();
76    
77    // Parse mnemonic
78    let mnemonic = Mnemonic::from_phrase(mnemonic_str)
79        .map_err(|e| ImportError::InvalidMnemonic(format!("{}", e)))?;
80    
81    let word_count = mnemonic_str.split_whitespace().count();
82    
83    // Get passphrase
84    let passphrase = config.passphrase.as_deref().unwrap_or("");
85    
86    // Generate seed
87    let seed = mnemonic.to_seed(passphrase);
88    
89    // Get network
90    let network = config.network.unwrap_or(Network::Mainnet);
91    
92    // Create master key
93    let master = ExtendedPrivateKey::from_seed(seed.as_bytes(), network)
94        .map_err(|e| ImportError::KeyDerivationFailed(format!("{}", e)))?;
95    
96    // Get derivation path
97    let path_str = config.path.as_deref().unwrap_or(paths::BIP44);
98    
99    // Parse and derive
100    let path: DerivationPath = path_str.parse()
101        .map_err(|e| ImportError::KeyDerivationFailed(format!("Invalid path: {}", e)))?;
102    
103    let derived = master.derive_path(&path)
104        .map_err(|e| ImportError::KeyDerivationFailed(format!("{}", e)))?;
105    
106    // Get private key
107    let private_key = derived.private_key()
108        .map_err(|e| ImportError::KeyDerivationFailed(format!("{}", e)))?;
109    
110    // Build metadata
111    let metadata = ImportMetadata {
112        derivation_path: Some(path_str.to_string()),
113        word_count: Some(word_count),
114        has_passphrase: !passphrase.is_empty(),
115    };
116    
117    Ok(ImportResult::new(private_key, ImportFormat::Mnemonic)
118        .with_network(network)
119        .with_compressed(true)
120        .with_metadata(metadata))
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    
127    #[test]
128    fn test_import_12_word_mnemonic() {
129        let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
130        let config = MnemonicImport::new(mnemonic);
131        let result = import_mnemonic(config).unwrap();
132        
133        assert_eq!(result.format, ImportFormat::Mnemonic);
134        assert_eq!(result.metadata.word_count, Some(12));
135        assert!(!result.metadata.has_passphrase);
136    }
137    
138    #[test]
139    fn test_import_with_passphrase() {
140        let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
141        let config = MnemonicImport::new(mnemonic)
142            .with_passphrase("TREZOR");
143        let result = import_mnemonic(config).unwrap();
144        
145        assert!(result.metadata.has_passphrase);
146    }
147    
148    #[test]
149    fn test_import_with_custom_path() {
150        let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
151        let config = MnemonicImport::new(mnemonic)
152            .with_path("m/84'/0'/0'/0/0");
153        let result = import_mnemonic(config).unwrap();
154        
155        assert_eq!(result.metadata.derivation_path, Some("m/84'/0'/0'/0/0".to_string()));
156    }
157    
158    #[test]
159    fn test_invalid_mnemonic() {
160        let mnemonic = "invalid words that are not a valid mnemonic phrase at all";
161        let config = MnemonicImport::new(mnemonic);
162        let result = import_mnemonic(config);
163        
164        assert!(matches!(result, Err(ImportError::InvalidMnemonic(_))));
165    }
166    
167    #[test]
168    fn test_deterministic() {
169        let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
170        
171        let config1 = MnemonicImport::new(mnemonic).with_path("m/44'/0'/0'/0/0");
172        let config2 = MnemonicImport::new(mnemonic).with_path("m/44'/0'/0'/0/0");
173        
174        let result1 = import_mnemonic(config1).unwrap();
175        let result2 = import_mnemonic(config2).unwrap();
176        
177        assert_eq!(result1.private_key.to_bytes(), result2.private_key.to_bytes());
178    }
179}