use bip39::{Language, Mnemonic};
use rand::RngCore;
use zeroize::Zeroize;
use super::error::RecoveryError;
#[derive(Debug, Clone)]
pub struct RecoveryConfig {
pub word_count: usize,
pub language: Language,
pub use_passphrase: bool,
}
impl Default for RecoveryConfig {
fn default() -> Self {
Self {
word_count: 24,
language: Language::English,
use_passphrase: false,
}
}
}
impl RecoveryConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_word_count(mut self, count: usize) -> Self {
self.word_count = count;
self
}
#[must_use]
pub fn with_language(mut self, language: Language) -> Self {
self.language = language;
self
}
#[must_use]
pub fn with_passphrase(mut self, use_passphrase: bool) -> Self {
self.use_passphrase = use_passphrase;
self
}
}
fn entropy_bytes_for_word_count(word_count: usize) -> Result<usize, RecoveryError> {
match word_count {
12 => Ok(16),
15 => Ok(20),
18 => Ok(24),
21 => Ok(28),
24 => Ok(32),
_ => Err(RecoveryError::MnemonicGenerationFailed(format!(
"invalid word count {word_count}: must be 12, 15, 18, 21, or 24"
))),
}
}
pub fn generate_recovery_mnemonic(config: &RecoveryConfig) -> Result<Mnemonic, RecoveryError> {
let entropy_len = entropy_bytes_for_word_count(config.word_count)?;
let mut entropy = vec![0u8; entropy_len];
rand::thread_rng().fill_bytes(&mut entropy);
let result = Mnemonic::from_entropy_in(config.language, &entropy)
.map_err(|e| RecoveryError::MnemonicGenerationFailed(e.to_string()));
entropy.zeroize();
result
}
pub fn validate_mnemonic(
mnemonic_words: &str,
language: Language,
) -> Result<Mnemonic, RecoveryError> {
Mnemonic::parse_in(language, mnemonic_words)
.map_err(|e| RecoveryError::InvalidMnemonic(e.to_string()))
}
#[must_use]
pub fn mnemonic_to_words(mnemonic: &Mnemonic) -> Vec<String> {
mnemonic.words().map(|s| s.to_string()).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mnemonic_generation() {
let config = RecoveryConfig::default();
let mnemonic = generate_recovery_mnemonic(&config).unwrap();
let words = mnemonic_to_words(&mnemonic);
assert_eq!(words.len(), 24, "Expected 24 words for default config");
}
#[test]
fn test_mnemonic_generation_12_words() {
let config = RecoveryConfig::new().with_word_count(12);
let mnemonic = generate_recovery_mnemonic(&config).unwrap();
let words = mnemonic_to_words(&mnemonic);
assert_eq!(words.len(), 12, "Expected 12 words");
}
#[test]
fn test_mnemonic_validation() {
let config = RecoveryConfig::default();
let mnemonic = generate_recovery_mnemonic(&config).unwrap();
let mnemonic_str = mnemonic.to_string();
let validated = validate_mnemonic(&mnemonic_str, Language::English).unwrap();
assert_eq!(validated.to_string(), mnemonic_str);
}
#[test]
fn test_invalid_mnemonic() {
let invalid_word = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon notaword";
let result = validate_mnemonic(invalid_word, Language::English);
assert!(
result.is_err(),
"Expected error for mnemonic with invalid word"
);
}
#[test]
fn test_mnemonic_to_words() {
let config = RecoveryConfig::default();
let mnemonic = generate_recovery_mnemonic(&config).unwrap();
let words = mnemonic_to_words(&mnemonic);
for word in &words {
assert!(!word.is_empty(), "Word should not be empty");
}
}
}