use crate::{
crypto::KeyPair,
neo_protocol::{Account, AccountTrait},
};
use bip39::{Language, Mnemonic};
use p256::elliptic_curve::zeroize::Zeroize;
use sha2::{Digest, Sha256};
pub struct Bip39Account {
account: Account,
mnemonic: String,
}
impl std::fmt::Debug for Bip39Account {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Bip39Account")
.field("account", &self.account)
.field("mnemonic_words", &self.mnemonic.split_whitespace().count())
.field("mnemonic", &"<redacted>")
.finish()
}
}
impl Drop for Bip39Account {
fn drop(&mut self) {
self.mnemonic.zeroize();
}
}
impl Bip39Account {
pub fn mnemonic(&self) -> &str {
&self.mnemonic
}
pub fn account(&self) -> &Account {
&self.account
}
pub fn create(password: &str) -> Result<Self, Box<dyn std::error::Error>> {
let mut rng = bip39::rand::thread_rng();
let mnemonic =
Mnemonic::generate_in_with(&mut rng, Language::English, 24).map_err(|e| {
Box::<dyn std::error::Error>::from(format!("Failed to generate mnemonic: {e}"))
})?;
let seed = mnemonic.to_seed(password);
let mut hasher = Sha256::new();
hasher.update(seed);
let private_key = hasher.finalize();
let key_pair = KeyPair::from_private_key(private_key.as_ref()).map_err(|e| {
Box::<dyn std::error::Error>::from(format!("Failed to create key pair: {e}"))
})?;
let account = Account::from_key_pair(key_pair.clone(), None, None).map_err(|e| {
Box::<dyn std::error::Error>::from(format!(
"Failed to create account from key pair: {}",
e
))
})?;
Ok(Self { account, mnemonic: mnemonic.to_string() })
}
pub fn from_bip39_mnemonic(
password: &str,
mnemonic: &str,
) -> Result<Self, Box<dyn std::error::Error>> {
let mnemonic = Mnemonic::parse_in(Language::English, mnemonic)?;
let seed = mnemonic.to_seed(password);
let mut hasher = Sha256::new();
hasher.update(seed);
let private_key = hasher.finalize();
let key_pair = KeyPair::from_private_key(private_key.as_ref()).map_err(|e| {
Box::<dyn std::error::Error>::from(format!("Failed to create key pair: {e}"))
})?;
let account = Account::from_key_pair(key_pair.clone(), None, None).map_err(|e| {
Box::<dyn std::error::Error>::from(format!(
"Failed to create account from key pair: {}",
e
))
})?;
Ok(Self { account, mnemonic: mnemonic.to_string() })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_bip39_account() {
let password =
std::env::var("TEST_PASSWORD").unwrap_or_else(|_| "test_password".to_string());
let account =
Bip39Account::create(&password).expect("Should be able to create Bip39Account in test");
assert_eq!(account.mnemonic.split_whitespace().count(), 24);
assert!(account.account.key_pair().is_some());
}
#[test]
fn test_recover_from_mnemonic() {
let password =
std::env::var("TEST_PASSWORD").unwrap_or_else(|_| "test_password".to_string());
let original =
Bip39Account::create(&password).expect("Should be able to create Bip39Account in test");
let mnemonic = original.mnemonic.clone();
let recovered = Bip39Account::from_bip39_mnemonic(&password, &mnemonic)
.expect("Should be able to recover Bip39Account from mnemonic in test");
assert_eq!(original.account.get_script_hash(), recovered.account.get_script_hash());
assert_eq!(original.mnemonic, recovered.mnemonic);
}
#[test]
fn test_invalid_mnemonic() {
let result = Bip39Account::from_bip39_mnemonic("password", "invalid mnemonic phrase");
assert!(result.is_err());
}
#[test]
fn test_different_passwords_different_accounts() {
let account1 = Bip39Account::create("password1")
.expect("Should be able to create Bip39Account in test");
let account2 = Bip39Account::create("password2")
.expect("Should be able to create Bip39Account in test");
assert_ne!(account1.account.get_script_hash(), account2.account.get_script_hash());
}
#[test]
fn test_generate_and_recover_bip39_account() {
let password =
std::env::var("TEST_PASSWORD").unwrap_or_else(|_| "test_password".to_string());
let account1 =
Bip39Account::create(&password).expect("Should be able to create Bip39Account in test");
let account2 = Bip39Account::from_bip39_mnemonic(&password, &account1.mnemonic)
.expect("Should be able to recover Bip39Account from mnemonic in test");
assert_eq!(account1.account.get_address(), account2.account.get_address());
assert!(account1.account.key_pair().is_some());
assert_eq!(account1.account.key_pair(), account2.account.key_pair());
assert_eq!(account1.mnemonic, account2.mnemonic);
assert!(!account1.mnemonic.is_empty());
}
}