ethers-rs/ethers-signers/src/wallet/mnemonic.rs

283 lines
10 KiB
Rust

//! Specific helper functions for creating/loading a mnemonic private key following BIP-39
//! specifications
use crate::{Wallet, WalletError};
use coins_bip32::path::DerivationPath;
use coins_bip39::{Mnemonic, Wordlist};
use ethers_core::{
k256::ecdsa::SigningKey,
types::PathOrString,
utils::{secret_key_to_address, to_checksum},
};
use rand::Rng;
use std::{fs::File, io::Write, marker::PhantomData, path::PathBuf, str::FromStr};
use thiserror::Error;
const DEFAULT_DERIVATION_PATH_PREFIX: &str = "m/44'/60'/0'/0/";
/// Represents a structure that can resolve into a `Wallet<SigningKey>`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MnemonicBuilder<W: Wordlist> {
/// The mnemonic phrase can be supplied to the builder as a string or a path to the file whose
/// contents are the phrase. A builder that has a valid phrase should `build` the wallet.
phrase: Option<PathOrString>,
/// The mnemonic builder can also be asked to generate a new random wallet by providing the
/// number of words in the phrase. By default this is set to 12.
word_count: usize,
/// The derivation path at which the extended private key child will be derived at. By default
/// the mnemonic builder uses the path: "m/44'/60'/0'/0/0".
derivation_path: DerivationPath,
/// Optional password for the mnemonic phrase.
password: Option<String>,
/// Optional field that if enabled, writes the mnemonic phrase to disk storage at the provided
/// path.
write_to: Option<PathBuf>,
/// PhantomData
_wordlist: PhantomData<W>,
}
/// Error produced by the mnemonic wallet module
#[derive(Error, Debug)]
pub enum MnemonicBuilderError {
/// Error suggests that a phrase (path or words) was expected but not found
#[error("Expected phrase not found")]
ExpectedPhraseNotFound,
/// Error suggests that a phrase (path or words) was not expected but found
#[error("Unexpected phrase found")]
UnexpectedPhraseFound,
}
impl<W: Wordlist> Default for MnemonicBuilder<W> {
fn default() -> Self {
Self {
phrase: None,
word_count: 12usize,
derivation_path: DerivationPath::from_str(&format!(
"{}{}",
DEFAULT_DERIVATION_PATH_PREFIX, 0
))
.expect("should parse the default derivation path"),
password: None,
write_to: None,
_wordlist: PhantomData,
}
}
}
impl<W: Wordlist> MnemonicBuilder<W> {
/// Sets the phrase in the mnemonic builder. The phrase can either be a string or a path to
/// the file that contains the phrase. Once a phrase is provided, the key will be generated
/// deterministically by calling the `build` method.
///
/// # Example
///
/// ```
/// use ethers_signers::{MnemonicBuilder, coins_bip39::English};
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
///
/// let wallet = MnemonicBuilder::<English>::default()
/// .phrase("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about")
/// .build()?;
///
/// # Ok(())
/// # }
/// ```
#[must_use]
pub fn phrase<P: Into<PathOrString>>(mut self, phrase: P) -> Self {
self.phrase = Some(phrase.into());
self
}
/// Sets the word count of a mnemonic phrase to be generated at random. If the `phrase` field
/// is set, then `word_count` will be ignored.
///
/// # Example
///
/// ```
/// use ethers_signers::{MnemonicBuilder, coins_bip39::English};
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
///
/// let mut rng = rand::thread_rng();
/// let wallet = MnemonicBuilder::<English>::default()
/// .word_count(24)
/// .build_random(&mut rng)?;
///
/// # Ok(())
/// # }
/// ```
#[must_use]
pub fn word_count(mut self, count: usize) -> Self {
self.word_count = count;
self
}
/// Sets the derivation path of the child key to be derived. The derivation path is calculated
/// using the default derivation path prefix used in Ethereum, i.e. "m/44'/60'/0'/0/{index}".
pub fn index<U: Into<u32>>(mut self, index: U) -> Result<Self, WalletError> {
self.derivation_path = DerivationPath::from_str(&format!(
"{}{}",
DEFAULT_DERIVATION_PATH_PREFIX,
index.into()
))?;
Ok(self)
}
/// Sets the derivation path of the child key to be derived.
pub fn derivation_path(mut self, path: &str) -> Result<Self, WalletError> {
self.derivation_path = DerivationPath::from_str(path)?;
Ok(self)
}
/// Sets the password used to construct the seed from the mnemonic phrase.
#[must_use]
pub fn password(mut self, password: &str) -> Self {
self.password = Some(password.to_string());
self
}
/// Sets the path to which the randomly generated phrase will be written to. This field is
/// ignored when building a wallet from the provided mnemonic phrase.
#[must_use]
pub fn write_to<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.write_to = Some(path.into());
self
}
/// Builds a `LocalWallet` using the parameters set in mnemonic builder. This method expects
/// the phrase field to be set.
pub fn build(&self) -> Result<Wallet<SigningKey>, WalletError> {
let mnemonic = match &self.phrase {
Some(path_or_string) => {
let phrase = path_or_string.read()?;
Mnemonic::<W>::new_from_phrase(&phrase)?
}
None => return Err(MnemonicBuilderError::ExpectedPhraseNotFound.into()),
};
self.mnemonic_to_wallet(&mnemonic)
}
/// Builds a `LocalWallet` using the parameters set in the mnemonic builder and constructing
/// the phrase using the provided random number generator.
pub fn build_random<R: Rng>(&self, rng: &mut R) -> Result<Wallet<SigningKey>, WalletError> {
let mnemonic = match &self.phrase {
None => Mnemonic::<W>::new_with_count(rng, self.word_count)?,
_ => return Err(MnemonicBuilderError::UnexpectedPhraseFound.into()),
};
let wallet = self.mnemonic_to_wallet(&mnemonic)?;
// Write the mnemonic phrase to storage if a directory has been provided.
if let Some(dir) = &self.write_to {
let mut file = File::create(dir.as_path().join(to_checksum(&wallet.address, None)))?;
file.write_all(mnemonic.to_phrase().as_bytes())?;
}
Ok(wallet)
}
fn mnemonic_to_wallet(
&self,
mnemonic: &Mnemonic<W>,
) -> Result<Wallet<SigningKey>, WalletError> {
let derived_priv_key =
mnemonic.derive_key(&self.derivation_path, self.password.as_deref())?;
let key: &coins_bip32::prelude::SigningKey = derived_priv_key.as_ref();
let signer = SigningKey::from_bytes(&key.to_bytes())?;
let address = secret_key_to_address(&signer);
Ok(Wallet::<SigningKey> { signer, address, chain_id: 1 })
}
}
#[cfg(test)]
#[cfg(not(target_arch = "wasm32"))]
mod tests {
use super::*;
use crate::coins_bip39::English;
use tempfile::tempdir;
const TEST_DERIVATION_PATH: &str = "m/44'/60'/0'/2/1";
#[tokio::test]
async fn mnemonic_deterministic() {
// Testcases have been taken from MyCryptoWallet
const TESTCASES: [(&str, u32, Option<&str>, &str); 4] = [
(
"work man father plunge mystery proud hollow address reunion sauce theory bonus",
0u32,
Some("TREZOR123"),
"0x431a00DA1D54c281AeF638A73121B3D153e0b0F6",
),
(
"inject danger program federal spice bitter term garbage coyote breeze thought funny",
1u32,
Some("LEDGER321"),
"0x231a3D0a05d13FAf93078C779FeeD3752ea1350C",
),
(
"fire evolve buddy tenant talent favorite ankle stem regret myth dream fresh",
2u32,
None,
"0x1D86AD5eBb2380dAdEAF52f61f4F428C485460E9",
),
(
"thumb soda tape crunch maple fresh imitate cancel order blind denial giraffe",
3u32,
None,
"0xFB78b25f69A8e941036fEE2A5EeAf349D81D4ccc",
),
];
TESTCASES.iter().for_each(|(phrase, index, password, expected_addr)| {
let wallet = match password {
Some(psswd) => MnemonicBuilder::<English>::default()
.phrase(*phrase)
.index(*index)
.unwrap()
.password(psswd)
.build()
.unwrap(),
None => MnemonicBuilder::<English>::default()
.phrase(*phrase)
.index(*index)
.unwrap()
.build()
.unwrap(),
};
assert_eq!(&to_checksum(&wallet.address, None), expected_addr);
})
}
#[tokio::test]
async fn mnemonic_write_read() {
let dir = tempdir().unwrap();
// Construct a wallet from random mnemonic phrase and write it to the temp dir.
let mut rng = rand::thread_rng();
let wallet1 = MnemonicBuilder::<English>::default()
.word_count(24)
.derivation_path(TEST_DERIVATION_PATH)
.unwrap()
.write_to(dir.as_ref())
.build_random(&mut rng)
.unwrap();
// Ensure that only one file has been created.
let paths = std::fs::read_dir(dir.as_ref()).unwrap();
assert_eq!(paths.count(), 1);
// Use the newly created file's path to instantiate wallet.
let phrase_path = dir.as_ref().join(to_checksum(&wallet1.address, None));
let wallet2 = MnemonicBuilder::<English>::default()
.phrase(phrase_path.to_str().unwrap())
.derivation_path(TEST_DERIVATION_PATH)
.unwrap()
.build()
.unwrap();
// Ensure that both wallets belong to the same address.
assert_eq!(wallet1.address, wallet2.address);
dir.close().unwrap();
}
}