bitcoin_address_generator/
lib.rs

1//! Bitcoin key and address derivation library for address generation, script hash calculation, and private key management.
2//!
3//! This library provides secure methods for:
4//! - BIP39 mnemonic generation
5//! - Address generation (Legacy, SegWit, and Taproot)
6//! - Script hash calculation
7//! - Private key generation
8
9use bip32::{DerivationPath, XPrv};
10use bip39::{Language, Mnemonic};
11use bitcoin::secp256k1::{PublicKey, Secp256k1, XOnlyPublicKey};
12use bitcoin::{Address, Network, key::CompressedPublicKey};
13use serde::{Deserialize, Serialize};
14use sha2::{Digest, Sha256};
15use std::error::Error as StdError;
16use std::{fmt, str::FromStr};
17use zeroize::Zeroize;
18
19const DEFAULT_NETWORK: Network = Network::Bitcoin;
20const DEFAULT_DERIVATION_PATH: &str = "m/84'/0'/0'/0/0";
21const DEFAULT_BIP39_PASSPHRASE: &str = "";
22
23/// Response structure for address generation containing the address, derivation path, and public key.
24#[derive(Debug, Serialize, Deserialize)]
25pub struct GetAddressResponse {
26    /// The generated Bitcoin address as a string
27    pub address: String,
28    /// The derivation path used to generate the address
29    pub path: String,
30    /// The hexadecimal representation of the public key
31    pub public_key: String,
32}
33
34/// BIP39 mnemonic word count options
35#[derive(Debug, Clone, Copy)]
36pub enum WordCount {
37    /// 12-word mnemonic (128 bits of entropy)
38    Words12 = 12,
39    /// 15-word mnemonic (160 bits of entropy)
40    Words15 = 15,
41    /// 18-word mnemonic (192 bits of entropy)
42    Words18 = 18,
43    /// 21-word mnemonic (224 bits of entropy)
44    Words21 = 21,
45    /// 24-word mnemonic (256 bits of entropy)
46    Words24 = 24,
47}
48
49impl WordCount {
50    /// Helper method to get the numeric value of the word count
51    pub fn value(&self) -> usize {
52        *self as usize
53    }
54}
55
56/// Custom error type for bitcoin key and address operations
57#[derive(Debug)]
58pub enum DerivationError {
59    /// Error when an invalid derivation path is provided
60    InvalidDerivationPath(String),
61    /// Error when an unsupported network type is used
62    InvalidNetworkType(String),
63    /// Error when the purpose field (first number after m/) is invalid
64    InvalidPurposeField(String),
65    /// Error when creating or using an X-only public key
66    InvalidXOnlyPubkey(String),
67    /// Error propagated from the BIP39 library
68    Bip39Error(bip39::Error),
69    /// Error propagated from the BIP32 library
70    Bip32Error(bip32::Error),
71    /// Error from the Bitcoin library
72    BitcoinError(String),
73    /// Error from the secp256k1 library
74    SecpError(bitcoin::secp256k1::Error),
75    /// Error when parsing numbers in derivation paths
76    ParseError(std::num::ParseIntError),
77    /// General catch-all error
78    GenericError(String),
79}
80
81impl fmt::Display for DerivationError {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        match self {
84            DerivationError::InvalidDerivationPath(msg) => {
85                write!(f, "Invalid derivation path: {}", msg)
86            }
87            DerivationError::InvalidNetworkType(msg) => write!(f, "Invalid network type: {}", msg),
88            DerivationError::InvalidPurposeField(msg) => {
89                write!(f, "Invalid purpose field: {}", msg)
90            }
91            DerivationError::InvalidXOnlyPubkey(msg) => {
92                write!(f, "Invalid X-only public key: {}", msg)
93            }
94            DerivationError::Bip39Error(e) => write!(f, "BIP39 error: {}", e),
95            DerivationError::Bip32Error(e) => write!(f, "BIP32 error: {}", e),
96            DerivationError::BitcoinError(e) => write!(f, "Bitcoin error: {}", e),
97            DerivationError::SecpError(e) => write!(f, "Secp256k1 error: {}", e),
98            DerivationError::ParseError(e) => write!(f, "Parse error: {}", e),
99            DerivationError::GenericError(msg) => write!(f, "Error: {}", msg),
100        }
101    }
102}
103
104impl StdError for DerivationError {}
105
106// Implement From traits for error conversions
107impl From<bip39::Error> for DerivationError {
108    fn from(err: bip39::Error) -> Self {
109        DerivationError::Bip39Error(err)
110    }
111}
112
113impl From<bip32::Error> for DerivationError {
114    fn from(err: bip32::Error) -> Self {
115        DerivationError::Bip32Error(err)
116    }
117}
118
119impl From<bitcoin::secp256k1::Error> for DerivationError {
120    fn from(err: bitcoin::secp256k1::Error) -> Self {
121        DerivationError::SecpError(err)
122    }
123}
124
125impl From<std::num::ParseIntError> for DerivationError {
126    fn from(err: std::num::ParseIntError) -> Self {
127        DerivationError::ParseError(err)
128    }
129}
130
131/// Secure container for seed data that automatically zeroizes memory when dropped
132struct SecureSeed {
133    seed: Vec<u8>,
134}
135
136impl SecureSeed {
137    /// Create a new secure seed container
138    pub fn new(seed: Vec<u8>) -> Self {
139        Self { seed }
140    }
141}
142
143impl Zeroize for SecureSeed {
144    fn zeroize(&mut self) {
145        self.seed.zeroize();
146    }
147}
148
149impl Drop for SecureSeed {
150    fn drop(&mut self) {
151        self.zeroize();
152    }
153}
154
155/// Secure container for mnemonic phrases that zeroizes memory when dropped
156struct SecureMnemonic {
157    mnemonic: Mnemonic,
158    // Store the phrase string separately so we can zeroize it
159    phrase: String,
160}
161
162impl SecureMnemonic {
163    /// Create a new secure mnemonic container
164    pub fn new(mnemonic: Mnemonic) -> Self {
165        let phrase = mnemonic.to_string();
166        Self { mnemonic, phrase }
167    }
168
169    /// Generate a seed from the mnemonic using the provided passphrase
170    pub fn to_seed(&self, passphrase: &str) -> Vec<u8> {
171        self.mnemonic.to_seed(passphrase).to_vec()
172    }
173}
174
175impl Zeroize for SecureMnemonic {
176    fn zeroize(&mut self) {
177        // Zeroize our stored phrase
178        self.phrase.zeroize();
179        // We can't directly zeroize the Mnemonic internal state,
180        // but we've at least zeroized our copy of the phrase
181    }
182}
183
184impl Drop for SecureMnemonic {
185    fn drop(&mut self) {
186        self.zeroize();
187    }
188}
189
190/// Secure wrapper for private keys that attempts to clean up memory when dropped
191struct SecurePrivateKey {
192    key: bitcoin::secp256k1::SecretKey,
193}
194
195impl SecurePrivateKey {
196    /// Create a new secure private key container
197    pub fn new(key: bitcoin::secp256k1::SecretKey) -> Self {
198        Self { key }
199    }
200
201    /// Get a reference to the wrapped secret key
202    pub fn key(&self) -> &bitcoin::secp256k1::SecretKey {
203        &self.key
204    }
205}
206
207impl Drop for SecurePrivateKey {
208    fn drop(&mut self) {
209        // Best-effort zeroing since SecretKey might not expose internal bytes directly
210        let _ = bitcoin::secp256k1::SecretKey::from_slice(&[0u8; 32]);
211    }
212}
213
214/// Secure wrapper for WIF private keys that attempts to clean up memory when dropped
215struct SecureWifKey {
216    key: bitcoin::PrivateKey,
217}
218
219impl SecureWifKey {
220    /// Create a new secure WIF key container
221    pub fn new(key: bitcoin::PrivateKey) -> Self {
222        Self { key }
223    }
224
225    /// Get the WIF representation of the private key
226    pub fn to_wif(&self) -> String {
227        self.key.to_wif()
228    }
229}
230
231impl Drop for SecureWifKey {
232    fn drop(&mut self) {
233        // Best-effort cleanup
234        if let Ok(zeroed) = bitcoin::secp256k1::SecretKey::from_slice(&[0u8; 32]) {
235            // This replaces the key with a zeroed version when possible
236            let _ = std::mem::replace(&mut self.key.inner, zeroed);
237        }
238    }
239}
240
241/// Generates a new BIP39 mnemonic phrase using a specified word count.
242///
243/// # Arguments
244/// * `word_count` - Optional number of words for the mnemonic (default: 12 words)
245/// * `language` - Optional language for the mnemonic words (default: English)
246///
247/// # Returns
248/// * `Result<String, DerivationError>` - A new mnemonic phrase or an error
249///
250/// # Errors
251/// * `DerivationError::Bip39Error` - If mnemonic generation fails
252pub fn generate_mnemonic(
253    word_count: Option<WordCount>,
254    language: Option<Language>,
255) -> Result<String, DerivationError> {
256    let word_count = word_count.unwrap_or(WordCount::Words12);
257    let lang = language.unwrap_or(Language::English);
258
259    // Generate a mnemonic with the specified number of words in the chosen language
260    let mnemonic = Mnemonic::generate_in(lang, word_count.value())?;
261    let phrase = mnemonic.to_string();
262
263    Ok(phrase)
264}
265
266/// Validates that the purpose field in the derivation path is consistent with the expected address type
267///
268/// # Arguments
269/// * `purpose` - The purpose field value from the derivation path (e.g., "44'")
270///
271/// # Returns
272/// * `Result<(), DerivationError>` - Ok if valid, error otherwise
273///
274/// # Errors
275/// * `DerivationError::InvalidPurposeField` - If purpose is not a supported value
276fn validate_purpose_field(purpose: &str) -> Result<(), DerivationError> {
277    match purpose {
278        "44'" | "49'" | "84'" | "86'" => Ok(()),
279        _ => Err(DerivationError::InvalidPurposeField(format!(
280            "Unsupported purpose field: {}. Expected one of: 44', 49', 84', 86'",
281            purpose
282        ))),
283    }
284}
285
286/// Derives a Bitcoin address from a mnemonic phrase, derivation path, network type, and optional BIP39 passphrase.
287///
288/// # Arguments
289/// * `mnemonic_phrase` - A BIP39 mnemonic phrase (space-separated words)
290/// * `derivation_path_str` - Optional BIP32 derivation path (e.g., "m/84'/0'/0'/0/0"). Default is "m/84'/0'/0'/0/0".
291/// * `network` - Optional network type. Default is Network::Bitcoin.
292/// * `bip39_passphrase` - Optional BIP39 passphrase. Default is an empty string.
293///
294/// # Returns
295/// * `Result<GetAddressResponse, DerivationError>` - Address information or an error
296///
297/// # Errors
298/// * `DerivationError::InvalidDerivationPath` - If the path format is invalid or network mismatches
299/// * `DerivationError::InvalidNetworkType` - If an unsupported network is specified
300/// * `DerivationError::InvalidPurposeField` - If the purpose field is not supported
301/// * `DerivationError::Bip39Error` - If the mnemonic is invalid
302/// * `DerivationError::Bip32Error` - If key derivation fails
303/// * `DerivationError::SecpError` - If secp256k1 operations fail
304/// * `DerivationError::BitcoinError` - If address generation fails
305/// * `DerivationError::InvalidXOnlyPubkey` - If creating an X-only public key fails
306pub fn derive_bitcoin_address(
307    mnemonic_phrase: &str,
308    derivation_path_str: Option<&str>,
309    network: Option<Network>,
310    bip39_passphrase: Option<&str>,
311) -> Result<GetAddressResponse, DerivationError> {
312    let network = network.unwrap_or(DEFAULT_NETWORK);
313    let bip39_passphrase = bip39_passphrase.unwrap_or(DEFAULT_BIP39_PASSPHRASE);
314    let derivation_path_str = derivation_path_str.unwrap_or(DEFAULT_DERIVATION_PATH);
315
316    // Check if the derivation path is in the correct format
317    let path_parts: Vec<&str> = derivation_path_str.split('/').collect();
318    if path_parts.len() != 6 || path_parts[0] != "m" {
319        return Err(DerivationError::InvalidDerivationPath(
320            "Invalid derivation path format. Expected format example: m/84'/0'/0'/0/0".to_string(),
321        ));
322    }
323
324    // Extract and validate the purpose field
325    let purpose = path_parts.get(1).ok_or_else(|| {
326        DerivationError::InvalidDerivationPath(
327            "Missing purpose field in derivation path".to_string(),
328        )
329    })?;
330
331    // Validate the purpose field
332    validate_purpose_field(purpose)?;
333
334    // Check if the second number is correct based on the network type
335    let second_number = path_parts[2].trim_end_matches('\'').parse::<u32>()?;
336    match network {
337        Network::Bitcoin => {
338            if second_number != 0 {
339                return Err(DerivationError::InvalidDerivationPath(format!(
340                    "Invalid Coin number in the derivation path for {}. Expected 0.",
341                    network
342                )));
343            }
344        }
345        Network::Testnet | Network::Regtest | Network::Signet => {
346            if second_number != 1 {
347                return Err(DerivationError::InvalidDerivationPath(format!(
348                    "Invalid Coin number in the derivation path for {}. Expected 1.",
349                    network
350                )));
351            }
352        }
353        // Handle other network types that might be added in the future
354        _ => {
355            return Err(DerivationError::InvalidNetworkType(format!(
356                "Unsupported network type: {}",
357                network
358            )));
359        }
360    }
361
362    // Parse mnemonic and derive keys
363    let mnemonic = match Mnemonic::parse_in(Language::English, mnemonic_phrase) {
364        Ok(m) => SecureMnemonic::new(m),
365        Err(e) => return Err(DerivationError::Bip39Error(e)),
366    };
367
368    // Generate the seed
369    let mut seed_bytes = mnemonic.to_seed(bip39_passphrase);
370    let secure_seed = SecureSeed::new(seed_bytes.clone());
371
372    // Parse the derivation path
373    let derivation_path = match derivation_path_str.parse::<DerivationPath>() {
374        Ok(path) => path,
375        Err(e) => {
376            seed_bytes.zeroize();
377            return Err(DerivationError::Bip32Error(e));
378        }
379    };
380
381    // Derive the extended private key
382    let xprv = match XPrv::derive_from_path(&secure_seed.seed, &derivation_path) {
383        Ok(key) => key,
384        Err(e) => {
385            seed_bytes.zeroize();
386            return Err(DerivationError::Bip32Error(e));
387        }
388    };
389
390    // Derive the secret key
391    let secp = Secp256k1::new();
392    let secret_key = match bitcoin::secp256k1::SecretKey::from_slice(&xprv.private_key().to_bytes())
393    {
394        Ok(key) => SecurePrivateKey::new(key),
395        Err(e) => {
396            seed_bytes.zeroize();
397            return Err(DerivationError::SecpError(e));
398        }
399    };
400
401    // Derive the public key
402    let public_key = PublicKey::from_secret_key(&secp, secret_key.key());
403
404    // Zero out sensitive data
405    seed_bytes.zeroize();
406
407    // Convert to bitcoin CompressedPublicKey
408    let compressed_public_key = match CompressedPublicKey::from_slice(&public_key.serialize()) {
409        Ok(key) => key,
410        Err(e) => {
411            return Err(DerivationError::BitcoinError(format!(
412                "Failed to create compressed public key: {}",
413                e
414            )));
415        }
416    };
417
418    // Determine the address type based on the derivation path
419    let address = match *purpose {
420        "44'" => Address::p2pkh(&compressed_public_key, network),
421        "49'" => Address::p2shwpkh(&compressed_public_key, network),
422        "84'" => Address::p2wpkh(&compressed_public_key, network),
423        "86'" => {
424            // For P2TR, we need to convert to an XOnlyPublicKey
425            let x_only_pubkey = match XOnlyPublicKey::from_slice(&public_key.serialize()[1..]) {
426                Ok(key) => key,
427                Err(e) => {
428                    return Err(DerivationError::InvalidXOnlyPubkey(format!(
429                        "Failed to create XOnlyPublicKey: {}",
430                        e
431                    )));
432                }
433            };
434            let merkle_root = None; // Set the merkle_root to None for a single public key
435            Address::p2tr(&secp, x_only_pubkey, merkle_root, network)
436        }
437        _ => {
438            return Err(DerivationError::InvalidPurposeField(format!(
439                "Unsupported purpose field: {}",
440                purpose
441            )));
442        }
443    };
444
445    let address_string = address.to_string();
446    let public_key_string = public_key.to_string();
447
448    Ok(GetAddressResponse {
449        address: address_string,
450        path: derivation_path_str.to_string(),
451        public_key: public_key_string,
452    })
453}
454
455/// Response structure containing multiple generated Bitcoin addresses.
456#[derive(Debug, Serialize, Deserialize)]
457pub struct GetAddressesResponse {
458    /// Vector of generated Bitcoin addresses
459    pub addresses: Vec<GetAddressResponse>,
460}
461
462/// Derives multiple Bitcoin addresses from a single mnemonic by iterating through a range of indices.
463///
464/// # Arguments
465/// * `mnemonic_phrase` - A BIP39 mnemonic phrase (space-separated words)
466/// * `derivation_path_str` - The derivation path. Can be either a base path (e.g., "m/84'/0'/0'")
467///   or a full path (e.g., "m/84'/0'/0'/0/0"). If a full path is provided, only the base part
468///   (up to the account level) will be used.
469/// * `network` - Optional network type. Default is Network::Bitcoin.
470/// * `bip39_passphrase` - Optional BIP39 passphrase. Default is an empty string.
471/// * `is_change` - Optional boolean indicating whether to derive change addresses (1) or receiving addresses (0).
472///   Default is false (receiving).
473/// * `start_index` - Optional starting index. Default is 0.
474/// * `count` - Optional number of addresses to generate. Default is 1.
475///
476/// # Returns
477/// * `Result<GetAddressesResponse, DerivationError>` - Collection of address information or an error
478///
479/// # Errors
480/// * Same error types as `derive_bitcoin_address`
481pub fn derive_bitcoin_addresses(
482    mnemonic_phrase: &str,
483    derivation_path_str: Option<&str>,
484    network: Option<Network>,
485    bip39_passphrase: Option<&str>,
486    is_change: Option<bool>,
487    start_index: Option<u32>,
488    count: Option<u32>,
489) -> Result<GetAddressesResponse, DerivationError> {
490    let network = network.unwrap_or(DEFAULT_NETWORK);
491    let bip39_passphrase = bip39_passphrase.unwrap_or(DEFAULT_BIP39_PASSPHRASE);
492    let path = derivation_path_str.unwrap_or("m/84'/0'/0'");
493    let is_change = is_change.unwrap_or(false);
494    let start_index = start_index.unwrap_or(0);
495    let count = count.unwrap_or(1);
496
497    // Create a vector to store all the derived addresses
498    let mut addresses = Vec::with_capacity(count as usize);
499
500    // Split the path to extract the base path
501    let path_parts: Vec<&str> = path.split('/').collect();
502
503    // Extract the base path (stopping at the account level)
504    let base_path = if path_parts.len() >= 4 && path_parts[0] == "m" {
505        // Take the first 4 components (m/purpose'/coin'/account')
506        path_parts[..4].join("/")
507    } else {
508        return Err(DerivationError::InvalidDerivationPath(
509            "Invalid derivation path format. Expected format that includes at least: m/purpose'/coin'/account'".to_string()
510        ));
511    };
512
513    // Extract and validate the purpose field
514    let purpose = path_parts.get(1).ok_or_else(|| {
515        DerivationError::InvalidDerivationPath(
516            "Missing purpose field in derivation path".to_string(),
517        )
518    })?;
519
520    // Validate the purpose field
521    validate_purpose_field(purpose)?;
522
523    // Check if the second number is correct based on the network type
524    let second_number = path_parts[2].trim_end_matches('\'').parse::<u32>()?;
525    match network {
526        Network::Bitcoin => {
527            if second_number != 0 {
528                return Err(DerivationError::InvalidDerivationPath(format!(
529                    "Invalid Coin number in the derivation path for {}. Expected 0.",
530                    network
531                )));
532            }
533        }
534        Network::Testnet | Network::Regtest | Network::Signet => {
535            if second_number != 1 {
536                return Err(DerivationError::InvalidDerivationPath(format!(
537                    "Invalid Coin number in the derivation path for {}. Expected 1.",
538                    network
539                )));
540            }
541        }
542        // Handle other network types that might be added in the future
543        _ => {
544            return Err(DerivationError::InvalidNetworkType(format!(
545                "Unsupported network type: {}",
546                network
547            )));
548        }
549    }
550
551    // Determine the change path component (0 for receiving addresses, 1 for change addresses)
552    let change_component = if is_change { "1" } else { "0" };
553
554    // Generate addresses for the specified range
555    for i in start_index..(start_index + count) {
556        // Construct the full derivation path for this index
557        let full_path = format!("{}/{}/{}", base_path, change_component, i);
558
559        // Use the existing derive_bitcoin_address function to derive the address
560        match derive_bitcoin_address(
561            mnemonic_phrase,
562            Some(&full_path),
563            Some(network),
564            Some(bip39_passphrase),
565        ) {
566            Ok(address) => addresses.push(address),
567            Err(e) => return Err(e),
568        }
569    }
570
571    Ok(GetAddressesResponse { addresses })
572}
573
574/// Calculates a script hash for a given Bitcoin address and network type.
575///
576/// The script hash is calculated by taking the SHA256 hash of the scriptPubKey
577/// and then reversing the byte order, as required by the Electrum protocol.
578///
579/// # Arguments
580/// * `address` - A Bitcoin address string
581/// * `network` - Optional network type. Default is Network::Bitcoin.
582///
583/// # Returns
584/// * `Result<String, DerivationError>` - The script hash as a hex string or an error
585///
586/// # Errors
587/// * `DerivationError::BitcoinError` - If the address cannot be parsed or network validation fails
588pub fn calculate_script_hash(
589    address: &str,
590    network: Option<Network>,
591) -> Result<String, DerivationError> {
592    let network = network.unwrap_or(DEFAULT_NETWORK);
593
594    // Parse the address
595    let parsed_addr = match Address::from_str(address) {
596        Ok(addr) => addr,
597        Err(e) => {
598            return Err(DerivationError::BitcoinError(format!(
599                "Failed to parse address: {}",
600                e
601            )));
602        }
603    };
604
605    let addr = match parsed_addr.require_network(network) {
606        Ok(address) => address,
607        Err(e) => {
608            return Err(DerivationError::BitcoinError(format!(
609                "Network mismatch: {}",
610                e
611            )));
612        }
613    };
614
615    // Get the script from the address
616    let script = addr.script_pubkey();
617    let script_bytes = script.as_bytes();
618
619    // Calculate the script hash
620    let hash = Sha256::digest(script_bytes);
621
622    // Reverse the bytes of the hash
623    let mut reversed_hash = hash.to_vec();
624    reversed_hash.reverse();
625
626    // Convert the reversed hash to hexadecimal representation
627    let script_hash_hex = hex::encode(reversed_hash);
628
629    Ok(script_hash_hex)
630}
631
632/// Derives a private key in WIF format from a mnemonic phrase, derivation path, network type, and optional BIP39 passphrase.
633///
634/// # Arguments
635/// * `mnemonic_phrase` - A BIP39 mnemonic phrase (space-separated words)
636/// * `derivation_path_str` - Optional BIP32 derivation path (e.g., "m/84'/0'/0'/0/0"). Default is "m/84'/0'/0'/0/0".
637/// * `network` - Optional network type. Default is Network::Bitcoin.
638/// * `bip39_passphrase` - Optional BIP39 passphrase. Default is an empty string.
639///
640/// # Returns
641/// * `Result<String, DerivationError>` - The private key in WIF format or an error
642///
643/// # Errors
644/// * `DerivationError::Bip39Error` - If the mnemonic is invalid
645/// * `DerivationError::Bip32Error` - If key derivation fails
646/// * `DerivationError::SecpError` - If secp256k1 operations fail
647pub fn derive_private_key(
648    mnemonic_phrase: &str,
649    derivation_path_str: Option<&str>,
650    network: Option<Network>,
651    bip39_passphrase: Option<&str>,
652) -> Result<String, DerivationError> {
653    let network = network.unwrap_or(DEFAULT_NETWORK);
654    let bip39_passphrase = bip39_passphrase.unwrap_or(DEFAULT_BIP39_PASSPHRASE);
655    let derivation_path_str = derivation_path_str.unwrap_or(DEFAULT_DERIVATION_PATH);
656
657    // Parse mnemonic and create secure wrapper
658    let mnemonic = match Mnemonic::parse_in(Language::English, mnemonic_phrase) {
659        Ok(m) => SecureMnemonic::new(m),
660        Err(e) => return Err(DerivationError::Bip39Error(e)),
661    };
662
663    // Generate the seed
664    let mut seed_bytes = mnemonic.to_seed(bip39_passphrase);
665    let secure_seed = SecureSeed::new(seed_bytes.clone());
666
667    // Parse the derivation path
668    let derivation_path = match derivation_path_str.parse::<DerivationPath>() {
669        Ok(path) => path,
670        Err(e) => {
671            seed_bytes.zeroize();
672            return Err(DerivationError::Bip32Error(e));
673        }
674    };
675
676    // Derive the extended private key
677    let xprv = match XPrv::derive_from_path(&secure_seed.seed, &derivation_path) {
678        Ok(key) => key,
679        Err(e) => {
680            seed_bytes.zeroize();
681            return Err(DerivationError::Bip32Error(e));
682        }
683    };
684
685    // Derive the secret key
686    let secret_key = match bitcoin::secp256k1::SecretKey::from_slice(&xprv.private_key().to_bytes())
687    {
688        Ok(key) => key,
689        Err(e) => {
690            seed_bytes.zeroize();
691            return Err(DerivationError::SecpError(e));
692        }
693    };
694
695    // Zero out sensitive data
696    seed_bytes.zeroize();
697
698    // Convert the private key to WIF format using a secure wrapper
699    let private_key_wif = SecureWifKey::new(bitcoin::PrivateKey {
700        compressed: true,
701        network: network.into(),
702        inner: secret_key,
703    });
704
705    // Get the WIF string
706    let private_key_string = private_key_wif.to_wif();
707
708    Ok(private_key_string)
709}
710
711// ============================================================================
712// BIP39 Mnemonic Utilities
713// ============================================================================
714
715/// Validates a BIP39 mnemonic phrase.
716///
717/// # Arguments
718/// * `mnemonic_phrase` - The mnemonic phrase to validate (space-separated words)
719///
720/// # Returns
721/// * `Result<(), DerivationError>` - Ok if valid, error otherwise
722///
723/// # Example
724/// ```
725/// use bitcoin_address_generator::validate_mnemonic;
726///
727/// let valid = validate_mnemonic("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about");
728/// assert!(valid.is_ok());
729///
730/// let invalid = validate_mnemonic("invalid word sequence");
731/// assert!(invalid.is_err());
732/// ```
733pub fn validate_mnemonic(mnemonic_phrase: &str) -> Result<(), DerivationError> {
734    Mnemonic::from_str(mnemonic_phrase)
735        .map(|_| ())
736        .map_err(|e| DerivationError::Bip39Error(e))
737}
738
739/// Checks if a word is a valid BIP39 word for the specified language.
740///
741/// # Arguments
742/// * `word` - The word to check
743/// * `language` - Optional language (default: English)
744///
745/// # Returns
746/// * `bool` - true if the word is valid, false otherwise
747///
748/// # Example
749/// ```
750/// use bitcoin_address_generator::is_valid_bip39_word;
751///
752/// assert!(is_valid_bip39_word("abandon", None));
753/// assert!(!is_valid_bip39_word("notaword", None));
754/// ```
755pub fn is_valid_bip39_word(word: &str, language: Option<Language>) -> bool {
756    let lang = language.unwrap_or(Language::English);
757    lang.word_list()
758        .iter()
759        .any(|w| w.to_lowercase() == word.to_lowercase())
760}
761
762/// Gets word suggestions for partial input (autocomplete).
763///
764/// # Arguments
765/// * `partial_word` - The partial word to get suggestions for
766/// * `limit` - Maximum number of suggestions to return
767/// * `language` - Optional language (default: English)
768///
769/// # Returns
770/// * `Vec<String>` - A sorted list of suggested words
771///
772/// # Example
773/// ```
774/// use bitcoin_address_generator::get_bip39_suggestions;
775///
776/// let suggestions = get_bip39_suggestions("ab", 5, None);
777/// assert!(suggestions.contains(&"abandon".to_string()));
778/// assert!(suggestions.len() <= 5);
779/// ```
780pub fn get_bip39_suggestions(
781    partial_word: &str,
782    limit: usize,
783    language: Option<Language>,
784) -> Vec<String> {
785    let lang = language.unwrap_or(Language::English);
786    let lowercased = partial_word.to_lowercase();
787
788    let mut suggestions: Vec<String> = lang
789        .word_list()
790        .iter()
791        .filter(|word| word.starts_with(&lowercased))
792        .map(|w| w.to_string())
793        .collect();
794
795    suggestions.sort();
796    suggestions.truncate(limit);
797    suggestions
798}
799
800/// Gets the full BIP39 wordlist for the specified language.
801///
802/// # Arguments
803/// * `language` - Optional language (default: English)
804///
805/// # Returns
806/// * `Vec<String>` - The complete wordlist
807///
808/// # Example
809/// ```
810/// use bitcoin_address_generator::get_bip39_wordlist;
811///
812/// let wordlist = get_bip39_wordlist(None);
813/// assert_eq!(wordlist.len(), 2048);
814/// ```
815pub fn get_bip39_wordlist(language: Option<Language>) -> Vec<String> {
816    let lang = language.unwrap_or(Language::English);
817    lang.word_list()
818        .iter()
819        .map(|w| w.to_string())
820        .collect()
821}
822
823/// Converts a mnemonic phrase to entropy bytes.
824///
825/// # Arguments
826/// * `mnemonic_phrase` - The mnemonic phrase to convert
827///
828/// # Returns
829/// * `Result<Vec<u8>, DerivationError>` - The entropy bytes or an error
830///
831/// # Security
832/// **WARNING**: The returned entropy is sensitive cryptographic material.
833/// Callers must ensure the returned `Vec<u8>` is properly zeroized when no longer needed.
834/// Consider using `zeroize::Zeroize` trait to securely clear the data from memory.
835///
836/// # Example
837/// ```
838/// use bitcoin_address_generator::mnemonic_to_entropy;
839///
840/// let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
841/// let entropy = mnemonic_to_entropy(mnemonic).unwrap();
842/// assert_eq!(entropy.len(), 16); // 128 bits for 12-word mnemonic
843/// ```
844pub fn mnemonic_to_entropy(mnemonic_phrase: &str) -> Result<Vec<u8>, DerivationError> {
845    let mnemonic = Mnemonic::from_str(mnemonic_phrase)?;
846    Ok(mnemonic.to_entropy().to_vec())
847}
848
849/// Converts entropy bytes to a mnemonic phrase.
850///
851/// # Arguments
852/// * `entropy` - The entropy bytes to convert
853/// * `language` - Optional language (default: English)
854///
855/// # Returns
856/// * `Result<String, DerivationError>` - The mnemonic phrase or an error
857///
858/// # Example
859/// ```
860/// use bitcoin_address_generator::entropy_to_mnemonic;
861///
862/// let entropy = vec![0u8; 16]; // 128 bits
863/// let mnemonic = entropy_to_mnemonic(&entropy, None).unwrap();
864/// assert_eq!(mnemonic.split_whitespace().count(), 12);
865/// ```
866pub fn entropy_to_mnemonic(
867    entropy: &[u8],
868    language: Option<Language>,
869) -> Result<String, DerivationError> {
870    let lang = language.unwrap_or(Language::English);
871    let mnemonic = Mnemonic::from_entropy_in(lang, entropy)?;
872    Ok(mnemonic.to_string())
873}
874
875/// Converts a mnemonic phrase to a seed with optional passphrase.
876///
877/// # Arguments
878/// * `mnemonic_phrase` - The mnemonic phrase to convert
879/// * `passphrase` - Optional BIP39 passphrase (default: empty string)
880///
881/// # Returns
882/// * `Result<Vec<u8>, DerivationError>` - The seed bytes (always 64 bytes) or an error
883///
884/// # Security
885/// **WARNING**: The returned seed is highly sensitive cryptographic material.
886/// Callers must ensure the returned `Vec<u8>` is properly zeroized when no longer needed.
887/// Consider using `zeroize::Zeroize` trait to securely clear the data from memory.
888///
889/// # Example
890/// ```
891/// use bitcoin_address_generator::mnemonic_to_seed;
892///
893/// let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
894/// let seed = mnemonic_to_seed(mnemonic, None).unwrap();
895/// assert_eq!(seed.len(), 64);
896/// ```
897pub fn mnemonic_to_seed(
898    mnemonic_phrase: &str,
899    passphrase: Option<&str>,
900) -> Result<Vec<u8>, DerivationError> {
901    let mnemonic = Mnemonic::from_str(mnemonic_phrase)?;
902    let passphrase = passphrase.unwrap_or("");
903    Ok(mnemonic.to_seed(passphrase).to_vec())
904}
905
906#[cfg(test)]
907mod tests {
908    use super::*;
909    use std::option::Option;
910
911    #[test]
912    fn test_generate_mnemonic() {
913        // Test 12-word mnemonic
914        let mnemonic12: String = generate_mnemonic(
915            Option::from(WordCount::Words12),
916            Option::from(Language::English),
917        )
918        .unwrap();
919        println!("Generated 12-word mnemonic: {}", mnemonic12);
920        assert_eq!(mnemonic12.split_whitespace().count(), 12);
921
922        // Test 15-word mnemonic
923        let mnemonic15: String = generate_mnemonic(
924            Option::from(WordCount::Words15),
925            Option::from(Language::English),
926        )
927        .unwrap();
928        println!("Generated 15-word mnemonic: {}", mnemonic15);
929        assert_eq!(mnemonic15.split_whitespace().count(), 15);
930
931        // Test 18-word mnemonic
932        let mnemonic18: String = generate_mnemonic(
933            Option::from(WordCount::Words18),
934            Option::from(Language::English),
935        )
936        .unwrap();
937        println!("Generated 18-word mnemonic: {}", mnemonic18);
938        assert_eq!(mnemonic18.split_whitespace().count(), 18);
939
940        // Test 21-word mnemonic
941        let mnemonic21: String = generate_mnemonic(
942            Option::from(WordCount::Words21),
943            Option::from(Language::English),
944        )
945        .unwrap();
946        println!("Generated 21-word mnemonic: {}", mnemonic21);
947        assert_eq!(mnemonic21.split_whitespace().count(), 21);
948
949        // Test 24-word mnemonic
950        let mnemonic24: String = generate_mnemonic(
951            Option::from(WordCount::Words24),
952            Option::from(Language::English),
953        )
954        .unwrap();
955        println!("Generated 24-word mnemonic: {}", mnemonic24);
956        assert_eq!(mnemonic24.split_whitespace().count(), 24);
957    }
958
959    #[test]
960    fn test_derive_address_p2pkh() {
961        // Test with a known mnemonic and expected address for legacy P2PKH (44' path)
962        let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
963        let path = "m/44'/0'/0'/0/0";
964        let address = derive_bitcoin_address(
965            mnemonic,
966            Option::from(path),
967            Option::from(Network::Bitcoin),
968            None,
969        )
970        .unwrap();
971        println!("P2PKH address: {}", address.address);
972        println!("P2PKH pubkey: {}", address.public_key);
973        assert_eq!(address.address, "1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA");
974        assert_eq!(address.path, path);
975    }
976
977    #[test]
978    fn test_derive_address_p2shwpkh() {
979        // Test with a known mnemonic and expected address for P2SH-wrapped SegWit (49' path)
980        let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
981        let path = "m/49'/0'/0'/0/0";
982        let address = derive_bitcoin_address(
983            mnemonic,
984            Option::from(path),
985            Option::from(Network::Bitcoin),
986            None,
987        )
988        .unwrap();
989        println!("P2SH-WPKH address: {}", address.address);
990        println!("P2SH-WPKH pubkey: {}", address.public_key);
991        assert_eq!(address.address, "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf");
992        assert_eq!(address.path, path);
993    }
994
995    #[test]
996    fn test_derive_address_p2wpkh() {
997        // Test with a known mnemonic and expected address for native SegWit (84' path)
998        let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
999        let path = "m/84'/0'/0'/0/0";
1000        let address = derive_bitcoin_address(
1001            mnemonic,
1002            Option::from(path),
1003            Option::from(Network::Bitcoin),
1004            None,
1005        )
1006        .unwrap();
1007        println!("P2WPKH address: {}", address.address);
1008        println!("P2WPKH pubkey: {}", address.public_key);
1009        assert_eq!(
1010            address.address,
1011            "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"
1012        );
1013        assert_eq!(address.path, path);
1014    }
1015
1016    #[test]
1017    fn test_derive_address_p2tr() {
1018        // Test with a known mnemonic and expected address for Taproot (86' path)
1019        let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
1020        let path = "m/86'/0'/0'/0/0";
1021        let address = derive_bitcoin_address(
1022            mnemonic,
1023            Option::from(path),
1024            Option::from(Network::Bitcoin),
1025            None,
1026        )
1027        .unwrap();
1028        println!("P2TR address: {}", address.address);
1029        println!("P2TR pubkey: {}", address.public_key);
1030        // We're using a flexible assertion here since the exact address might vary by implementation
1031        assert!(address.address.starts_with("bc1p"));
1032        assert_eq!(address.path, path);
1033    }
1034
1035    #[test]
1036    fn test_calculate_script_hash() {
1037        // Test script hash generation for P2PKH address
1038        let script_hash = calculate_script_hash(
1039            "1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA",
1040            Option::from(Network::Bitcoin),
1041        )
1042        .unwrap();
1043        println!("P2PKH script hash: {}", script_hash);
1044        assert_eq!(
1045            script_hash,
1046            "1e8750b8a4c0912d8b84f7eb53472cbdcb57f9e0cde263b2e51ecbe30853cd68"
1047        );
1048
1049        // Test script hash generation for P2WPKH address
1050        let script_hash = calculate_script_hash(
1051            "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu",
1052            Option::from(Network::Bitcoin),
1053        )
1054        .unwrap();
1055        println!("P2WPKH script hash: {}", script_hash);
1056        // Let's check the actual value
1057        assert_eq!(script_hash.len(), 64); // It should be a 32-byte hash (64 hex chars)
1058    }
1059
1060    #[test]
1061    fn test_derive_private_key() {
1062        // Test private key generation
1063        let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
1064        let path = "m/84'/0'/0'/0/0";
1065        let private_key = derive_private_key(
1066            mnemonic,
1067            Option::from(path),
1068            Option::from(Network::Bitcoin),
1069            None,
1070        )
1071        .unwrap();
1072        assert_eq!(
1073            private_key,
1074            "KyZpNDKnfs94vbrwhJneDi77V6jF64PWPF8x5cdJb8ifgg2DUc9d"
1075        );
1076    }
1077
1078    #[test]
1079    fn test_derive_bitcoin_addresses() {
1080        // Test deriving multiple addresses with base path
1081        let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
1082        let base_path = "m/84'/0'/0'";
1083        let result = derive_bitcoin_addresses(
1084            mnemonic,
1085            Option::from(base_path),
1086            Option::from(Network::Bitcoin),
1087            None,
1088            None,
1089            None,
1090            Option::from(3),
1091        )
1092        .unwrap();
1093
1094        // Check that we got the right number of addresses
1095        assert_eq!(result.addresses.len(), 3);
1096
1097        // Check the paths are correct
1098        assert_eq!(result.addresses[0].path, "m/84'/0'/0'/0/0");
1099        assert_eq!(result.addresses[1].path, "m/84'/0'/0'/0/1");
1100        assert_eq!(result.addresses[2].path, "m/84'/0'/0'/0/2");
1101
1102        // Check the first address is correct
1103        assert_eq!(
1104            result.addresses[0].address,
1105            "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"
1106        );
1107
1108        // Test with full path - should extract and use only the base path
1109        let full_path = "m/84'/0'/0'/0/5"; // Has index 5, but we should use our start_index instead
1110        let full_path_result = derive_bitcoin_addresses(
1111            mnemonic,
1112            Option::from(full_path),
1113            Option::from(Network::Bitcoin),
1114            None,
1115            None,
1116            Option::from(10),
1117            Option::from(2),
1118        )
1119        .unwrap();
1120
1121        // We should ignore the /0/5 part of the full path and use our parameters
1122        assert_eq!(full_path_result.addresses[0].path, "m/84'/0'/0'/0/10");
1123        assert_eq!(full_path_result.addresses[1].path, "m/84'/0'/0'/0/11");
1124
1125        // Test deriving change addresses with full path
1126        let full_path_change = derive_bitcoin_addresses(
1127            mnemonic,
1128            Option::from(full_path),
1129            Option::from(Network::Bitcoin),
1130            None,
1131            Option::from(true),
1132            None,
1133            Option::from(2),
1134        )
1135        .unwrap();
1136
1137        // Should use the is_change parameter regardless of what was in the path
1138        assert_eq!(full_path_change.addresses[0].path, "m/84'/0'/0'/1/0");
1139        assert_eq!(full_path_change.addresses[1].path, "m/84'/0'/0'/1/1");
1140
1141        // Test with a full change path - should still extract only the base path
1142        let full_change_path = "m/84'/0'/0'/1/0";
1143        let change_override_result = derive_bitcoin_addresses(
1144            mnemonic,
1145            Option::from(full_change_path),
1146            Option::from(Network::Bitcoin),
1147            None,
1148            Option::from(false),
1149            None,
1150            Option::from(2),
1151        )
1152        .unwrap();
1153
1154        // Should use the is_change parameter (false) regardless of what was in the path
1155        assert_eq!(change_override_result.addresses[0].path, "m/84'/0'/0'/0/0");
1156        assert_eq!(change_override_result.addresses[1].path, "m/84'/0'/0'/0/1");
1157    }
1158
1159    #[test]
1160    fn test_validate_mnemonic() {
1161        let valid_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
1162        assert!(validate_mnemonic(valid_mnemonic).is_ok());
1163
1164        let invalid_mnemonic = "invalid word sequence that is not valid";
1165        assert!(validate_mnemonic(invalid_mnemonic).is_err());
1166    }
1167
1168    #[test]
1169    fn test_derive_addresses_with_varying_mnemonic_lengths() {
1170        // Test 12-word mnemonic (known valid test vector)
1171        let mnemonic12 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
1172        let result12 = derive_bitcoin_address(
1173            mnemonic12,
1174            Some("m/84'/0'/0'/0/0"),
1175            Some(Network::Bitcoin),
1176            None,
1177        );
1178        assert!(result12.is_ok());
1179        assert_eq!(result12.unwrap().address, "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu");
1180
1181        // Generate and test 15-word mnemonic
1182        let mnemonic15 = generate_mnemonic(Some(WordCount::Words15), None).unwrap();
1183        assert_eq!(mnemonic15.split_whitespace().count(), 15);
1184        let result15 = derive_bitcoin_address(
1185            &mnemonic15,
1186            Some("m/84'/0'/0'/0/0"),
1187            Some(Network::Bitcoin),
1188            None,
1189        );
1190        assert!(result15.is_ok());
1191        assert!(result15.unwrap().address.starts_with("bc1"));
1192
1193        // Generate and test 18-word mnemonic
1194        let mnemonic18 = generate_mnemonic(Some(WordCount::Words18), None).unwrap();
1195        assert_eq!(mnemonic18.split_whitespace().count(), 18);
1196        let result18 = derive_bitcoin_address(
1197            &mnemonic18,
1198            Some("m/84'/0'/0'/0/0"),
1199            Some(Network::Bitcoin),
1200            None,
1201        );
1202        assert!(result18.is_ok());
1203        assert!(result18.unwrap().address.starts_with("bc1"));
1204
1205        // Generate and test 21-word mnemonic
1206        let mnemonic21 = generate_mnemonic(Some(WordCount::Words21), None).unwrap();
1207        assert_eq!(mnemonic21.split_whitespace().count(), 21);
1208        let result21 = derive_bitcoin_address(
1209            &mnemonic21,
1210            Some("m/84'/0'/0'/0/0"),
1211            Some(Network::Bitcoin),
1212            None,
1213        );
1214        assert!(result21.is_ok());
1215        assert!(result21.unwrap().address.starts_with("bc1"));
1216
1217        // Generate and test 24-word mnemonic
1218        let mnemonic24 = generate_mnemonic(Some(WordCount::Words24), None).unwrap();
1219        assert_eq!(mnemonic24.split_whitespace().count(), 24);
1220        let result24 = derive_bitcoin_address(
1221            &mnemonic24,
1222            Some("m/84'/0'/0'/0/0"),
1223            Some(Network::Bitcoin),
1224            None,
1225        );
1226        assert!(result24.is_ok());
1227        assert!(result24.unwrap().address.starts_with("bc1"));
1228    }
1229
1230    #[test]
1231    fn test_is_valid_bip39_word() {
1232        assert!(is_valid_bip39_word("abandon", None));
1233        assert!(is_valid_bip39_word("ABANDON", None)); // Case insensitive
1234        assert!(!is_valid_bip39_word("notaword", None));
1235    }
1236
1237    #[test]
1238    fn test_get_bip39_suggestions() {
1239        let suggestions = get_bip39_suggestions("ab", 5, None);
1240        assert!(!suggestions.is_empty());
1241        assert!(suggestions.contains(&"abandon".to_string()));
1242        assert!(suggestions.contains(&"ability".to_string()));
1243        assert!(suggestions.len() <= 5);
1244
1245        // Check that suggestions are sorted
1246        let mut sorted = suggestions.clone();
1247        sorted.sort();
1248        assert_eq!(suggestions, sorted);
1249    }
1250
1251    #[test]
1252    fn test_get_bip39_wordlist() {
1253        let wordlist = get_bip39_wordlist(None);
1254        assert_eq!(wordlist.len(), 2048);
1255        assert!(wordlist.contains(&"abandon".to_string()));
1256        assert!(wordlist.contains(&"zoo".to_string()));
1257    }
1258
1259    #[test]
1260    fn test_mnemonic_entropy_conversion() {
1261        // Test 12-word mnemonic (128 bits) - known valid test vector
1262        let mnemonic12 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
1263        let entropy12 = mnemonic_to_entropy(mnemonic12).unwrap();
1264        assert_eq!(entropy12.len(), 16); // 128 bits for 12-word mnemonic
1265        let recovered_mnemonic12 = entropy_to_mnemonic(&entropy12, None).unwrap();
1266        assert_eq!(mnemonic12, recovered_mnemonic12);
1267
1268        // Generate and test 15-word mnemonic (160 bits)
1269        let generated15 = generate_mnemonic(Some(WordCount::Words15), None).unwrap();
1270        let entropy15 = mnemonic_to_entropy(&generated15).unwrap();
1271        assert_eq!(entropy15.len(), 20); // 160 bits for 15-word mnemonic
1272        let recovered_mnemonic15 = entropy_to_mnemonic(&entropy15, None).unwrap();
1273        assert_eq!(generated15, recovered_mnemonic15);
1274
1275        // Generate and test 18-word mnemonic (192 bits)
1276        let generated18 = generate_mnemonic(Some(WordCount::Words18), None).unwrap();
1277        let entropy18 = mnemonic_to_entropy(&generated18).unwrap();
1278        assert_eq!(entropy18.len(), 24); // 192 bits for 18-word mnemonic
1279        let recovered_mnemonic18 = entropy_to_mnemonic(&entropy18, None).unwrap();
1280        assert_eq!(generated18, recovered_mnemonic18);
1281
1282        // Generate and test 21-word mnemonic (224 bits)
1283        let generated21 = generate_mnemonic(Some(WordCount::Words21), None).unwrap();
1284        let entropy21 = mnemonic_to_entropy(&generated21).unwrap();
1285        assert_eq!(entropy21.len(), 28); // 224 bits for 21-word mnemonic
1286        let recovered_mnemonic21 = entropy_to_mnemonic(&entropy21, None).unwrap();
1287        assert_eq!(generated21, recovered_mnemonic21);
1288
1289        // Generate and test 24-word mnemonic (256 bits)
1290        let generated24 = generate_mnemonic(Some(WordCount::Words24), None).unwrap();
1291        let entropy24 = mnemonic_to_entropy(&generated24).unwrap();
1292        assert_eq!(entropy24.len(), 32); // 256 bits for 24-word mnemonic
1293        let recovered_mnemonic24 = entropy_to_mnemonic(&entropy24, None).unwrap();
1294        assert_eq!(generated24, recovered_mnemonic24);
1295    }
1296
1297    #[test]
1298    fn test_mnemonic_to_seed() {
1299        let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
1300
1301        // Without passphrase - verify against known test vector
1302        let seed1 = mnemonic_to_seed(mnemonic, None).unwrap();
1303        assert_eq!(seed1.len(), 64);
1304        let expected_seed = hex::decode("5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4").unwrap();
1305        assert_eq!(seed1, expected_seed);
1306
1307        // With passphrase
1308        let seed2 = mnemonic_to_seed(mnemonic, Some("passphrase")).unwrap();
1309        assert_eq!(seed2.len(), 64);
1310
1311        // Different passphrases should produce different seeds
1312        assert_ne!(seed1, seed2);
1313    }
1314}