use bip32::{DerivationPath, XPrv};
use bip39::{Language, Mnemonic};
use bitcoin::secp256k1::{PublicKey, Secp256k1, XOnlyPublicKey};
use bitcoin::{Address, Network, key::CompressedPublicKey};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::error::Error as StdError;
use std::{fmt, str::FromStr};
use zeroize::Zeroize;
const DEFAULT_NETWORK: Network = Network::Bitcoin;
const DEFAULT_DERIVATION_PATH: &str = "m/84'/0'/0'/0/0";
const DEFAULT_BIP39_PASSPHRASE: &str = "";
#[derive(Debug, Serialize, Deserialize)]
pub struct GetAddressResponse {
pub address: String,
pub path: String,
pub public_key: String,
}
#[derive(Debug, Clone, Copy)]
pub enum WordCount {
Words12 = 12,
Words15 = 15,
Words18 = 18,
Words21 = 21,
Words24 = 24,
}
impl WordCount {
pub fn value(&self) -> usize {
*self as usize
}
}
#[derive(Debug)]
pub enum DerivationError {
InvalidDerivationPath(String),
InvalidNetworkType(String),
InvalidPurposeField(String),
InvalidXOnlyPubkey(String),
Bip39Error(bip39::Error),
Bip32Error(bip32::Error),
BitcoinError(String),
SecpError(bitcoin::secp256k1::Error),
ParseError(std::num::ParseIntError),
GenericError(String),
}
impl fmt::Display for DerivationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DerivationError::InvalidDerivationPath(msg) => {
write!(f, "Invalid derivation path: {}", msg)
}
DerivationError::InvalidNetworkType(msg) => write!(f, "Invalid network type: {}", msg),
DerivationError::InvalidPurposeField(msg) => {
write!(f, "Invalid purpose field: {}", msg)
}
DerivationError::InvalidXOnlyPubkey(msg) => {
write!(f, "Invalid X-only public key: {}", msg)
}
DerivationError::Bip39Error(e) => write!(f, "BIP39 error: {}", e),
DerivationError::Bip32Error(e) => write!(f, "BIP32 error: {}", e),
DerivationError::BitcoinError(e) => write!(f, "Bitcoin error: {}", e),
DerivationError::SecpError(e) => write!(f, "Secp256k1 error: {}", e),
DerivationError::ParseError(e) => write!(f, "Parse error: {}", e),
DerivationError::GenericError(msg) => write!(f, "Error: {}", msg),
}
}
}
impl StdError for DerivationError {}
impl From<bip39::Error> for DerivationError {
fn from(err: bip39::Error) -> Self {
DerivationError::Bip39Error(err)
}
}
impl From<bip32::Error> for DerivationError {
fn from(err: bip32::Error) -> Self {
DerivationError::Bip32Error(err)
}
}
impl From<bitcoin::secp256k1::Error> for DerivationError {
fn from(err: bitcoin::secp256k1::Error) -> Self {
DerivationError::SecpError(err)
}
}
impl From<std::num::ParseIntError> for DerivationError {
fn from(err: std::num::ParseIntError) -> Self {
DerivationError::ParseError(err)
}
}
struct SecureSeed {
seed: Vec<u8>,
}
impl SecureSeed {
pub fn new(seed: Vec<u8>) -> Self {
Self { seed }
}
}
impl Zeroize for SecureSeed {
fn zeroize(&mut self) {
self.seed.zeroize();
}
}
impl Drop for SecureSeed {
fn drop(&mut self) {
self.zeroize();
}
}
struct SecureMnemonic {
mnemonic: Mnemonic,
phrase: String,
}
impl SecureMnemonic {
pub fn new(mnemonic: Mnemonic) -> Self {
let phrase = mnemonic.to_string();
Self { mnemonic, phrase }
}
pub fn to_seed(&self, passphrase: &str) -> Vec<u8> {
self.mnemonic.to_seed(passphrase).to_vec()
}
}
impl Zeroize for SecureMnemonic {
fn zeroize(&mut self) {
self.phrase.zeroize();
}
}
impl Drop for SecureMnemonic {
fn drop(&mut self) {
self.zeroize();
}
}
struct SecurePrivateKey {
key: bitcoin::secp256k1::SecretKey,
}
impl SecurePrivateKey {
pub fn new(key: bitcoin::secp256k1::SecretKey) -> Self {
Self { key }
}
pub fn key(&self) -> &bitcoin::secp256k1::SecretKey {
&self.key
}
}
impl Drop for SecurePrivateKey {
fn drop(&mut self) {
let _ = bitcoin::secp256k1::SecretKey::from_slice(&[0u8; 32]);
}
}
struct SecureWifKey {
key: bitcoin::PrivateKey,
}
impl SecureWifKey {
pub fn new(key: bitcoin::PrivateKey) -> Self {
Self { key }
}
pub fn to_wif(&self) -> String {
self.key.to_wif()
}
}
impl Drop for SecureWifKey {
fn drop(&mut self) {
if let Ok(zeroed) = bitcoin::secp256k1::SecretKey::from_slice(&[0u8; 32]) {
let _ = std::mem::replace(&mut self.key.inner, zeroed);
}
}
}
pub fn generate_mnemonic(
word_count: Option<WordCount>,
language: Option<Language>,
) -> Result<String, DerivationError> {
let word_count = word_count.unwrap_or(WordCount::Words12);
let lang = language.unwrap_or(Language::English);
let mnemonic = Mnemonic::generate_in(lang, word_count.value())?;
let phrase = mnemonic.to_string();
Ok(phrase)
}
fn validate_purpose_field(purpose: &str) -> Result<(), DerivationError> {
match purpose {
"44'" | "49'" | "84'" | "86'" => Ok(()),
_ => Err(DerivationError::InvalidPurposeField(format!(
"Unsupported purpose field: {}. Expected one of: 44', 49', 84', 86'",
purpose
))),
}
}
pub fn derive_bitcoin_address(
mnemonic_phrase: &str,
derivation_path_str: Option<&str>,
network: Option<Network>,
bip39_passphrase: Option<&str>,
) -> Result<GetAddressResponse, DerivationError> {
let network = network.unwrap_or(DEFAULT_NETWORK);
let bip39_passphrase = bip39_passphrase.unwrap_or(DEFAULT_BIP39_PASSPHRASE);
let derivation_path_str = derivation_path_str.unwrap_or(DEFAULT_DERIVATION_PATH);
let path_parts: Vec<&str> = derivation_path_str.split('/').collect();
if path_parts.len() != 6 || path_parts[0] != "m" {
return Err(DerivationError::InvalidDerivationPath(
"Invalid derivation path format. Expected format example: m/84'/0'/0'/0/0".to_string(),
));
}
let purpose = path_parts.get(1).ok_or_else(|| {
DerivationError::InvalidDerivationPath(
"Missing purpose field in derivation path".to_string(),
)
})?;
validate_purpose_field(purpose)?;
let second_number = path_parts[2].trim_end_matches('\'').parse::<u32>()?;
match network {
Network::Bitcoin => {
if second_number != 0 {
return Err(DerivationError::InvalidDerivationPath(format!(
"Invalid Coin number in the derivation path for {}. Expected 0.",
network
)));
}
}
Network::Testnet | Network::Regtest | Network::Signet => {
if second_number != 1 {
return Err(DerivationError::InvalidDerivationPath(format!(
"Invalid Coin number in the derivation path for {}. Expected 1.",
network
)));
}
}
_ => {
return Err(DerivationError::InvalidNetworkType(format!(
"Unsupported network type: {}",
network
)));
}
}
let mnemonic = match Mnemonic::parse_in(Language::English, mnemonic_phrase) {
Ok(m) => SecureMnemonic::new(m),
Err(e) => return Err(DerivationError::Bip39Error(e)),
};
let mut seed_bytes = mnemonic.to_seed(bip39_passphrase);
let secure_seed = SecureSeed::new(seed_bytes.clone());
let derivation_path = match derivation_path_str.parse::<DerivationPath>() {
Ok(path) => path,
Err(e) => {
seed_bytes.zeroize();
return Err(DerivationError::Bip32Error(e));
}
};
let xprv = match XPrv::derive_from_path(&secure_seed.seed, &derivation_path) {
Ok(key) => key,
Err(e) => {
seed_bytes.zeroize();
return Err(DerivationError::Bip32Error(e));
}
};
let secp = Secp256k1::new();
let secret_key = match bitcoin::secp256k1::SecretKey::from_slice(&xprv.private_key().to_bytes())
{
Ok(key) => SecurePrivateKey::new(key),
Err(e) => {
seed_bytes.zeroize();
return Err(DerivationError::SecpError(e));
}
};
let public_key = PublicKey::from_secret_key(&secp, secret_key.key());
seed_bytes.zeroize();
let compressed_public_key = match CompressedPublicKey::from_slice(&public_key.serialize()) {
Ok(key) => key,
Err(e) => {
return Err(DerivationError::BitcoinError(format!(
"Failed to create compressed public key: {}",
e
)));
}
};
let address = match *purpose {
"44'" => Address::p2pkh(&compressed_public_key, network),
"49'" => Address::p2shwpkh(&compressed_public_key, network),
"84'" => Address::p2wpkh(&compressed_public_key, network),
"86'" => {
let x_only_pubkey = match XOnlyPublicKey::from_slice(&public_key.serialize()[1..]) {
Ok(key) => key,
Err(e) => {
return Err(DerivationError::InvalidXOnlyPubkey(format!(
"Failed to create XOnlyPublicKey: {}",
e
)));
}
};
let merkle_root = None; Address::p2tr(&secp, x_only_pubkey, merkle_root, network)
}
_ => {
return Err(DerivationError::InvalidPurposeField(format!(
"Unsupported purpose field: {}",
purpose
)));
}
};
let address_string = address.to_string();
let public_key_string = public_key.to_string();
Ok(GetAddressResponse {
address: address_string,
path: derivation_path_str.to_string(),
public_key: public_key_string,
})
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GetAddressesResponse {
pub addresses: Vec<GetAddressResponse>,
}
pub fn derive_bitcoin_addresses(
mnemonic_phrase: &str,
derivation_path_str: Option<&str>,
network: Option<Network>,
bip39_passphrase: Option<&str>,
is_change: Option<bool>,
start_index: Option<u32>,
count: Option<u32>,
) -> Result<GetAddressesResponse, DerivationError> {
let network = network.unwrap_or(DEFAULT_NETWORK);
let bip39_passphrase = bip39_passphrase.unwrap_or(DEFAULT_BIP39_PASSPHRASE);
let path = derivation_path_str.unwrap_or("m/84'/0'/0'");
let is_change = is_change.unwrap_or(false);
let start_index = start_index.unwrap_or(0);
let count = count.unwrap_or(1);
let mut addresses = Vec::with_capacity(count as usize);
let path_parts: Vec<&str> = path.split('/').collect();
let base_path = if path_parts.len() >= 4 && path_parts[0] == "m" {
path_parts[..4].join("/")
} else {
return Err(DerivationError::InvalidDerivationPath(
"Invalid derivation path format. Expected format that includes at least: m/purpose'/coin'/account'".to_string()
));
};
let purpose = path_parts.get(1).ok_or_else(|| {
DerivationError::InvalidDerivationPath(
"Missing purpose field in derivation path".to_string(),
)
})?;
validate_purpose_field(purpose)?;
let second_number = path_parts[2].trim_end_matches('\'').parse::<u32>()?;
match network {
Network::Bitcoin => {
if second_number != 0 {
return Err(DerivationError::InvalidDerivationPath(format!(
"Invalid Coin number in the derivation path for {}. Expected 0.",
network
)));
}
}
Network::Testnet | Network::Regtest | Network::Signet => {
if second_number != 1 {
return Err(DerivationError::InvalidDerivationPath(format!(
"Invalid Coin number in the derivation path for {}. Expected 1.",
network
)));
}
}
_ => {
return Err(DerivationError::InvalidNetworkType(format!(
"Unsupported network type: {}",
network
)));
}
}
let change_component = if is_change { "1" } else { "0" };
for i in start_index..(start_index + count) {
let full_path = format!("{}/{}/{}", base_path, change_component, i);
match derive_bitcoin_address(
mnemonic_phrase,
Some(&full_path),
Some(network),
Some(bip39_passphrase),
) {
Ok(address) => addresses.push(address),
Err(e) => return Err(e),
}
}
Ok(GetAddressesResponse { addresses })
}
pub fn calculate_script_hash(
address: &str,
network: Option<Network>,
) -> Result<String, DerivationError> {
let network = network.unwrap_or(DEFAULT_NETWORK);
let parsed_addr = match Address::from_str(address) {
Ok(addr) => addr,
Err(e) => {
return Err(DerivationError::BitcoinError(format!(
"Failed to parse address: {}",
e
)));
}
};
let addr = match parsed_addr.require_network(network) {
Ok(address) => address,
Err(e) => {
return Err(DerivationError::BitcoinError(format!(
"Network mismatch: {}",
e
)));
}
};
let script = addr.script_pubkey();
let script_bytes = script.as_bytes();
let hash = Sha256::digest(script_bytes);
let mut reversed_hash = hash.to_vec();
reversed_hash.reverse();
let script_hash_hex = hex::encode(reversed_hash);
Ok(script_hash_hex)
}
pub fn derive_private_key(
mnemonic_phrase: &str,
derivation_path_str: Option<&str>,
network: Option<Network>,
bip39_passphrase: Option<&str>,
) -> Result<String, DerivationError> {
let network = network.unwrap_or(DEFAULT_NETWORK);
let bip39_passphrase = bip39_passphrase.unwrap_or(DEFAULT_BIP39_PASSPHRASE);
let derivation_path_str = derivation_path_str.unwrap_or(DEFAULT_DERIVATION_PATH);
let mnemonic = match Mnemonic::parse_in(Language::English, mnemonic_phrase) {
Ok(m) => SecureMnemonic::new(m),
Err(e) => return Err(DerivationError::Bip39Error(e)),
};
let mut seed_bytes = mnemonic.to_seed(bip39_passphrase);
let secure_seed = SecureSeed::new(seed_bytes.clone());
let derivation_path = match derivation_path_str.parse::<DerivationPath>() {
Ok(path) => path,
Err(e) => {
seed_bytes.zeroize();
return Err(DerivationError::Bip32Error(e));
}
};
let xprv = match XPrv::derive_from_path(&secure_seed.seed, &derivation_path) {
Ok(key) => key,
Err(e) => {
seed_bytes.zeroize();
return Err(DerivationError::Bip32Error(e));
}
};
let secret_key = match bitcoin::secp256k1::SecretKey::from_slice(&xprv.private_key().to_bytes())
{
Ok(key) => key,
Err(e) => {
seed_bytes.zeroize();
return Err(DerivationError::SecpError(e));
}
};
seed_bytes.zeroize();
let private_key_wif = SecureWifKey::new(bitcoin::PrivateKey {
compressed: true,
network: network.into(),
inner: secret_key,
});
let private_key_string = private_key_wif.to_wif();
Ok(private_key_string)
}
pub fn validate_mnemonic(mnemonic_phrase: &str) -> Result<(), DerivationError> {
Mnemonic::from_str(mnemonic_phrase)
.map(|_| ())
.map_err(|e| DerivationError::Bip39Error(e))
}
pub fn is_valid_bip39_word(word: &str, language: Option<Language>) -> bool {
let lang = language.unwrap_or(Language::English);
lang.word_list()
.iter()
.any(|w| w.to_lowercase() == word.to_lowercase())
}
pub fn get_bip39_suggestions(
partial_word: &str,
limit: usize,
language: Option<Language>,
) -> Vec<String> {
let lang = language.unwrap_or(Language::English);
let lowercased = partial_word.to_lowercase();
let mut suggestions: Vec<String> = lang
.word_list()
.iter()
.filter(|word| word.starts_with(&lowercased))
.map(|w| w.to_string())
.collect();
suggestions.sort();
suggestions.truncate(limit);
suggestions
}
pub fn get_bip39_wordlist(language: Option<Language>) -> Vec<String> {
let lang = language.unwrap_or(Language::English);
lang.word_list()
.iter()
.map(|w| w.to_string())
.collect()
}
pub fn mnemonic_to_entropy(mnemonic_phrase: &str) -> Result<Vec<u8>, DerivationError> {
let mnemonic = Mnemonic::from_str(mnemonic_phrase)?;
Ok(mnemonic.to_entropy().to_vec())
}
pub fn entropy_to_mnemonic(
entropy: &[u8],
language: Option<Language>,
) -> Result<String, DerivationError> {
let lang = language.unwrap_or(Language::English);
let mnemonic = Mnemonic::from_entropy_in(lang, entropy)?;
Ok(mnemonic.to_string())
}
pub fn mnemonic_to_seed(
mnemonic_phrase: &str,
passphrase: Option<&str>,
) -> Result<Vec<u8>, DerivationError> {
let mnemonic = Mnemonic::from_str(mnemonic_phrase)?;
let passphrase = passphrase.unwrap_or("");
Ok(mnemonic.to_seed(passphrase).to_vec())
}
#[cfg(test)]
mod tests {
use super::*;
use std::option::Option;
#[test]
fn test_generate_mnemonic() {
let mnemonic12: String = generate_mnemonic(
Option::from(WordCount::Words12),
Option::from(Language::English),
)
.unwrap();
println!("Generated 12-word mnemonic: {}", mnemonic12);
assert_eq!(mnemonic12.split_whitespace().count(), 12);
let mnemonic15: String = generate_mnemonic(
Option::from(WordCount::Words15),
Option::from(Language::English),
)
.unwrap();
println!("Generated 15-word mnemonic: {}", mnemonic15);
assert_eq!(mnemonic15.split_whitespace().count(), 15);
let mnemonic18: String = generate_mnemonic(
Option::from(WordCount::Words18),
Option::from(Language::English),
)
.unwrap();
println!("Generated 18-word mnemonic: {}", mnemonic18);
assert_eq!(mnemonic18.split_whitespace().count(), 18);
let mnemonic21: String = generate_mnemonic(
Option::from(WordCount::Words21),
Option::from(Language::English),
)
.unwrap();
println!("Generated 21-word mnemonic: {}", mnemonic21);
assert_eq!(mnemonic21.split_whitespace().count(), 21);
let mnemonic24: String = generate_mnemonic(
Option::from(WordCount::Words24),
Option::from(Language::English),
)
.unwrap();
println!("Generated 24-word mnemonic: {}", mnemonic24);
assert_eq!(mnemonic24.split_whitespace().count(), 24);
}
#[test]
fn test_derive_address_p2pkh() {
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let path = "m/44'/0'/0'/0/0";
let address = derive_bitcoin_address(
mnemonic,
Option::from(path),
Option::from(Network::Bitcoin),
None,
)
.unwrap();
println!("P2PKH address: {}", address.address);
println!("P2PKH pubkey: {}", address.public_key);
assert_eq!(address.address, "1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA");
assert_eq!(address.path, path);
}
#[test]
fn test_derive_address_p2shwpkh() {
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let path = "m/49'/0'/0'/0/0";
let address = derive_bitcoin_address(
mnemonic,
Option::from(path),
Option::from(Network::Bitcoin),
None,
)
.unwrap();
println!("P2SH-WPKH address: {}", address.address);
println!("P2SH-WPKH pubkey: {}", address.public_key);
assert_eq!(address.address, "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf");
assert_eq!(address.path, path);
}
#[test]
fn test_derive_address_p2wpkh() {
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let path = "m/84'/0'/0'/0/0";
let address = derive_bitcoin_address(
mnemonic,
Option::from(path),
Option::from(Network::Bitcoin),
None,
)
.unwrap();
println!("P2WPKH address: {}", address.address);
println!("P2WPKH pubkey: {}", address.public_key);
assert_eq!(
address.address,
"bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"
);
assert_eq!(address.path, path);
}
#[test]
fn test_derive_address_p2tr() {
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let path = "m/86'/0'/0'/0/0";
let address = derive_bitcoin_address(
mnemonic,
Option::from(path),
Option::from(Network::Bitcoin),
None,
)
.unwrap();
println!("P2TR address: {}", address.address);
println!("P2TR pubkey: {}", address.public_key);
assert!(address.address.starts_with("bc1p"));
assert_eq!(address.path, path);
}
#[test]
fn test_calculate_script_hash() {
let script_hash = calculate_script_hash(
"1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA",
Option::from(Network::Bitcoin),
)
.unwrap();
println!("P2PKH script hash: {}", script_hash);
assert_eq!(
script_hash,
"1e8750b8a4c0912d8b84f7eb53472cbdcb57f9e0cde263b2e51ecbe30853cd68"
);
let script_hash = calculate_script_hash(
"bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu",
Option::from(Network::Bitcoin),
)
.unwrap();
println!("P2WPKH script hash: {}", script_hash);
assert_eq!(script_hash.len(), 64); }
#[test]
fn test_derive_private_key() {
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let path = "m/84'/0'/0'/0/0";
let private_key = derive_private_key(
mnemonic,
Option::from(path),
Option::from(Network::Bitcoin),
None,
)
.unwrap();
assert_eq!(
private_key,
"KyZpNDKnfs94vbrwhJneDi77V6jF64PWPF8x5cdJb8ifgg2DUc9d"
);
}
#[test]
fn test_derive_bitcoin_addresses() {
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let base_path = "m/84'/0'/0'";
let result = derive_bitcoin_addresses(
mnemonic,
Option::from(base_path),
Option::from(Network::Bitcoin),
None,
None,
None,
Option::from(3),
)
.unwrap();
assert_eq!(result.addresses.len(), 3);
assert_eq!(result.addresses[0].path, "m/84'/0'/0'/0/0");
assert_eq!(result.addresses[1].path, "m/84'/0'/0'/0/1");
assert_eq!(result.addresses[2].path, "m/84'/0'/0'/0/2");
assert_eq!(
result.addresses[0].address,
"bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"
);
let full_path = "m/84'/0'/0'/0/5"; let full_path_result = derive_bitcoin_addresses(
mnemonic,
Option::from(full_path),
Option::from(Network::Bitcoin),
None,
None,
Option::from(10),
Option::from(2),
)
.unwrap();
assert_eq!(full_path_result.addresses[0].path, "m/84'/0'/0'/0/10");
assert_eq!(full_path_result.addresses[1].path, "m/84'/0'/0'/0/11");
let full_path_change = derive_bitcoin_addresses(
mnemonic,
Option::from(full_path),
Option::from(Network::Bitcoin),
None,
Option::from(true),
None,
Option::from(2),
)
.unwrap();
assert_eq!(full_path_change.addresses[0].path, "m/84'/0'/0'/1/0");
assert_eq!(full_path_change.addresses[1].path, "m/84'/0'/0'/1/1");
let full_change_path = "m/84'/0'/0'/1/0";
let change_override_result = derive_bitcoin_addresses(
mnemonic,
Option::from(full_change_path),
Option::from(Network::Bitcoin),
None,
Option::from(false),
None,
Option::from(2),
)
.unwrap();
assert_eq!(change_override_result.addresses[0].path, "m/84'/0'/0'/0/0");
assert_eq!(change_override_result.addresses[1].path, "m/84'/0'/0'/0/1");
}
#[test]
fn test_validate_mnemonic() {
let valid_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
assert!(validate_mnemonic(valid_mnemonic).is_ok());
let invalid_mnemonic = "invalid word sequence that is not valid";
assert!(validate_mnemonic(invalid_mnemonic).is_err());
}
#[test]
fn test_derive_addresses_with_varying_mnemonic_lengths() {
let mnemonic12 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let result12 = derive_bitcoin_address(
mnemonic12,
Some("m/84'/0'/0'/0/0"),
Some(Network::Bitcoin),
None,
);
assert!(result12.is_ok());
assert_eq!(result12.unwrap().address, "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu");
let mnemonic15 = generate_mnemonic(Some(WordCount::Words15), None).unwrap();
assert_eq!(mnemonic15.split_whitespace().count(), 15);
let result15 = derive_bitcoin_address(
&mnemonic15,
Some("m/84'/0'/0'/0/0"),
Some(Network::Bitcoin),
None,
);
assert!(result15.is_ok());
assert!(result15.unwrap().address.starts_with("bc1"));
let mnemonic18 = generate_mnemonic(Some(WordCount::Words18), None).unwrap();
assert_eq!(mnemonic18.split_whitespace().count(), 18);
let result18 = derive_bitcoin_address(
&mnemonic18,
Some("m/84'/0'/0'/0/0"),
Some(Network::Bitcoin),
None,
);
assert!(result18.is_ok());
assert!(result18.unwrap().address.starts_with("bc1"));
let mnemonic21 = generate_mnemonic(Some(WordCount::Words21), None).unwrap();
assert_eq!(mnemonic21.split_whitespace().count(), 21);
let result21 = derive_bitcoin_address(
&mnemonic21,
Some("m/84'/0'/0'/0/0"),
Some(Network::Bitcoin),
None,
);
assert!(result21.is_ok());
assert!(result21.unwrap().address.starts_with("bc1"));
let mnemonic24 = generate_mnemonic(Some(WordCount::Words24), None).unwrap();
assert_eq!(mnemonic24.split_whitespace().count(), 24);
let result24 = derive_bitcoin_address(
&mnemonic24,
Some("m/84'/0'/0'/0/0"),
Some(Network::Bitcoin),
None,
);
assert!(result24.is_ok());
assert!(result24.unwrap().address.starts_with("bc1"));
}
#[test]
fn test_is_valid_bip39_word() {
assert!(is_valid_bip39_word("abandon", None));
assert!(is_valid_bip39_word("ABANDON", None)); assert!(!is_valid_bip39_word("notaword", None));
}
#[test]
fn test_get_bip39_suggestions() {
let suggestions = get_bip39_suggestions("ab", 5, None);
assert!(!suggestions.is_empty());
assert!(suggestions.contains(&"abandon".to_string()));
assert!(suggestions.contains(&"ability".to_string()));
assert!(suggestions.len() <= 5);
let mut sorted = suggestions.clone();
sorted.sort();
assert_eq!(suggestions, sorted);
}
#[test]
fn test_get_bip39_wordlist() {
let wordlist = get_bip39_wordlist(None);
assert_eq!(wordlist.len(), 2048);
assert!(wordlist.contains(&"abandon".to_string()));
assert!(wordlist.contains(&"zoo".to_string()));
}
#[test]
fn test_mnemonic_entropy_conversion() {
let mnemonic12 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let entropy12 = mnemonic_to_entropy(mnemonic12).unwrap();
assert_eq!(entropy12.len(), 16); let recovered_mnemonic12 = entropy_to_mnemonic(&entropy12, None).unwrap();
assert_eq!(mnemonic12, recovered_mnemonic12);
let generated15 = generate_mnemonic(Some(WordCount::Words15), None).unwrap();
let entropy15 = mnemonic_to_entropy(&generated15).unwrap();
assert_eq!(entropy15.len(), 20); let recovered_mnemonic15 = entropy_to_mnemonic(&entropy15, None).unwrap();
assert_eq!(generated15, recovered_mnemonic15);
let generated18 = generate_mnemonic(Some(WordCount::Words18), None).unwrap();
let entropy18 = mnemonic_to_entropy(&generated18).unwrap();
assert_eq!(entropy18.len(), 24); let recovered_mnemonic18 = entropy_to_mnemonic(&entropy18, None).unwrap();
assert_eq!(generated18, recovered_mnemonic18);
let generated21 = generate_mnemonic(Some(WordCount::Words21), None).unwrap();
let entropy21 = mnemonic_to_entropy(&generated21).unwrap();
assert_eq!(entropy21.len(), 28); let recovered_mnemonic21 = entropy_to_mnemonic(&entropy21, None).unwrap();
assert_eq!(generated21, recovered_mnemonic21);
let generated24 = generate_mnemonic(Some(WordCount::Words24), None).unwrap();
let entropy24 = mnemonic_to_entropy(&generated24).unwrap();
assert_eq!(entropy24.len(), 32); let recovered_mnemonic24 = entropy_to_mnemonic(&entropy24, None).unwrap();
assert_eq!(generated24, recovered_mnemonic24);
}
#[test]
fn test_mnemonic_to_seed() {
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let seed1 = mnemonic_to_seed(mnemonic, None).unwrap();
assert_eq!(seed1.len(), 64);
let expected_seed = hex::decode("5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4").unwrap();
assert_eq!(seed1, expected_seed);
let seed2 = mnemonic_to_seed(mnemonic, Some("passphrase")).unwrap();
assert_eq!(seed2.len(), 64);
assert_ne!(seed1, seed2);
}
}