use bip32::Seed as Bip32Seed;
use bip39::{
ErrorKind as Bip39ErrorKind, Language, Mnemonic as Bip39Mnemonic, MnemonicType,
Seed as Bip39Seed,
};
#[derive(Clone)]
pub struct Mnemonic {
inner: Bip39Mnemonic,
}
impl std::fmt::Debug for Mnemonic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Mnemonic")
.field("phrase", &"<redacted>")
.finish()
}
}
impl Mnemonic {
pub fn new_random() -> Self {
let inner = Bip39Mnemonic::new(MnemonicType::Words24, Language::English);
Self { inner }
}
pub fn from_phrase(phrase: &str) -> Result<Self, Bip39ErrorKind> {
let inner = Bip39Mnemonic::from_phrase(phrase, Language::English)?;
Ok(Self { inner })
}
pub fn to_phrase(&self) -> &str {
self.inner.phrase()
}
pub fn to_seed(&self, password: Option<&str>) -> Result<Bip32Seed, MnemonicError> {
let master_seed = Bip39Seed::new(&self.inner, password.unwrap_or(""));
let master_seed_bytes: [u8; 64] =
master_seed
.as_bytes()
.try_into()
.map_err(|_| MnemonicError::InvalidSeedLength {
expected: 64,
actual: master_seed.as_bytes().len(),
})?;
let bip_32_master_seed = Bip32Seed::new(master_seed_bytes);
Ok(bip_32_master_seed)
}
}
#[derive(Debug, thiserror::Error)]
pub enum MnemonicError {
#[error("Invalid seed length: expected {expected}, got {actual}")]
InvalidSeedLength {
expected: usize,
actual: usize,
},
#[error("BIP39 error: {0}")]
Bip39(#[from] Bip39ErrorKind),
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
use super::*;
#[test]
fn password_is_unicode_normalized() {
let phrase = "absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter all";
let password = "nullius à nym.zone ¹teſts² English";
let expected_seed_hex = "3028751d811a60dc04039d4b5eebe8539a1beea2cae3e0805a0a775f8623b0f9e2a5d6b7a213c478faa0652e4eb940935ac20742171536275baccc1c46a5d016";
let mnemonic = Mnemonic::from_phrase(phrase).unwrap();
let seed = mnemonic.to_seed(Some(password)).unwrap();
let seed_hex: String = seed
.as_bytes()
.iter()
.map(|b| format!("{:02x}", b))
.collect();
assert_eq!(seed_hex, expected_seed_hex);
}
}