use alloc::string::{String, ToString};
use bip39::{Language, Mnemonic};
use zeroize::Zeroizing;
use crate::DeriveError;
#[derive(Debug)]
pub struct Wallet {
mnemonic: Zeroizing<String>,
seed: Zeroizing<[u8; 64]>,
has_passphrase: bool,
language: Language,
}
impl Wallet {
#[cfg(feature = "rand")]
pub fn generate(word_count: usize, passphrase: Option<&str>) -> Result<Self, DeriveError> {
Self::generate_in(Language::English, word_count, passphrase)
}
#[cfg(feature = "rand")]
pub fn generate_in(
language: Language,
word_count: usize,
passphrase: Option<&str>,
) -> Result<Self, DeriveError> {
if !matches!(word_count, 12 | 15 | 18 | 21 | 24) {
return Err(DeriveError::InvalidWordCount(word_count));
}
let mnemonic = Mnemonic::generate_in(language, word_count)?;
Ok(Self::from_parts(&mnemonic, language, passphrase))
}
#[cfg(feature = "rand_core")]
pub fn generate_in_with<R>(
rng: &mut R,
language: Language,
word_count: usize,
passphrase: Option<&str>,
) -> Result<Self, DeriveError>
where
R: bip39::rand_core::RngCore + bip39::rand_core::CryptoRng,
{
if !matches!(word_count, 12 | 15 | 18 | 21 | 24) {
return Err(DeriveError::InvalidWordCount(word_count));
}
let mnemonic = Mnemonic::generate_in_with(rng, language, word_count)?;
Ok(Self::from_parts(&mnemonic, language, passphrase))
}
pub fn from_entropy(entropy: &[u8], passphrase: Option<&str>) -> Result<Self, DeriveError> {
Self::from_entropy_in(Language::English, entropy, passphrase)
}
pub fn from_entropy_in(
language: Language,
entropy: &[u8],
passphrase: Option<&str>,
) -> Result<Self, DeriveError> {
let mnemonic = Mnemonic::from_entropy_in(language, entropy)?;
Ok(Self::from_parts(&mnemonic, language, passphrase))
}
pub fn from_mnemonic(phrase: &str, passphrase: Option<&str>) -> Result<Self, DeriveError> {
let mnemonic: Mnemonic = phrase.parse()?;
let language = mnemonic.language();
Ok(Self::from_parts(&mnemonic, language, passphrase))
}
pub fn from_mnemonic_in(
language: Language,
phrase: &str,
passphrase: Option<&str>,
) -> Result<Self, DeriveError> {
let mnemonic = Mnemonic::parse_in(language, phrase)?;
Ok(Self::from_parts(&mnemonic, language, passphrase))
}
fn from_parts(mnemonic: &Mnemonic, language: Language, passphrase: Option<&str>) -> Self {
let passphrase_str = passphrase.unwrap_or("");
let seed_bytes = mnemonic.to_seed(passphrase_str);
Self {
mnemonic: Zeroizing::new(mnemonic.to_string()),
seed: Zeroizing::new(seed_bytes),
has_passphrase: passphrase.is_some() && !passphrase_str.is_empty(),
language,
}
}
#[inline]
#[must_use]
pub fn mnemonic(&self) -> &str {
&self.mnemonic
}
#[inline]
#[must_use]
pub fn seed(&self) -> &[u8; 64] {
&self.seed
}
#[must_use]
pub const fn has_passphrase(&self) -> bool {
self.has_passphrase
}
#[inline]
#[must_use]
pub const fn language(&self) -> Language {
self.language
}
#[inline]
#[must_use]
pub fn word_count(&self) -> usize {
self.mnemonic.split_whitespace().count()
}
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
#[cfg(feature = "rand")]
#[test]
fn test_generate_12_words() {
let wallet = Wallet::generate(12, None).unwrap();
assert_eq!(wallet.word_count(), 12);
assert!(!wallet.has_passphrase());
}
#[cfg(feature = "rand")]
#[test]
fn test_generate_24_words() {
let wallet = Wallet::generate(24, None).unwrap();
assert_eq!(wallet.word_count(), 24);
}
#[cfg(feature = "rand")]
#[test]
fn test_generate_with_passphrase() {
let wallet = Wallet::generate(12, Some("secret")).unwrap();
assert!(wallet.has_passphrase());
}
#[test]
fn test_invalid_entropy_length() {
let result = Wallet::from_entropy(&[0u8; 15], None);
assert!(result.is_err());
}
#[test]
fn test_from_entropy() {
let entropy = [0u8; 16];
let wallet = Wallet::from_entropy(&entropy, None).unwrap();
assert_eq!(wallet.word_count(), 12);
}
#[test]
fn test_from_mnemonic() {
let wallet = Wallet::from_mnemonic(TEST_MNEMONIC, None).unwrap();
assert_eq!(wallet.mnemonic(), TEST_MNEMONIC);
}
#[test]
fn test_passphrase_changes_seed() {
let wallet1 = Wallet::from_mnemonic(TEST_MNEMONIC, None).unwrap();
let wallet2 = Wallet::from_mnemonic(TEST_MNEMONIC, Some("password")).unwrap();
assert_ne!(wallet1.seed(), wallet2.seed());
}
#[test]
fn test_deterministic_seed() {
let wallet1 = Wallet::from_mnemonic(TEST_MNEMONIC, Some("test")).unwrap();
let wallet2 = Wallet::from_mnemonic(TEST_MNEMONIC, Some("test")).unwrap();
assert_eq!(wallet1.seed(), wallet2.seed());
}
#[test]
fn kat_bip39_seed_vector() {
let wallet = Wallet::from_mnemonic(TEST_MNEMONIC, None).unwrap();
assert_eq!(
hex::encode(wallet.seed()),
"5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc1\
9a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4"
);
}
#[test]
fn kat_all_zero_entropy_produces_abandon_about() {
let wallet = Wallet::from_entropy(&[0u8; 16], None).unwrap();
assert_eq!(wallet.mnemonic(), TEST_MNEMONIC);
}
}