rustywallet_import/
mnemonic_import.rs1use crate::error::{ImportError, Result};
4use crate::types::{ImportMetadata, ImportResult, ImportFormat};
5use rustywallet_mnemonic::Mnemonic;
6use rustywallet_hd::{ExtendedPrivateKey, DerivationPath, Network};
7
8#[derive(Debug, Clone)]
10pub struct MnemonicImport {
11 pub mnemonic: String,
13 pub passphrase: Option<String>,
15 pub path: Option<String>,
17 pub network: Option<Network>,
19}
20
21impl MnemonicImport {
22 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 pub fn with_passphrase(mut self, passphrase: impl Into<String>) -> Self {
34 self.passphrase = Some(passphrase.into());
35 self
36 }
37
38 pub fn with_path(mut self, path: impl Into<String>) -> Self {
40 self.path = Some(path.into());
41 self
42 }
43
44 pub fn with_network(mut self, network: Network) -> Self {
46 self.network = Some(network);
47 self
48 }
49}
50
51pub mod paths {
53 pub const BIP44: &str = "m/44'/0'/0'/0/0";
55 pub const BIP49: &str = "m/49'/0'/0'/0/0";
57 pub const BIP84: &str = "m/84'/0'/0'/0/0";
59}
60
61pub fn import_mnemonic(config: MnemonicImport) -> Result<ImportResult> {
75 let mnemonic_str = config.mnemonic.trim();
76
77 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 let passphrase = config.passphrase.as_deref().unwrap_or("");
85
86 let seed = mnemonic.to_seed(passphrase);
88
89 let network = config.network.unwrap_or(Network::Mainnet);
91
92 let master = ExtendedPrivateKey::from_seed(seed.as_bytes(), network)
94 .map_err(|e| ImportError::KeyDerivationFailed(format!("{}", e)))?;
95
96 let path_str = config.path.as_deref().unwrap_or(paths::BIP44);
98
99 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 let private_key = derived.private_key()
108 .map_err(|e| ImportError::KeyDerivationFailed(format!("{}", e)))?;
109
110 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}