1use crate::{Wallet, WalletError};
4
5use coins_bip32::path::DerivationPath;
6use coins_bip39::{Mnemonic, Wordlist};
7use ethers_core::{
8 k256::ecdsa::SigningKey,
9 types::PathOrString,
10 utils::{secret_key_to_address, to_checksum},
11};
12use rand::Rng;
13use std::{fs::File, io::Write, marker::PhantomData, path::PathBuf, str::FromStr};
14use thiserror::Error;
15
16const DEFAULT_DERIVATION_PATH_PREFIX: &str = "m/44'/60'/0'/0/";
17
18#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct MnemonicBuilder<W: Wordlist> {
21 phrase: Option<PathOrString>,
24 word_count: usize,
27 derivation_path: DerivationPath,
30 password: Option<String>,
32 write_to: Option<PathBuf>,
35 _wordlist: PhantomData<W>,
37}
38
39#[derive(Error, Debug)]
41pub enum MnemonicBuilderError {
42 #[error("Expected phrase not found")]
44 ExpectedPhraseNotFound,
45 #[error("Unexpected phrase found")]
47 UnexpectedPhraseFound,
48}
49
50impl<W: Wordlist> Default for MnemonicBuilder<W> {
51 fn default() -> Self {
52 Self {
53 phrase: None,
54 word_count: 12usize,
55 derivation_path: DerivationPath::from_str(&format!(
56 "{}{}",
57 DEFAULT_DERIVATION_PATH_PREFIX, 0
58 ))
59 .expect("should parse the default derivation path"),
60 password: None,
61 write_to: None,
62 _wordlist: PhantomData,
63 }
64 }
65}
66
67impl<W: Wordlist> MnemonicBuilder<W> {
68 #[must_use]
86 pub fn phrase<P: Into<PathOrString>>(mut self, phrase: P) -> Self {
87 self.phrase = Some(phrase.into());
88 self
89 }
90
91 #[must_use]
109 pub fn word_count(mut self, count: usize) -> Self {
110 self.word_count = count;
111 self
112 }
113
114 pub fn index<U: Into<u32>>(mut self, index: U) -> Result<Self, WalletError> {
117 self.derivation_path = DerivationPath::from_str(&format!(
118 "{}{}",
119 DEFAULT_DERIVATION_PATH_PREFIX,
120 index.into()
121 ))?;
122 Ok(self)
123 }
124
125 pub fn derivation_path(mut self, path: &str) -> Result<Self, WalletError> {
127 self.derivation_path = DerivationPath::from_str(path)?;
128 Ok(self)
129 }
130
131 #[must_use]
133 pub fn password(mut self, password: &str) -> Self {
134 self.password = Some(password.to_string());
135 self
136 }
137
138 #[must_use]
141 pub fn write_to<P: Into<PathBuf>>(mut self, path: P) -> Self {
142 self.write_to = Some(path.into());
143 self
144 }
145
146 pub fn build(&self) -> Result<Wallet<SigningKey>, WalletError> {
149 let mnemonic = match &self.phrase {
150 Some(path_or_string) => {
151 let phrase = path_or_string.read()?;
152 Mnemonic::<W>::new_from_phrase(&phrase)?
153 }
154 None => return Err(MnemonicBuilderError::ExpectedPhraseNotFound.into()),
155 };
156 self.mnemonic_to_wallet(&mnemonic)
157 }
158
159 pub fn build_random<R: Rng>(&self, rng: &mut R) -> Result<Wallet<SigningKey>, WalletError> {
162 let mnemonic = match &self.phrase {
163 None => Mnemonic::<W>::new_with_count(rng, self.word_count)?,
164 _ => return Err(MnemonicBuilderError::UnexpectedPhraseFound.into()),
165 };
166 let wallet = self.mnemonic_to_wallet(&mnemonic)?;
167
168 if let Some(dir) = &self.write_to {
170 let mut file = File::create(dir.as_path().join(to_checksum(&wallet.address, None)))?;
171 file.write_all(mnemonic.to_phrase().as_bytes())?;
172 }
173
174 Ok(wallet)
175 }
176
177 fn mnemonic_to_wallet(
178 &self,
179 mnemonic: &Mnemonic<W>,
180 ) -> Result<Wallet<SigningKey>, WalletError> {
181 let derived_priv_key =
182 mnemonic.derive_key(&self.derivation_path, self.password.as_deref())?;
183 let key: &coins_bip32::prelude::SigningKey = derived_priv_key.as_ref();
184 let signer = SigningKey::from_bytes(&key.to_bytes())?;
185 let address = secret_key_to_address(&signer);
186
187 Ok(Wallet::<SigningKey> { signer, address, chain_id: 1 })
188 }
189}
190
191#[cfg(test)]
192#[cfg(not(target_arch = "wasm32"))]
193mod tests {
194 use super::*;
195
196 use crate::coins_bip39::English;
197 use tempfile::tempdir;
198
199 const TEST_DERIVATION_PATH: &str = "m/44'/60'/0'/2/1";
200
201 #[tokio::test]
202 async fn mnemonic_deterministic() {
203 const TESTCASES: [(&str, u32, Option<&str>, &str); 4] = [
205 (
206 "work man father plunge mystery proud hollow address reunion sauce theory bonus",
207 0u32,
208 Some("TREZOR123"),
209 "0x431a00DA1D54c281AeF638A73121B3D153e0b0F6",
210 ),
211 (
212 "inject danger program federal spice bitter term garbage coyote breeze thought funny",
213 1u32,
214 Some("LEDGER321"),
215 "0x231a3D0a05d13FAf93078C779FeeD3752ea1350C",
216 ),
217 (
218 "fire evolve buddy tenant talent favorite ankle stem regret myth dream fresh",
219 2u32,
220 None,
221 "0x1D86AD5eBb2380dAdEAF52f61f4F428C485460E9",
222 ),
223 (
224 "thumb soda tape crunch maple fresh imitate cancel order blind denial giraffe",
225 3u32,
226 None,
227 "0xFB78b25f69A8e941036fEE2A5EeAf349D81D4ccc",
228 ),
229 ];
230 TESTCASES.iter().for_each(|(phrase, index, password, expected_addr)| {
231 let wallet = match password {
232 Some(psswd) => MnemonicBuilder::<English>::default()
233 .phrase(*phrase)
234 .index(*index)
235 .unwrap()
236 .password(psswd)
237 .build()
238 .unwrap(),
239 None => MnemonicBuilder::<English>::default()
240 .phrase(*phrase)
241 .index(*index)
242 .unwrap()
243 .build()
244 .unwrap(),
245 };
246 assert_eq!(&to_checksum(&wallet.address, None), expected_addr);
247 })
248 }
249
250 #[tokio::test]
251 async fn mnemonic_write_read() {
252 let dir = tempdir().unwrap();
253
254 let mut rng = rand::thread_rng();
256 let wallet1 = MnemonicBuilder::<English>::default()
257 .word_count(24)
258 .derivation_path(TEST_DERIVATION_PATH)
259 .unwrap()
260 .write_to(dir.as_ref())
261 .build_random(&mut rng)
262 .unwrap();
263
264 let paths = std::fs::read_dir(dir.as_ref()).unwrap();
266 assert_eq!(paths.count(), 1);
267
268 let phrase_path = dir.as_ref().join(to_checksum(&wallet1.address, None));
270 let wallet2 = MnemonicBuilder::<English>::default()
271 .phrase(phrase_path.to_str().unwrap())
272 .derivation_path(TEST_DERIVATION_PATH)
273 .unwrap()
274 .build()
275 .unwrap();
276
277 assert_eq!(wallet1.address, wallet2.address);
279
280 dir.close().unwrap();
281 }
282}