alloy_signer_local/
mnemonic.rs1use crate::{LocalSigner, LocalSignerError, PrivateKeySigner};
7use alloy_signer::utils::secret_key_to_address;
8use coins_bip32::path::DerivationPath;
9use coins_bip39::{English, Mnemonic, Wordlist};
10use k256::ecdsa::SigningKey;
11use rand::Rng;
12use std::{marker::PhantomData, path::PathBuf};
13use thiserror::Error;
14
15const DEFAULT_DERIVATION_PATH_PREFIX: &str = "m/44'/60'/0'/0/";
16const DEFAULT_DERIVATION_PATH: &str = "m/44'/60'/0'/0/0";
17
18#[derive(Clone, Debug, PartialEq, Eq)]
20#[must_use = "builders do nothing unless `build` is called"]
21pub struct MnemonicBuilder<W: Wordlist = English> {
22 phrase: Option<String>,
25 word_count: usize,
28 derivation_path: DerivationPath,
31 password: Option<String>,
33 write_to: Option<PathBuf>,
36 _wordlist: PhantomData<W>,
38}
39
40#[derive(Debug, Error)]
42pub enum MnemonicBuilderError {
43 #[error("expected phrase not found")]
45 ExpectedPhraseNotFound,
46 #[error("unexpected phrase found")]
48 UnexpectedPhraseFound,
49}
50
51impl<W: Wordlist> Default for MnemonicBuilder<W> {
52 fn default() -> Self {
53 Self {
54 phrase: None,
55 word_count: 12usize,
56 derivation_path: DEFAULT_DERIVATION_PATH.parse().unwrap(),
57 password: None,
58 write_to: None,
59 _wordlist: PhantomData,
60 }
61 }
62}
63
64impl<W: Wordlist> MnemonicBuilder<W> {
65 pub fn phrase<P: Into<String>>(mut self, phrase: P) -> Self {
82 self.phrase = Some(phrase.into());
83 self
84 }
85
86 pub const fn word_count(mut self, count: usize) -> Self {
100 self.word_count = count;
101 self
102 }
103
104 pub fn index(self, index: u32) -> Result<Self, LocalSignerError> {
107 self.derivation_path(format!("{DEFAULT_DERIVATION_PATH_PREFIX}{index}"))
108 }
109
110 pub fn derivation_path<T: AsRef<str>>(mut self, path: T) -> Result<Self, LocalSignerError> {
112 self.derivation_path = path.as_ref().parse()?;
113 Ok(self)
114 }
115
116 pub fn password<T: Into<String>>(mut self, password: T) -> Self {
118 self.password = Some(password.into());
119 self
120 }
121
122 pub fn write_to<P: Into<PathBuf>>(mut self, path: P) -> Self {
125 self.write_to = Some(path.into());
126 self
127 }
128
129 pub fn build(&self) -> Result<PrivateKeySigner, LocalSignerError> {
132 let mnemonic = match &self.phrase {
133 Some(phrase) => Mnemonic::<W>::new_from_phrase(phrase)?,
134 None => return Err(MnemonicBuilderError::ExpectedPhraseNotFound.into()),
135 };
136 self.mnemonic_to_signer(&mnemonic)
137 }
138
139 pub fn build_random(&self) -> Result<PrivateKeySigner, LocalSignerError> {
142 self.build_random_with(&mut rand::thread_rng())
143 }
144
145 pub fn build_random_with<R: Rng>(
148 &self,
149 rng: &mut R,
150 ) -> Result<PrivateKeySigner, LocalSignerError> {
151 let mnemonic = match &self.phrase {
152 None => Mnemonic::<W>::new_with_count(rng, self.word_count)?,
153 _ => return Err(MnemonicBuilderError::UnexpectedPhraseFound.into()),
154 };
155 let signer = self.mnemonic_to_signer(&mnemonic)?;
156
157 if let Some(dir) = &self.write_to {
159 std::fs::write(dir.join(signer.address.to_string()), mnemonic.to_phrase().as_bytes())?;
160 }
161
162 Ok(signer)
163 }
164
165 fn mnemonic_to_signer(
166 &self,
167 mnemonic: &Mnemonic<W>,
168 ) -> Result<PrivateKeySigner, LocalSignerError> {
169 let derived_priv_key =
170 mnemonic.derive_key(&self.derivation_path, self.password.as_deref())?;
171 let key: &coins_bip32::prelude::SigningKey = derived_priv_key.as_ref();
172 let credential = SigningKey::from_bytes(&key.to_bytes())?;
173 let address = secret_key_to_address(&credential);
174 Ok(LocalSigner::<SigningKey> { credential, address, chain_id: None })
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use coins_bip39::English;
182 use tempfile::tempdir;
183
184 const TEST_DERIVATION_PATH: &str = "m/44'/60'/0'/2/1";
185
186 #[test]
187 fn mnemonic_deterministic() {
188 let tests = [
190 (
191 "work man father plunge mystery proud hollow address reunion sauce theory bonus",
192 0u32,
193 Some("TREZOR123"),
194 "0x431a00DA1D54c281AeF638A73121B3D153e0b0F6",
195 ),
196 (
197 "inject danger program federal spice bitter term garbage coyote breeze thought funny",
198 1u32,
199 Some("LEDGER321"),
200 "0x231a3D0a05d13FAf93078C779FeeD3752ea1350C",
201 ),
202 (
203 "fire evolve buddy tenant talent favorite ankle stem regret myth dream fresh",
204 2u32,
205 None,
206 "0x1D86AD5eBb2380dAdEAF52f61f4F428C485460E9",
207 ),
208 (
209 "thumb soda tape crunch maple fresh imitate cancel order blind denial giraffe",
210 3u32,
211 None,
212 "0xFB78b25f69A8e941036fEE2A5EeAf349D81D4ccc",
213 ),
214 ];
215 for (phrase, index, password, expected_addr) in tests {
216 let mut builder =
217 MnemonicBuilder::<English>::default().phrase(phrase).index(index).unwrap();
218 if let Some(psswd) = password {
219 builder = builder.password(psswd);
220 }
221 let signer = builder.build().unwrap();
222 assert_eq!(&signer.address.to_string(), expected_addr);
223 }
224 }
225
226 #[test]
227 fn mnemonic_write_read() {
228 let dir = tempdir().unwrap();
229
230 let signer1 = MnemonicBuilder::<English>::default()
231 .word_count(24)
232 .derivation_path(TEST_DERIVATION_PATH)
233 .unwrap()
234 .write_to(dir.as_ref())
235 .build_random()
236 .unwrap();
237
238 let paths = std::fs::read_dir(dir.as_ref()).unwrap();
240 assert_eq!(paths.count(), 1);
241
242 let phrase_path = dir.as_ref().join(signer1.address.to_string());
244 let phrase = std::fs::read_to_string(phrase_path).unwrap();
245 let signer2 = MnemonicBuilder::<English>::default()
246 .phrase(phrase)
247 .derivation_path(TEST_DERIVATION_PATH)
248 .unwrap()
249 .build()
250 .unwrap();
251
252 assert_eq!(signer1.address, signer2.address);
254
255 dir.close().unwrap();
256 }
257}