diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f0e3bca --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk \ No newline at end of file diff --git a/.gitignore b/.gitignore index dd15442..641b0b4 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,6 @@ target/ .vscode/ coverage/ -*.profraw \ No newline at end of file +*.profraw + +.devcontainer/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 1244ee1..fe66a09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,25 +43,20 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", + "once_cell", "windows-sys", ] -[[package]] -name = "base58" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" - [[package]] name = "bip-tools" version = "0.1.0" dependencies = [ - "base58", + "bs58", "clap", "hmac", "ripemd", @@ -78,11 +73,20 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "cc" -version = "1.2.6" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333" +checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" dependencies = [ "shlex", ] @@ -95,9 +99,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.23" +version = "4.5.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184" dependencies = [ "clap_builder", "clap_derive", @@ -105,9 +109,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.23" +version = "4.5.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9" dependencies = [ "anstream", "anstyle", @@ -117,9 +121,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" dependencies = [ "heck", "proc-macro2", @@ -141,9 +145,9 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "cpufeatures" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -206,11 +210,17 @@ version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +[[package]] +name = "once_cell" +version = "1.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" + [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -282,15 +292,30 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.93" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "tinyvec" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "typenum" version = "1.17.0" @@ -299,9 +324,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "utf8parse" diff --git a/Cargo.toml b/Cargo.toml index 47769bc..3d0eb17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,17 +9,10 @@ description = "A Rust library and CLI tool for Bitcoin address generation and ma name = "bip-tools" path = "src/main.rs" -[package.metadata.llvm-cov] -output-dir = "coverage" -html = true - [dependencies] secp256k1 = "0.29" hmac = "0.12" sha2 = "0.10" ripemd = "0.1" -base58 = "0.2" clap = { version = "4.5.16", features = ["derive"] } - -[package.metadata.bin] -commitlint-rs = { version = "0.1.11", bins = ["commitlint"] } +bs58 = "0.5.1" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6ee55cd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM rust:1.85 AS builder + +WORKDIR /usr/src/bip-tools +COPY . . + +RUN cargo install --path . + +FROM debian:bookworm-slim + +COPY --from=builder /usr/local/cargo/bin/bip-tools /usr/local/bin/bip-tools + +ENTRYPOINT ["bip-tools"] \ No newline at end of file diff --git a/README.md b/README.md index 0db9673..9d9df99 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,30 @@ # bip-tools -A Rust library and CLI tool for Bitcoin address generation and management using extended public keys (xpub). This project implements BIP32 and BIP44 specifications for hierarchical deterministic wallet address derivation. +⚠️Notice: This project is currently in the testing phase and is not yet ready for production use.⚠️ + +A robust Rust library and CLI tool for hierarchical deterministic (HD) wallet operations, supporting multiple cryptocurrencies and address formats. Built for developers and blockchain enthusiasts, bip-tools provides an efficient way to manage extended public keys (xpub) and generate addresses compliant with BIP32 and BIP44 standards. ## Features -- Extended Public Key (xpub) management -- BIP32 hierarchical deterministic address generation -- BIP44 compliant address derivation -- Command-line interface for easy address generation -- Support for legacy Bitcoin addresses (P2PKH) +- **Extended Public Key (xpub) Management:** Parse, serialize, and derive child keys from xpub strings. +- **BIP32 and BIP44 Compliance:** Generate addresses using standard derivation paths for BIP32 and BIP44. +- **Multi-Cryptocurrency Support:** Supports Bitcoin, Litecoin, Dogecoin, and Bitcoin Cash. +- **Flexible Address Formats:** Generate legacy (P2PKH) addresses and Bitcoin Cash-specific formats (Legacy, CashAddr, CashAddr with prefix). +- **Command-Line Interface (CLI):** User-friendly CLI for generating addresses with customizable options. + +## Supported Coins +| Coin | BIP32 Version | Address Formats | +|------|---------------|-----------------| +| Bitcoin | xpub | Legacy | +| Litecoin | xpub | Legacy | +| Dogecoin | xpub | Legacy | +| Bitcoin Cash | xpub | Legacy, CashAddr | + +### Address Formats +- Legacy (P2PKH) +- Bitcoin Cash: + - CashAddr (with/without prefix) + - Legacy Base58 ## Installation @@ -16,7 +32,7 @@ The compiled binary will be available at `target/release/bip-tools` ## Using Cargo ```bash -cargo install --git https://github.com/yigitraphy/biptools.git +cargo install --git https://github.com/blockchain-labs-inc/bip-tools.git ``` ### Building from Source @@ -27,7 +43,7 @@ cargo install --git https://github.com/yigitraphy/biptools.git - Cargo package manager ```bash -git clone https://github.com/yigitraphy/biptools.git +git clone https://github.com/blockchain-labs-inc/bip-tools.git cd bip-tools cargo build --release ``` @@ -44,51 +60,115 @@ bip-tools = "0.1.0" ### Example Code ```rust -use bip_tools::Xpub; - -// Parse an xpub from its Base58 string representation -let xpub = Xpub::from_base58("xpub6CUGRUo...").unwrap(); - -// Generate 5 BIP44 addresses -let addresses = xpub.derive_bip44_addresses(5).unwrap(); -for (i, address) in addresses.iter().enumerate() { - println!("Address {}: {}", i, address); +use bip_tools::{CoinType, Xpub}; + +fn main() -> Result<(), Box> { + let xpub_str = "xpub6Dix4qijz1p9XB7eiuYe5anj3qiveYg4UQvqhJcJbMraGEQegMhbt3BcLd5fnmgp6eWRGtjiWcdkck749k5KgYHXH8UY9MDRwDye43ok3Hr"; + let xpub = Xpub::from_base58(xpub_str, CoinType::Bitcoin)?; + let addresses = xpub.derive_bip32_addresses(3, &None)?; + + for (i, addr) in addresses.iter().enumerate() { + println!("Address {}: {}", i, addr); + } + + Ok(()) } ``` ## CLI Usage -The CLI tool provides two main commands for address generation: +The bip-tools CLI provides two main commands for address generation: bip32 and bip44. Each command supports customizable options for coin type, chain type, and address format (for Bitcoin Cash). ### BIP32 Address Generation +Generate BIP32 Addresses: ```bash -bip-tools bip32 +cargo run bip32 [OPTIONS] ``` Example: ```bash -cargo run bip32 "xpub6CUGRUo..." 5 +cargo run bip32 "xpub6CUGRUo..." 5 bitcoin 0 ``` ### BIP44 Address Generation ```bash -bip-tools bip44 +cargo run bip44 [OPTIONS] <0|1> ``` Example: ```bash -cargo run bip44 "xpub6CUGRUo..." 5 +cargo run bip44 "xpub6CUGRUo..." 5 bitcoin 0 +``` + +Example 2: +```bash +cargo run bip44 "xpub6BtoTpW..." 3 bitcoincash --format cashaddr 0 +``` +(Example 2) Output: +```bash +Generating 3 BIP-44 addresses for: bitcoincash with chain type 0 +Child 0: qzmmuhsacaa... +Child 1: qpmypx075hz... +Child 2: qram84egkfm... ``` ### CLI Options - ``: Your extended public key in Base58 format - ``: Number of addresses to generate +- ``: Specifies which cryptocurrency to generate addresses for (bitcoin, litecoin, dogecoin, bitcoincash). +- `<0|1>`: Determines address type - Constant 0 is used for external chain and constant 1 for internal chain (also known as change addresses). +- `--format`: Selects address format for Bitcoin Cash (legacy for old-style, cashaddr for new format, cashaddr-p for new format with prefix). - `--help`: Display help information - `--version`: Display version information +## Using Docker + +You can run bip-tools using Docker without installing Rust or any dependencies locally. + +### Building the Docker image + +```bash +docker build -t bip-tools . +``` + +#### BIP32 Address Generation (for Docker) + +Generate BIP32 Addresses: +```bash +docker run bip32 [OPTIONS] +``` + +Example: +```bash +docker run bip32 "xpub6CUGRUo..." 5 bitcoin 0 +``` + +#### BIP44 Address Generation (for Docker) + +```bash +docker run bip44 [OPTIONS] <0|1> +``` + +Example: +```bash +docker run bip44 "xpub6CUGRUo..." 5 bitcoin 0 +``` + +Example 2: +```bash +docker run bip44 "xpub6BtoTpW..." 3 bitcoincash --format cashaddr 0 +``` +(Example 2) Output: +```bash +Generating 3 BIP-44 addresses for: bitcoincash with chain type 0 +Child 0: qzmmuhsacaa... +Child 1: qpmypx075hz... +Child 2: qram84egkfm... +``` + ## Technical Details ### Implementation Notes @@ -136,7 +216,8 @@ biptools/ │ └── SECURITY.md # Security policies and vulnerability reporting ├── src/ │ ├── lib.rs # Core library implementation (Xpub struct and functionality) -│ └── main.rs # CLI implementation +│ ├── main.rs # CLI implementation +| ├── utils.rs # Handles Bitcoin Cash address formatting and conversions. ├── tests/ │ ├── bip32_vectors.rs # Test vectors and validation tests for BIP32 standard │ └── bip44_vectors.rs # Test vectors and validation tests for BIP44 standard diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index f4f0043..a52b37f 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -17,7 +17,7 @@ We use GitHub to host code, to track issues and feature requests, as well as acc 1. Fork the repository and create your branch from `main`: ```bash # Clone your fork - git clone https://github.com/yigitraphy/bip-tools.git + git clone https://github.com/blockchain-labs-inc/bip-tools.git # Navigate to the newly cloned directory cd bip-tools diff --git a/src/lib.rs b/src/lib.rs index f0651d3..b7fc276 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,18 @@ -use base58::{FromBase58, ToBase58}; use ripemd::Ripemd160; use secp256k1::PublicKey; use sha2::{Digest, Sha256}; +use std::convert::TryInto; +pub mod utils; +use crate::utils::{AddressFormat, CashAddress}; + +// Coin type following BIP44 specification +#[derive(Clone, Copy, Debug)] +pub enum CoinType { + Bitcoin = 0, + Litecoin = 2, + Dogecoin = 3, + BitcoinCash = 145, +} #[derive(Clone)] /// Represents an extended public key (xpub) following the BIP32 specification @@ -12,6 +23,7 @@ pub struct Xpub { pub child_number: u32, // Index of this key pub chain_code: [u8; 32], // Chain code (32 bytes) pub public_key: PublicKey, // Compressed public key (33 bytes) + pub coin_type: CoinType, // Coin type for address generation } impl Xpub { @@ -22,6 +34,7 @@ impl Xpub { child_number: u32, chain_code: [u8; 32], public_key: PublicKey, + coin_type: CoinType, ) -> Self { Self { depth, @@ -29,26 +42,31 @@ impl Xpub { child_number, chain_code, public_key, + coin_type, } } /// Converts a Base58 encoded xpub string into an Xpub instance. - pub fn from_base58(xpub: &str) -> Result { - // Decode the xpub from Base58 - let decoded = xpub - .from_base58() + pub fn from_base58(xpub: &str, coin_type: CoinType) -> Result { + let decoded = bs58::decode(xpub) + .into_vec() .map_err(|e| format!("Base58 decode error: {:?}", e))?; - if decoded.len() != 82 { return Err("Invalid xpub length".to_string()); } + let expected_checksum = &decoded[78..82]; + let actual_checksum = &Sha256::digest(Sha256::digest(&decoded[..78]))[..4]; + if expected_checksum != actual_checksum { + return Err("Invalid checksum".to_string()); + } + // Extract components from the decoded xpub // bytes [0..4]: version bytes (not stored) // bytes [4]: depth // bytes [5..9]: parent fingerprint // bytes [9..13]: child number - // bytes [13..45]: chain cod + // bytes [13..45]: chain code // bytes [45..78]: public key let depth = decoded[4]; let parent_fingerprint = u32::from_be_bytes(decoded[5..9].try_into().unwrap()); @@ -63,6 +81,7 @@ impl Xpub { child_number, chain_code, public_key, + coin_type, )) } @@ -70,11 +89,15 @@ impl Xpub { pub fn to_base58(&self) -> String { let mut serialized = [0u8; 78]; - // Version bytes (4 bytes) - serialized[0] = 0x04; - serialized[1] = 0x88; - serialized[2] = 0xB2; - serialized[3] = 0x1E; + // Version bytes (4 bytes) based on coin type + let version_bytes = match self.coin_type { + CoinType::Bitcoin => [0x04, 0x88, 0xB2, 0x1E], + CoinType::Litecoin => [0x01, 0x9D, 0x9C, 0xFE], + CoinType::Dogecoin => [0x02, 0xFA, 0x92, 0x8C], + CoinType::BitcoinCash => [0x04, 0x88, 0xB2, 0x1E], + }; + + serialized[0..4].copy_from_slice(&version_bytes); // Depth (1 byte) serialized[4] = self.depth; @@ -84,6 +107,7 @@ impl Xpub { // Child number (4 bytes) serialized[9..13].copy_from_slice(&self.child_number.to_be_bytes()); + // Chain code (32 bytes) serialized[13..45].copy_from_slice(&self.chain_code); @@ -96,7 +120,7 @@ impl Xpub { final_data[..78].copy_from_slice(&serialized); final_data[78..82].copy_from_slice(&checksum[..4]); - final_data.to_base58() + bs58::encode(final_data).into_string() } /// Generates a legacy P2PKH (Pay to Public Key Hash) Bitcoin address from the public key @@ -104,21 +128,32 @@ impl Xpub { /// 2. Adds version byte (0x00 for mainnet) /// 3. Adds double SHA256 checksum /// 4. Encodes in Base58Check format - pub fn to_bitcoin_address(&self) -> String { - let mut hasher = Sha256::new(); - hasher.update(self.public_key.serialize()); - let sha256 = hasher.finalize(); - - let pubkey_hash = Ripemd160::digest(sha256); - - let mut address_bytes = [0u8; 25]; - address_bytes[0] = 0x00; - address_bytes[1..21].copy_from_slice(&pubkey_hash); - - let checksum = &Sha256::digest(Sha256::digest(&address_bytes[..21]))[..4]; - address_bytes[21..].copy_from_slice(checksum); - - address_bytes.to_base58() + pub fn to_address(&self, format: &Option) -> String { + let hash160 = { + let mut hasher = Sha256::new(); + hasher.update(self.public_key.serialize()); + Ripemd160::digest(hasher.finalize()) + }; + + match self.coin_type { + CoinType::BitcoinCash => { + let format = format.as_ref().unwrap_or(&AddressFormat::CashAddr); + CashAddress::from_pubkey(&self.public_key.serialize(), format) + } + _ => { + let mut address_bytes = [0u8; 25]; + address_bytes[0] = match self.coin_type { + CoinType::Bitcoin => 0x00, + CoinType::Litecoin => 0x30, + CoinType::Dogecoin => 0x1E, + CoinType::BitcoinCash => unreachable!(), + }; + address_bytes[1..21].copy_from_slice(&hash160); + let checksum = Sha256::digest(Sha256::digest(&address_bytes[..21])); + address_bytes[21..25].copy_from_slice(&checksum[..4]); + bs58::encode(address_bytes).into_string() + } + } } /// Derives a non-hardened child Xpub from the current Xpub @@ -165,19 +200,23 @@ impl Xpub { child_number: index, chain_code, public_key: child_pubkey, + coin_type: self.coin_type, }) } /// Generates multiple Bitcoin addresses using BIP32 derivation path - pub fn derive_bip32_addresses(&self, count: u32) -> Result, String> { + pub fn derive_bip32_addresses( + &self, + count: u32, + format: &Option, + ) -> Result, String> { let mut addresses = Vec::with_capacity(count as usize); - let current = self.clone(); // Generate sequential addresses for i in 0..count { - match current.derive_non_hardened(i) { + match self.derive_non_hardened(i) { Ok(child) => { - addresses.push(child.to_bitcoin_address()); + addresses.push(child.to_address(format)); } Err(e) => { return Err(format!("Error deriving child {}: {}", i, e)); @@ -190,25 +229,35 @@ impl Xpub { /// Generates multiple Bitcoin addresses using BIP44 derivation path /// Follows m/44'/0'/0'/0/i path structure - pub fn derive_bip44_addresses(&self, count: u32) -> Result, String> { + pub fn derive_bip44_addresses( + &self, + count: u32, + chain_type: u32, + format: &Option, + ) -> Result, String> { let mut addresses = Vec::with_capacity(count as usize); - //BIP44 path: m/44'/0'/0'/0/i - let account = self - .derive_non_hardened(0) - .map_err(|e| format!("Error deriving account: {}", e))?; + // Validate chain type: must be 0 (external) or 1 (change) + if chain_type > 1 { + return Err(format!( + "Invalid chain type: {}. Use 0 for external or 1 for change", + chain_type + )); + } + + // Derive chain type directly from the xpub (e.g., m/44'/0'/0'/0 or m/44'/0'/0'/1) + let chain = self + .derive_non_hardened(chain_type) + .map_err(|e| format!("Error deriving chain: {}", e))?; - // Generate addresses at m/44'/0'/0'/0/i + // Generate sequential addresses for i in 0..count { - match account.derive_non_hardened(i) { - Ok(child) => { - addresses.push(child.to_bitcoin_address()); - } - Err(e) => { - return Err(format!("Error deriving child {}: {}", i, e)); - } - } + let child = chain + .derive_non_hardened(i) + .map_err(|e| format!("Error deriving child {}: {}", i, e))?; + addresses.push(child.to_address(format)); } + Ok(addresses) } @@ -217,7 +266,6 @@ impl Xpub { pub fn fingerprint(&self) -> u32 { let hash = Sha256::digest(self.public_key.serialize()); let hash160 = Ripemd160::digest(hash); - u32::from_be_bytes(hash160[0..4].try_into().unwrap()) } } diff --git a/src/main.rs b/src/main.rs index 275f005..37f8896 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,9 @@ -use bip_tools::Xpub; +use bip_tools::{utils, CoinType, Xpub}; use clap::{Args, Parser, Subcommand}; #[derive(Debug, Parser)] #[command( - name = "bip_tools", // Name of the CLI application + name = "bip_tools", // Name of the CLI application arg_required_else_help(true), // Show help if no arguments provided version, // Enables automatic version flag about, // Short description from Cargo.toml @@ -18,17 +18,46 @@ struct Cli { #[derive(Debug, Subcommand)] enum Commands { /// Generate public addresses from a BIP32 extended public key - Bip32(AddressGeneratorArgs), + Bip32(Bip32Args), /// Generate public addresses from a BIP44 extended public key - Bip44(AddressGeneratorArgs), + Bip44(Bip44Args), } -/// Common arguments for both BIP32 adn BIP44 address generation +/// Arguments for generating BIP32 addresses #[derive(Debug, Args)] -#[command(flatten_help = true)] -struct AddressGeneratorArgs { +struct Bip32Args { + /// Extended public key (xpub) in Base58 format extended_public_key: String, + + /// Number of addresses to generate count: u32, + + /// Coin type (e.g., bitcoin, litecoin, dogecoin, bitcoincash) + coin_type: String, + + /// Address format (optional, only used for Bitcoin Cash) + #[arg(short, long)] + format: Option, +} + +/// Arguments for BIP44 address generation with chain type +#[derive(Debug, Args)] +struct Bip44Args { + /// Extended public key (xpub) in Base58 format + extended_public_key: String, + + /// Number of addresses to generate + count: u32, + + /// Coin type (e.g., bitcoin, litecoin, dogecoin, bitcoincash) + coin_type: String, + + /// Chain type: 0 for external chain (normal), 1 for change chain (receiving) + chain_type: u32, + + /// Address format (optional, only used for Bitcoin Cash) + #[arg(short, long)] + format: Option, } /// Main entry point of the application @@ -48,10 +77,31 @@ fn main() -> Result<(), Box> { // Match on the subcommand and execute corresponding functionality match cli.commands { Commands::Bip32(args) => { - let xpub = Xpub::from_base58(&args.extended_public_key)?; - println!("Generating {} BIP-32 addresses: ", args.count); + let coin_type = match args.coin_type.to_lowercase().as_str() { + "bitcoin" => CoinType::Bitcoin, + "litecoin" => CoinType::Litecoin, + "dogecoin" => CoinType::Dogecoin, + "bitcoincash" => CoinType::BitcoinCash, + _ => { + eprintln!("Unsupported coin type: {}", args.coin_type); + return Err("Unsupported coin type".into()); + } + }; - match xpub.derive_bip32_addresses(args.count) { + let xpub = Xpub::from_base58(&args.extended_public_key, coin_type)?; + println!( + "Generating {} BIP-32 addresses for: {}", + args.count, args.coin_type + ); + + let format = match args.format.as_deref() { + Some("legacy") => Some(utils::AddressFormat::Legacy), + Some("cashaddr") => Some(utils::AddressFormat::CashAddr), + Some("cashaddr-p") => Some(utils::AddressFormat::CashAddrWithPrefix), + _ => None, + }; + + match xpub.derive_bip32_addresses(args.count, &format) { Ok(addresses) => { // Print each derived address with its index for (i, address) in addresses.iter().enumerate() { @@ -64,10 +114,28 @@ fn main() -> Result<(), Box> { } } Commands::Bip44(args) => { - let xpub = Xpub::from_base58(&args.extended_public_key)?; - println!("Generating {} BIP44 addresses:", args.count); + let coin_type = match args.coin_type.to_lowercase().as_str() { + "bitcoin" => CoinType::Bitcoin, + "litecoin" => CoinType::Litecoin, + "dogecoin" => CoinType::Dogecoin, + "bitcoincash" => CoinType::BitcoinCash, + _ => return Err("Unsupported coin type".into()), + }; + + let xpub = Xpub::from_base58(&args.extended_public_key, coin_type)?; + println!( + "Generating {} BIP-44 addresses for: {} with chain type {}", + args.count, args.coin_type, args.chain_type + ); + + let format = match args.format.as_deref() { + Some("legacy") => Some(utils::AddressFormat::Legacy), + Some("cashaddr") => Some(utils::AddressFormat::CashAddr), + Some("cashaddr-p") => Some(utils::AddressFormat::CashAddrWithPrefix), + _ => None, + }; - match xpub.derive_bip44_addresses(args.count) { + match xpub.derive_bip44_addresses(args.count, args.chain_type, &format) { Ok(addresses) => { // Print each derived address with its index for (i, address) in addresses.iter().enumerate() { diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..b59ad00 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,143 @@ +use ripemd::Ripemd160; +use sha2::{Digest, Sha256}; + +// CashAddr spesific constants +const CASHADDR_CHARSET: &str = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; +const CASHADDR_PREFIX: &str = "bitcoincash"; + +// Bitcoin Cash address format +pub enum AddressFormat { + Legacy, // Base58 format + CashAddr, // CashAddr (not prefix) + CashAddrWithPrefix, // CashAddr (with prefix) +} + +pub struct CashAddress; + +impl CashAddress { + /// Create an address from a public key in the spesific format + pub fn from_pubkey(pubkey: &[u8], format: &AddressFormat) -> String { + // Hash the public key using SHA256 and RIPEMD160 + let hash = Ripemd160::digest(Sha256::digest(pubkey)); + + // Format the address based on the requested format + match format { + AddressFormat::Legacy => Self::legacy_address(&hash), + AddressFormat::CashAddr => Self::cashaddr(&hash, false), + AddressFormat::CashAddrWithPrefix => Self::cashaddr(&hash, true), + } + } + + /// Legacy Base58 format + fn legacy_address(hash: &[u8]) -> String { + let mut address_byte = vec![0x00]; // P2PKH version + address_byte.extend_from_slice(hash); + let checksum = Sha256::digest(Sha256::digest(&address_byte)); + address_byte.extend_from_slice(&checksum[..4]); + bs58::encode(address_byte).into_string() + } + + // CashAddr Format + fn cashaddr(hash: &[u8], with_prefix: bool) -> String { + let payload = Self::build_payload(hash); + let checksum = Self::compute_checksum(&payload); + let encoded = Self::encode_payload(&payload, &checksum); + if with_prefix { + format!("bitcoincash:{}", encoded) + } else { + encoded + } + } + + /// Helper Functions + fn build_payload(hash: &[u8]) -> Vec { + let mut payload = vec![0x00]; + payload.extend_from_slice(hash); + Self::convert_bits(&payload, 8, 5, true).expect("Failed to convert bits") + } + + fn encode_payload(payload: &[u8], checksum: &[u8]) -> String { + let full_encoded: Vec = payload.iter().chain(checksum.iter()).cloned().collect(); + + full_encoded + .iter() + .map(|&b| CASHADDR_CHARSET.as_bytes()[b as usize] as char) + .collect::() + } + + fn hrp_expand(hrp: &str) -> Vec { + let mut ret = Vec::with_capacity(hrp.len() * 2 + 1); + for b in hrp.bytes() { + ret.push(b & 0x1F); + } + ret.push(0); // Separator + ret + } + + fn compute_checksum(payload: &[u8]) -> Vec { + let mut data = Vec::new(); + data.extend(Self::hrp_expand(CASHADDR_PREFIX)); + data.extend_from_slice(payload); + data.extend(vec![0u8; 8]); // Checksum placeholder + + let poly = Self::poly_mod(&data); + (0..8) + .map(|i| ((poly >> (5 * (7 - i))) & 0x1F) as u8) + .collect() + } + + fn poly_mod(data: &[u8]) -> u64 { + let mut c = 1u64; + for &d in data { + let c0 = (c >> 35) as u8; + c = ((c & 0x07ffffffff) << 5) ^ u64::from(d); + + if c0 & 0x01 != 0 { + c ^= 0x98f2bc8e61; + } + if c0 & 0x02 != 0 { + c ^= 0x79b76d99e2; + } + if c0 & 0x04 != 0 { + c ^= 0xf33e5fb3c4; + } + if c0 & 0x08 != 0 { + c ^= 0xae2eabe2a8; + } + if c0 & 0x10 != 0 { + c ^= 0x1e4f43e470; + } + } + c ^ 1 + } + + fn convert_bits(data: &[u8], from: u32, to: u32, pad: bool) -> Result, String> { + if from >= 32 || to >= 32 { + return Err("Invalid bit size: from and to must be less than 32".to_string()); + } + + let mut acc: u64 = 0; + let mut bits: u32 = 0; + let mut result = Vec::new(); + let maxv = (1 << to) - 1; + + for &value in data { + if (value as u32) >= (1 << from) { + return Err(format!("Invalid value {}", value)); + } + acc = (acc << from) | (value as u64); + bits += from; + + while bits >= to { + bits -= to; + result.push(((acc >> bits) & maxv) as u8); + } + } + + if pad && bits > 0 { + result.push(((acc << (to - bits)) & maxv) as u8); + } + + Ok(result) + } +} diff --git a/tests/bip32_vectors.rs b/tests/bip32_vectors.rs index 5f9adc6..c2fa885 100644 --- a/tests/bip32_vectors.rs +++ b/tests/bip32_vectors.rs @@ -1,333 +1,658 @@ -use bip_tools::Xpub; - #[cfg(test)] -mod bip32_tests { - use crate::Xpub; - use base58::FromBase58; - use secp256k1::PublicKey; - use sha2::{Digest, Sha256}; - - // Test data for mainnet BIP32 derivation - const TEST_XPUB: &str = "xpub681vrYy1g8k1xtcNPi2WN9pGiHDejoCvUT4GG2Mbs9rcs98VWvoQXmgT2J1umYQs9p2qp6xdMjJ2AU1rNcCMq9RmtKNhowJKYvVgKwS59xX"; - - // Known valid addresses for test validation - const EXPECTED_BIP32_ADDRESSES: [&str; 3] = [ - "1FvSF5syVSTnsbNzFpPd4mNFcSvwtTxqLw", - "175AcMJAwppLkCKAGkazkM9ygTbvPc5Cn5", - "1GZZ8iuVo1BwfGei132MTRmNoBRDt7Lfvf", - ]; - - // Version bytes for mainnet xpub - const MAINNET_VERSION: [u8; 4] = [0x04, 0x88, 0xB2, 0x1E]; - - // Helper function to get test public key - fn get_test_public_key() -> PublicKey { - let decoded = TEST_XPUB.from_base58().unwrap(); - PublicKey::from_slice(&decoded[45..78]).unwrap() - } +mod tests { + /// Bitcoin (BTC) BIP32 Test Module + mod bitcoin { + use bip_tools::{CoinType, Xpub}; + + // Constants + const COIN_TYPE: CoinType = CoinType::Bitcoin; + const XPUB_BTC_BIP32: &str = "xpub6Dix4qijz1p9XB7eiuYe5anj3qiveYg4UQvqhJcJbMraGEQegMhbt3BcLd5fnmgp6eWRGtjiWcdkck749k5KgYHXH8UY9MDRwDye43ok3Hr"; + const BIP32_EXPECTED_ADDRESS_BTC: [&str; 3] = [ + "1Ea7axUseGWah1Y7Mxetmz9P6nRrJVFAA4", + "1gnuicPb9Jbg8EQamG72ZK3dDyCmjNxZV", + "15Jz4V68onxWmdRdC2ZR8KDfghY1np1E9w", + ]; + + /// Test generating a multiple BIP32 address + #[test] + fn test_bip32_btc_multiple_addresses() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP32, COIN_TYPE).unwrap(); + let addresses = xpub + .derive_bip32_addresses(3, &None) + .expect("BIP32 multiple address derivation failed"); + assert_eq!(addresses.len(), 3, "Should generate 3 address"); + assert_eq!( + addresses[0], BIP32_EXPECTED_ADDRESS_BTC[0], + "Multiple BIP32 addresses do not match expected" + ); + } - // Basic Structure Tests - - #[test] - fn test_bip32_xpub_new() { - let depth: u8 = 0; - let parent_fingerprint: u32 = 0; - let child_number: u32 = 0; - let chain_code: [u8; 32] = [0; 32]; - let public_key = get_test_public_key(); - - let xpub = Xpub::new( - depth, - parent_fingerprint, - child_number, - chain_code, - public_key, - ); - - assert_eq!(xpub.depth, depth, "Depth should match"); - assert_eq!( - xpub.parent_fingerprint, parent_fingerprint, - "Parent fingerprint should match" - ); - assert_eq!(xpub.child_number, child_number, "Child number should match"); - assert_eq!(xpub.chain_code, chain_code, "Chain code should match"); - assert_eq!(xpub.public_key, public_key, "Public key should match"); - } + /// Ensure BIP32 derivation for BTC is deterministic (same input yields same output) + #[test] + fn test_bip32_btc_derivation_consistency() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP32, COIN_TYPE).unwrap(); + let addresses1 = xpub.derive_bip32_addresses(1, &None).unwrap(); + let addresses2 = xpub.derive_bip32_addresses(1, &None).unwrap(); + assert_eq!( + addresses1, addresses2, + "BIP32 addresses should be consistent across derivations" + ); + } - #[test] - fn test_bip32_xpub_from_base58() { - // Test valid and invalid xpub parsing - let xpub = Xpub::from_base58(TEST_XPUB); - assert!(xpub.is_ok(), "Failed to parse valid xpub"); + /// Test BIP32 derivation for BTC over a large index range + #[test] + fn test_bip32_btc_large_index_range() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP32, COIN_TYPE).unwrap(); + let count = 1000; + let addresses = xpub + .derive_bip32_addresses(count, &None) + .expect("BIP32 large index derivation failed"); + assert_eq!( + addresses.len(), + count as usize, + "Should generate 1000 addresses" + ); + } - // Additional checks on parsed xpub - let parsed = xpub.unwrap(); - let decoded = TEST_XPUB.from_base58().unwrap(); - assert_eq!(decoded[4], parsed.depth, "Depth should match"); - } + /// Test BIP44 Bitcoin xpub parsing with a short invalid xpub and checks if an error is returned + #[test] + fn test_bip32_btc_short_invalid_xpub() { + let invalid_xpub = "xpub123"; + let result = Xpub::from_base58(invalid_xpub, CoinType::Bitcoin); + assert!(result.is_err(), "Short xpub fail for BIP32 Bitcoin"); + } - #[test] - fn test_bip32_xpub_invalid_base58() { - // Test invalid Base58 characters - let result = Xpub::from_base58("invalid!base58@string"); - assert!(result.is_err()); - assert!(result - .as_ref() - .err() - .unwrap() - .contains("Base58 decode error")); - - // Test invalid lenght - let result = Xpub::from_base58("1aaaaaaaa"); - assert!(result.is_err()); - assert!(result - .as_ref() - .err() - .unwrap() - .contains("Invalid xpub length")); - - // Test invalid public key - let invalid_xpub = "xpub6CUGRUonZSQ4zHWHPYWmGLs3ySaVP7envEXHHYQFDvD85JQBY6kw5VexFge6qcCYwQFhbgFLRqCzq3JHcthYMSLf1r3kzjqFiGN1ZNDSqLv"; - let result = Xpub::from_base58(&invalid_xpub); - assert!(result.is_err()); - assert!(result - .as_ref() - .err() - .unwrap() - .contains("Invalid public key")); - } + /// Test BIP32 Bitcoin address format to ensure it start with '1' and has correct lenght + #[test] + fn test_bip32_btc_address_format() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP32, CoinType::Bitcoin).unwrap(); + let addresses = xpub.derive_bip32_addresses(3, &None).unwrap(); + for addr in addresses { + assert!( + addr.starts_with("1"), + "BIP32 Bitcoin address should start with '1'" + ); + assert!( + addr.len() >= 26 && addr.len() <= 35, + "BIP32 Bitcoin address lenght should be 26-35" + ); + } + } - // Format and Structural Checks + /// Verify address derivation at index 0 to ensure edge case is handled correctly. + #[test] + fn test_bip32_btc_index_zero() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP32, COIN_TYPE).unwrap(); + let address = xpub.derive_bip32_addresses(1, &None).unwrap(); + assert_eq!( + address[0], BIP32_EXPECTED_ADDRESS_BTC[0], + "BIP32 address at index 0 does not match expected" + ); + } - #[test] - fn test_bip32_xpub_version_bytes() { - // Validate mainnet version bytes - let decoded = TEST_XPUB.from_base58().unwrap(); - assert_eq!(decoded[0..4], MAINNET_VERSION, "Version bytes should match"); - } + /// Ensure hardened index derivation with xPub fails as per BIP32 rules. + #[test] + fn test_bip32_btc_hardened_index() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP32, COIN_TYPE).unwrap(); + let result = xpub.derive_non_hardened(0x80000000); + assert!(result.is_err(), "Hardened index derivation should fail"); + } - #[test] - fn test_bip32_xpub_checksum() { - // Verify checksum calculation is correct - let decoded = TEST_XPUB.from_base58().unwrap(); - let main_data = &decoded[0..decoded.len() - 4]; - let provided_checksum = &decoded[decoded.len() - 4..]; + /// Derive a child at the maximum non-hardened index (2^31 - 1) and verify the result for Bitcoin (BTC) + #[test] + fn test_bip32_btc_max_index() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP32, COIN_TYPE).unwrap(); + let result = xpub.derive_non_hardened(0x7FFFFFFF); + assert!(result.is_ok(), "Max non-hardened index should succeed"); + } - let calculated_checksum = &Sha256::digest(&Sha256::digest(main_data))[..4]; - assert_eq!(calculated_checksum, provided_checksum, "Invalid checksum"); - } + /// This test checks the depth, index, and parent fingerprint of a derived child from an xpub according to BIP32 (for BTC) + #[test] + fn test_bip32_btc_depth_and_fingerprint() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP32, COIN_TYPE).unwrap(); + let child = xpub.derive_non_hardened(0).unwrap(); + assert_eq!(child.depth, xpub.depth + 1, "Depth should increase by 1"); + assert_eq!(child.child_number, 0, "Child number should match index"); + assert_eq!( + child.parent_fingerprint, + xpub.fingerprint(), + "Parent fingerprint mismatch" + ); + } - #[test] - fn test_bip32_to_base58() { - let decoded_test = TEST_XPUB.from_base58().unwrap(); - println!( - "Original version bytes: {:02x} {:02x} {:02x} {:02x}", - decoded_test[0], decoded_test[1], decoded_test[2], decoded_test[3] - ); - println!("Original all bytes: {:?}", decoded_test); - - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); - let encoded = xpub.to_base58(); - let decoded = encoded.from_base58().unwrap(); - - println!( - "Encoded version bytes: {:02x} {:02x} {:02x} {:02x}", - decoded[0], decoded[1], decoded[2], decoded[3] - ); - println!("Encoded all bytes: {:?}", decoded); - - assert_eq!(encoded, TEST_XPUB, "Base58 encoding should match original"); - - assert_eq!(decoded[4], xpub.depth, "Depth should match"); - - assert_eq!(decoded[0], 0x04, "Version bytes 1 should match"); - assert_eq!(decoded[1], 0x88, "Version bytes 2 should match"); - assert_eq!(decoded[2], 0xB2, "Version bytes 3 should match"); - assert_eq!(decoded[3], 0x1E, "Version bytes 4 should match"); - } + // Tests that serializing and then deserializing a BIP32 xpub preserves its data + #[test] + fn test_bip32_btc_serialization_round_trip() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP32, COIN_TYPE).unwrap(); + let serialized = xpub.to_base58(); + let deserialized = Xpub::from_base58(&serialized, COIN_TYPE).unwrap(); + assert_eq!( + xpub.to_base58(), + deserialized.to_base58(), + "Serialization round-trip failed" + ); + } - #[test] - fn test_bip32_bitcoin_address_format() { - // Verify P2PKH address format and length - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); - let address = xpub.to_bitcoin_address(); - - // Format checks - assert!(address.starts_with('1'), "Should be P2PKH address"); - assert!( - address.len() >= 26 && address.len() <= 35, - "Address length should be valid" - ); - - // Decode and verify version byte - let decoded = address.from_base58().unwrap(); - assert_eq!(decoded[0], 0x00, "Should use P2PKH version byte"); + // Tests that derived BIP32 public keys are in compressed (33-byte) format + #[test] + fn test_bip32_btc_public_key_compression() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP32, COIN_TYPE).unwrap(); + let child = xpub.derive_non_hardened(0).unwrap(); + assert_eq!( + child.public_key.serialize().len(), + 33, + "Public key should be compressed" + ); + } } - #[test] - fn test_bip32_fingerprint_generation() { - // Check fingerprint calculation is non-zero - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); - let fingerprint = xpub.fingerprint(); - - // Test non-zero - assert!(fingerprint > 0, "Fingerprint should be not zero"); - - // Test deterministic - let second_fingerprint = xpub.fingerprint(); - assert_eq!( - fingerprint, second_fingerprint, - "Fingerprint should be deterministic" - ); - } + /// Litecoin (LTC) BIP32 Test Module + mod litecoin_bip32 { + use bip_tools::{CoinType, Xpub}; + + // Constants + const COIN_TYPE: CoinType = CoinType::Litecoin; + const XPUB_LTC_BIP32: &str = "Ltub2aDBHxW1JQKsKckPrXniDLiu8TG8HRsPMJJzTPbXgNwZVV4ccXoHqXTFxSHMtED518MZP3ukjaoyC71MivCHg3qj2NQAzfP3PcMnx1HxezW"; + const BIP32_EXPECTED_ADDRESS_LTC: [&str; 3] = [ + "LPs2CLDRwQuG6NTaYcqLFCAHseKcpred9m", + "LZrrce6ZWkfFWKreefxdX862eyuagabgF8", + "LNwSvqc7uudTKt4Gz8VevVJNJ7hGboxADY", + ]; + + /// Test generating a multiple BIP32 address + #[test] + fn test_bip32_ltc_multiple_addresses() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP32, COIN_TYPE).unwrap(); + let addresses = xpub + .derive_bip32_addresses(3, &None) + .expect("BIP32 multiple address derivation failed"); + assert_eq!(addresses.len(), 3, "Should generate 3 address"); + assert_eq!( + addresses[0], BIP32_EXPECTED_ADDRESS_LTC[0], + "Multiple BIP32 addresses do not match expected" + ); + } + + /// Ensure BIP32 derivation for LTC is deterministic (same input yields same output) + #[test] + fn test_bip32_ltc_derivation_consistency() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP32, COIN_TYPE).unwrap(); + let addresses1 = xpub.derive_bip32_addresses(1, &None).unwrap(); + let addresses2 = xpub.derive_bip32_addresses(1, &None).unwrap(); + assert_eq!( + addresses1, addresses2, + "BIP32 addresses should be consistent across derivations" + ); + } - // Address Derivation Tests + /// Test BIP32 derivation for LTC over a large index range + #[test] + fn test_bip32_ltc_large_index_range() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP32, COIN_TYPE).unwrap(); + let count = 1000; + let addresses = xpub + .derive_bip32_addresses(count, &None) + .expect("BIP32 large index derivation failed"); + assert_eq!( + addresses.len(), + count as usize, + "Should generate 1000 addresses" + ); + } - #[test] - fn test_bip32_address_derivation() { - // Verify address derivation matches expected test vectors - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); - let derived_addresses = xpub.derive_bip32_addresses(3).unwrap(); + /// Test BIP32 Litecoin xpub parsing with a short invalid xpub and checks if an error is returned + #[test] + fn test_bip32_ltc_short_invalid_xpub() { + let invalid_xpub = "xpub123"; + let result = Xpub::from_base58(invalid_xpub, CoinType::Litecoin); + assert!(result.is_err(), "Short xpub fail for BIP32 Litecoin"); + } - for (i, addr) in derived_addresses.iter().enumerate() { + /// Test BIP32 Litecoin address format to ensure it start with 'L' and has correct lenght + #[test] + fn test_bip32_ltc_address_format() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP32, CoinType::Litecoin).unwrap(); + let addresses = xpub.derive_bip32_addresses(3, &None).unwrap(); + for addr in addresses { + assert!( + addr.starts_with("L"), + "BIP32 Litecoin address should start with 'L'" + ); + assert!( + addr.len() >= 26 && addr.len() <= 35, + "BIP32 Litecoin address lenght should be 26-35" + ); + } + } + + /// Verify address derivation at index 0 to ensure edge case is handled correctly. + #[test] + fn test_bip32_ltc_index_zero() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP32, COIN_TYPE).unwrap(); + let address = xpub.derive_bip32_addresses(1, &None).unwrap(); assert_eq!( - addr, EXPECTED_BIP32_ADDRESSES[i], - "Address {} mismatch. Expected: {}, Got: {}", - i, EXPECTED_BIP32_ADDRESSES[i], addr + address[0], BIP32_EXPECTED_ADDRESS_LTC[0], + "BIP32 address at index 0 does not match expected" ); } - } - #[test] - fn test_bip32_zero_test_address_derivation() { - // Test handling of zero address request - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); - let result = xpub.derive_bip32_addresses(0); - assert!(result.is_ok(), "Should handle zero address request"); - assert_eq!( - result.unwrap().len(), - 0, - "Should return empty vector for zero count" - ); - } + /// Ensure hardened index derivation with xPub fails as per BIP32 rules. + #[test] + fn test_bip32_ltc_hardened_index() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP32, COIN_TYPE).unwrap(); + let result = xpub.derive_non_hardened(0x80000000); + assert!(result.is_err(), "Hardened index derivation should fail"); + } - #[test] - fn test_bip32_invalid_derivation_index() { - // Verify hardened derivation is rejected for xpub - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); - // Test hardened index - assert!( - xpub.derive_non_hardened(0x80000000).is_err(), - "Should fail with hardened index" - ); - - // Test maximum allowed index - assert!( - xpub.derive_non_hardened(0x7FFFFFFF).is_ok(), - "Should accept max non-hardened index" - ); - } + /// Derive a child at the maximum non-hardened index (2^31 - 1) and verify the result for Litecoin (LTC) + #[test] + fn test_bip32_ltc_max_index() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP32, COIN_TYPE).unwrap(); + let result = xpub.derive_non_hardened(0x7FFFFFFF); + assert!(result.is_ok(), "Max non-hardened index should succeed"); + } - // Advanced Derivation Tests - - #[test] - fn test_bip32_address_consistency() { - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); - - // Check consistent derivation with same index - let first_child = xpub.derive_non_hardened(1).unwrap(); - let second_child = xpub.derive_non_hardened(1).unwrap(); - assert_eq!( - first_child.to_bitcoin_address(), - second_child.to_bitcoin_address(), - "Same derivation index should produce identical addresses" - ); - - // Alternative derivation method check - let first_address = xpub.derive_non_hardened(1).unwrap().to_bitcoin_address(); - let second_address = xpub.derive_non_hardened(1).unwrap().to_bitcoin_address(); - assert_eq!( - first_address, second_address, - "Same index should produce same address regardless of derivation method" - ); - - // Verify different indices produce different addresses - let different_address = xpub.derive_non_hardened(2).unwrap().to_bitcoin_address(); - assert_ne!( - first_address, different_address, - "Different index should produce different address" - ); - } + /// This test checks the depth, index, and parent fingerprint of a derived child from an xpub according to BIP32 (for LTC) + #[test] + fn test_bip32_ltc_depth_and_fingerprint() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP32, COIN_TYPE).unwrap(); + let child = xpub.derive_non_hardened(0).unwrap(); + assert_eq!(child.depth, xpub.depth + 1, "Depth should increase by 1"); + assert_eq!(child.child_number, 0, "Child number should match index"); + assert_eq!( + child.parent_fingerprint, + xpub.fingerprint(), + "Parent fingerprint mismatch" + ); + } + + // Tests that serializing and then deserializing a BIP32 xpub preserves its data + #[test] + fn test_bip32_ltc_serialization_round_trip() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP32, COIN_TYPE).unwrap(); + let serialized = xpub.to_base58(); + let deserialized = Xpub::from_base58(&serialized, COIN_TYPE).unwrap(); + assert_eq!( + xpub.to_base58(), + deserialized.to_base58(), + "Serialization round-trip failed" + ); + } - #[test] - fn test_bip32_large_index_derivation() { - // Test derivation with maximum allowed non-hardened index - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); - let large_index = 0x7FFFFFFF; - let result = xpub.derive_non_hardened(large_index); - assert!(result.is_ok(), "Should handle large non-hardened index"); + // Tests that derived BIP32 public keys are in compressed (33-byte) format + #[test] + fn test_bip32_ltc_public_key_compression() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP32, COIN_TYPE).unwrap(); + let child = xpub.derive_non_hardened(0).unwrap(); + assert_eq!( + child.public_key.serialize().len(), + 33, + "Public key should be compressed" + ); + } } - // Multiple Address Tests + mod dogecoin_bip32 { + use bip_tools::{CoinType, Xpub}; + + // Constants + const COIN_TYPE: CoinType = CoinType::Dogecoin; + const XPUB_DOGE_BIP32: &str = "dgub8u3NcC3wtwJZFpsVP9Qg6GoTb6ik3i1BQXxCBbogozJk2jkXkMRwg286arkarfL8b998F1PnvkBRwnN5WR7PZcX1ir5yDrKWAMxfE7d4zjg"; + const BIP32_EXPECTED_ADDRESS_DOGE: [&str; 3] = [ + "DP5Hghi5FngxamwXteyb7kckNimUYrnpCX", + "DCSSfERm2HyRcmHQojPkhqZ9TSqErEctcn", + "D5nVkhrtA1f2VJhtd2BZLdayiC3zZpsVLx", + ]; + + /// Test BIP32 derivation for multiple addresses (for Dogecoin) + #[test] + fn test_bip32_doge_multiple_addresses() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP32, COIN_TYPE).unwrap(); + let addresses = xpub + .derive_bip32_addresses(3, &None) + .expect("BIP32 Multiple addresses derivation failed"); + assert_eq!(addresses.len(), 3, "Should generate 3 addresses"); + for (i, addr) in addresses.iter().enumerate() { + assert_eq!( + addr, BIP32_EXPECTED_ADDRESS_DOGE[i], + "Multiple BIP32 addresses do not match expected" + ); + } + } + + /// Ensure BIP32 derivation for DOGE is deterministic (same input yields same output) + #[test] + fn test_bip32_doge_derivation_consistency() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP32, COIN_TYPE).unwrap(); + let addresses1 = xpub.derive_bip32_addresses(1, &None).unwrap(); + let addresses2 = xpub.derive_bip32_addresses(1, &None).unwrap(); + assert_eq!( + addresses1, addresses2, + "BIP32 addresses should be consistent across derivations" + ); + } - #[test] - fn test_bip32_multiple_addresses_uniqueness() { - // Verify all derived addresses are unique - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); - let count = 100; - let addresses = xpub.derive_bip32_addresses(count).unwrap(); + /// Test BIP32 derivation for DOGE over a large index range + #[test] + fn test_bip32_doge_large_index_range() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP32, COIN_TYPE).unwrap(); + let count = 1000; + let addresses = xpub + .derive_bip32_addresses(count, &None) + .expect("BIP32 large index derivation failed"); + assert_eq!( + addresses.len(), + count as usize, + "Should generate 1000 addresses" + ); + } - println!("Testing uniqueness for {} addresses", count); + /// Test BIP32 Dogecoin xpub parsing with a short invalid xpub and checks if an error is returned + #[test] + fn test_bip32_doge_short_invalid_xpub() { + let invalid_xpub = "xpub123"; + let result = Xpub::from_base58(invalid_xpub, CoinType::Dogecoin); + assert!(result.is_err(), "Short xpub fail for BIP32 Dogecoin"); + } - // Check uniqueness of all addresses - for i in 0..addresses.len() { - for j in i + 1..addresses.len() { - assert_ne!( - addresses[i], addresses[j], - "Addresses at indices {} and {} should be unique", - i, j + /// Test BIP32 Dogecoin address format to ensure it start with 'D' and has correct length + #[test] + fn test_bip32_doge_address_format() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP32, CoinType::Dogecoin).unwrap(); + let addresses = xpub.derive_bip32_addresses(3, &None).unwrap(); + for addr in addresses { + assert!( + addr.starts_with("D"), + "BIP32 Dogecoin address should start with 'D'" + ); + assert!( + addr.len() >= 26 && addr.len() <= 35, + "BIP32 Dogecoin address length should be 26-35" ); } } - } - #[test] - fn test_bip32_known_addresses() { - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); - let addresses = xpub.derive_bip32_addresses(3).unwrap(); + /// Verify address derivation at index 0 to ensure edge case is handled correctly. + #[test] + fn test_bip32_doge_index_zero() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP32, COIN_TYPE).unwrap(); + let address = xpub.derive_bip32_addresses(1, &None).unwrap(); + assert_eq!( + address[0], BIP32_EXPECTED_ADDRESS_DOGE[0], + "BIP32 address at index 0 does not match expected" + ); + } + + /// Ensure hardened index derivation with xPub fails as per BIP32 rules. + #[test] + fn test_bip32_doge_hardened_index() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP32, COIN_TYPE).unwrap(); + let result = xpub.derive_non_hardened(0x80000000); + assert!(result.is_err(), "Hardened index derivation should fail"); + } + + /// Derive a child at the maximum non-hardened index (2^31 - 1) and verify the result for Dogecoin (DOGE) + #[test] + fn test_bip32_doge_max_index() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP32, COIN_TYPE).unwrap(); + let result = xpub.derive_non_hardened(0x7FFFFFFF); + assert!(result.is_ok(), "Max non-hardened index should succeed"); + } + + /// This test checks the depth, index, and parent fingerprint of a derived child from an xpub according to BIP32 (for DOGE) + #[test] + fn test_bip32_doge_depth_and_fingerprint() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP32, COIN_TYPE).unwrap(); + let child = xpub.derive_non_hardened(0).unwrap(); + assert_eq!(child.depth, xpub.depth + 1, "Depth should increase by 1"); + assert_eq!(child.child_number, 0, "Child number should match index"); + assert_eq!( + child.parent_fingerprint, + xpub.fingerprint(), + "Parent fingerprint mismatch" + ); + } + + // Tests that serializing and then deserializing a BIP32 xpub preserves its data + #[test] + fn test_bip32_doge_serialization_round_trip() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP32, COIN_TYPE).unwrap(); + let serialized = xpub.to_base58(); + let deserialized = Xpub::from_base58(&serialized, COIN_TYPE).unwrap(); + assert_eq!( + xpub.to_base58(), + deserialized.to_base58(), + "Serialization round-trip failed" + ); + } - for (i, expected) in EXPECTED_BIP32_ADDRESSES.iter().enumerate() { + // Tests that derived BIP32 public keys are in compressed (33-byte) format + #[test] + fn test_bip32_doge_public_key_compression() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP32, COIN_TYPE).unwrap(); + let child = xpub.derive_non_hardened(0).unwrap(); assert_eq!( - &addresses[i], expected, - "Address at index {} should match test vector", - i + child.public_key.serialize().len(), + 33, + "Public key should be compressed" ); } } - #[test] - fn test_bip32_address_limits() { - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); - - // Test with exactly 100 addresses - let result_100 = xpub.derive_bip32_addresses(100); - assert!( - result_100.is_ok(), - "Should succesfully generate 100 addresses" - ); - assert_eq!( - result_100.unwrap().len(), - 100, - "Should generate exactly 100 addresses" - ); - - // Test with more than 100 addresses - add warning - println!("Warning: Attemping to generate more than 100 addresses in test environment"); - let result_101 = xpub.derive_bip32_addresses(101); - assert!( - result_101.is_ok(), - "Should still work for more than 100 addresses" - ); - println!("Warning: Successfully generated more than 100 addresses. Consider limiting address generation in tests"); + // Bitcoin Cash (BCH) BIP32 Test Module + mod bitcoincash_bip32 { + use bip_tools::{utils, CoinType, Xpub}; + use utils::AddressFormat; + + // Expected addresses for Legacy format (Base58) + const COIN_TYPE: CoinType = CoinType::BitcoinCash; + const XPUB_BCH_BIP32: &str = "xpub6DsYunNirm7J62yWYTVR4qKHfzyRwPoxRaJXDdFYLEGQSFFiDe5wpAXf7VcX5XP9A6mHv5b6qpcPrCtuqoJpkjwr45y6LqxHZxBm93akLDC"; + + /// Expected addresses for Legacy format (Base58) + const BIP32_EXPECTED_ADDRESS_BCH_LEGACY: [&str; 3] = [ + "1Cm5tkbJtJnxkFwho3wGhYdLDxgtS6EWRy", + "123VubGmrM5jQA5QwWnkN3ELwxL97VwDrx", + "1GbJ3uk8vyGwcxrpBYw2wWQMWGfzYJAPbp", + ]; + + // Expected addresses for CashAddr format (not prefix) + const BIP32_EXPECTED_ADDRESS_BCH_CASHADDR: [&str; 3] = [ + "qzq0l3g35sh0dkvd4ukwy0xdt4wvnhgx3c5tv36l9w", + "qq9hz9nujds8l205rvdvtcs480qqcpz0kclfd80zga", + "qz4svseqjyp72xge6pkwh26nsdna6z77fysuzg7ust", + ]; + + // Expected addresses for CashAddr format (with prefix) + const BIP32_EXPECTED_ADDRESS_BCH_CASHADDR_PREFIX: [&str; 3] = [ + "bitcoincash:qzq0l3g35sh0dkvd4ukwy0xdt4wvnhgx3c5tv36l9w", + "bitcoincash:qq9hz9nujds8l205rvdvtcs480qqcpz0kclfd80zga", + "bitcoincash:qz4svseqjyp72xge6pkwh26nsdna6z77fysuzg7ust", + ]; + + /// Test BIP32 derivation for multiple legacy addresses and verify + #[test] + fn test_bip32_bch_multiple_legacy_addresses() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP32, COIN_TYPE).unwrap(); + let addresses = xpub + .derive_bip32_addresses(3, &Some(AddressFormat::Legacy)) + .expect("BIP32 Multiple addresses derivation failed"); + assert_eq!(addresses.len(), 3, "Should generate 3 addresses"); + assert_eq!( + addresses[0], BIP32_EXPECTED_ADDRESS_BCH_LEGACY[0], + "Multiple BIP32 addresses do not match expected" + ); + } + + /// Test BIP32 derivation for multiple cashaddr addresses and verify + #[test] + fn test_bip32_bch_multiple_cashaddr_addresses() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP32, COIN_TYPE).unwrap(); + let addresses = xpub + .derive_bip32_addresses(3, &Some(AddressFormat::CashAddr)) + .expect("BIP32 Multiple addresses derivation failed"); + assert_eq!(addresses.len(), 3, "Should generate 3 addresses"); + assert_eq!( + addresses[0], BIP32_EXPECTED_ADDRESS_BCH_CASHADDR[0], + "Multiple BIP32 addresses do not match expected" + ); + } + + /// Test BIP32 derivation for multiple cashaddr-prefix addresses and verify + #[test] + fn test_bip32_bch_multiple_cashaddr_prefix_addresses() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP32, COIN_TYPE).unwrap(); + let addresses = xpub + .derive_bip32_addresses(3, &Some(AddressFormat::CashAddrWithPrefix)) + .expect("BIP32 Multiple addresses derivation failed"); + assert_eq!(addresses.len(), 3, "Should generate 3 addresses"); + assert_eq!( + addresses[0], BIP32_EXPECTED_ADDRESS_BCH_CASHADDR_PREFIX[0], + "Multiple BIP32 addresses do not match expected" + ); + } + + /// Ensure BIP32 derivation for BCH is deterministic (same input yields same output) + #[test] + fn test_bip32_bch_derivation_consistency() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP32, COIN_TYPE).unwrap(); + let addresses1 = xpub.derive_bip32_addresses(1, &None).unwrap(); + let addresses2 = xpub.derive_bip32_addresses(1, &None).unwrap(); + assert_eq!( + addresses1, addresses2, + "BIP32 addresses should be consistent across derivations" + ); + } + + /// Test BIP32 derivation for BCH over a large index range + #[test] + fn test_bip32_bch_large_index_range() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP32, COIN_TYPE).unwrap(); + let count = 1000; + let addresses = xpub + .derive_bip32_addresses(count, &None) + .expect("BIP32 large index derivation failed"); + assert_eq!( + addresses.len(), + count as usize, + "Should generate 1000 addresses" + ); + } + + /// Test BIP32 Bitcoin Cash xpub parsing with a short invalid xpub and checks if an error is returned + #[test] + fn test_bip32_bch_short_invalid_xpub() { + let invalid_xpub = "xpub123"; + let result = Xpub::from_base58(invalid_xpub, CoinType::BitcoinCash); + assert!(result.is_err(), "Short xpub fail for BIP32 Bitcoin Cash"); + } + + /// Test BIP32 Bitcoin Cash address format to ensure it starts with 'q' (CashAddr) + #[test] + fn test_bip32_bch_address_format() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP32, CoinType::BitcoinCash).unwrap(); + let addresses = xpub + .derive_bip32_addresses(3, &Some(AddressFormat::CashAddr)) + .unwrap(); + for addr in addresses { + assert!( + addr.starts_with("q"), + "BIP32 Bitcoin Cash address should start with 'q' (CashAddr)" + ); + } + } + + /// Verify address derivation at index 0 to ensure edge case is handled correctly. + #[test] + fn test_bip32_bch_index_zero_legacy() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP32, COIN_TYPE).unwrap(); + let address = xpub + .derive_bip32_addresses(1, &Some(AddressFormat::Legacy)) + .unwrap(); + assert_eq!( + address[0], BIP32_EXPECTED_ADDRESS_BCH_LEGACY[0], + "BIP32 address at index 0 does not match expected" + ); + } + + /// Verify address derivation at index 0 to ensure edge case is handled correctly. + #[test] + fn test_bip32_bch_index_zero_cashaddr() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP32, COIN_TYPE).unwrap(); + let address = xpub + .derive_bip32_addresses(1, &Some(AddressFormat::CashAddr)) + .unwrap(); + assert_eq!( + address[0], BIP32_EXPECTED_ADDRESS_BCH_CASHADDR[0], + "BIP32 address at index 0 does not match expected" + ); + } + + /// Verify address derivation at index 0 to ensure edge case is handled correctly. + #[test] + fn test_bip32_bch_index_zero_cashaddr_withprefix() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP32, COIN_TYPE).unwrap(); + let address = xpub + .derive_bip32_addresses(1, &Some(AddressFormat::CashAddrWithPrefix)) + .unwrap(); + assert_eq!( + address[0], BIP32_EXPECTED_ADDRESS_BCH_CASHADDR_PREFIX[0], + "BIP32 address at index 0 does not match expected" + ); + } + + /// Ensure hardened index derivation with xPub fails as per BIP32 rules. + #[test] + fn test_bip32_bch_hardened_index() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP32, COIN_TYPE).unwrap(); + let result = xpub.derive_non_hardened(0x80000000); + assert!(result.is_err(), "Hardened index derivation should fail"); + } + + /// Derive a child at the maximum non-hardened index (2^31 - 1) and verify the result for Bitcoin Cash (BCH) + #[test] + fn test_bip32_bch_max_index() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP32, COIN_TYPE).unwrap(); + let result = xpub.derive_non_hardened(0x7FFFFFFF); + assert!(result.is_ok(), "Max non-hardened index should succeed"); + } + + /// This test checks the depth, index, and parent fingerprint of a derived child from an xpub according to BIP32 (for BCH) + #[test] + fn test_bip32_bch_depth_and_fingerprint() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP32, COIN_TYPE).unwrap(); + let child = xpub.derive_non_hardened(0).unwrap(); + assert_eq!(child.depth, xpub.depth + 1, "Depth should increase by 1"); + assert_eq!(child.child_number, 0, "Child number should match index"); + assert_eq!( + child.parent_fingerprint, + xpub.fingerprint(), + "Parent fingerprint mismatch" + ); + } + + // Tests that serializing and then deserializing a BIP32 xpub preserves its data + #[test] + fn test_bip32_bch_serialization_round_trip() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP32, COIN_TYPE).unwrap(); + let serialized = xpub.to_base58(); + let deserialized = Xpub::from_base58(&serialized, COIN_TYPE).unwrap(); + assert_eq!( + xpub.to_base58(), + deserialized.to_base58(), + "Serialization round-trip failed" + ); + } + + // Tests that derived BIP32 public keys are in compressed (33-byte) format + #[test] + fn test_bip32_bch_public_key_compression() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP32, COIN_TYPE).unwrap(); + let child = xpub.derive_non_hardened(0).unwrap(); + assert_eq!( + child.public_key.serialize().len(), + 33, + "Public key should be compressed" + ); + } } } diff --git a/tests/bip44_vectors.rs b/tests/bip44_vectors.rs index b7b6a65..1c06c47 100644 --- a/tests/bip44_vectors.rs +++ b/tests/bip44_vectors.rs @@ -1,268 +1,1084 @@ -use base58::FromBase58; -use bip_tools::Xpub; -use sha2::{Digest, Sha256}; - #[cfg(test)] -mod bip44_test { - use super::*; +mod test { + /// Bitcoin (BTC) test module + mod bitcoin { + use bip_tools::{CoinType, Xpub}; - // Test data for mainnet BIP44 derivation - const TEST_XPUB: &str = "xpub6CQrEh7fCh2jd4kdgqCxAQ4dpzvLGCmx5PM3GLQH1bQRCLWRUMHqeZ5XWi8QUM39BeFeBJaUA5VS4Vvw5oLaA6tHZBifTetFCxj6keSvfFS"; + // Coin-spesific constants + const COIN_TYPE: CoinType = CoinType::Bitcoin; + const XPUB_BTC_BIP44: &str = "xpub6CxEMjAQPnBECYbT4pJyfVWqZPb4TaHPcxhacFiVBSBA15NqF7UVfBDLg7Ccf89cQd1qFkJSr7bLVTfrEbBWSBrsNeYM5VaDugpR64PbE1T"; + const BIP44_EXPECTED_ADDRESS_BTC: [&str; 3] = [ + "1Ea7axUseGWah1Y7Mxetmz9P6nRrJVFAA4", + "1gnuicPb9Jbg8EQamG72ZK3dDyCmjNxZV", + "15Jz4V68onxWmdRdC2ZR8KDfghY1np1E9w", + ]; - // Known valid BIP44 addresses for test verification - const EXPECTED_BIP44_ADDRESSES: [&str; 3] = [ - "1AkcymbeHtiufKa1EgC1TY4E36ehdKVEDt", - "1BNedVV6nTX9oN77tMtoToFQ6FGQf8A3sY", - "176FPbVE5GScCh7jvMcj6TjBwrecs8BeAR", - ]; + const XPUB_BTC_BIP44_1: &str = "xpub6CB1R3PaHfGja4za4QgTzx7MD3tVDAySVeuBA6B94qNcb4PKSSRo68o6okuRseWgdW5zC9HNm9C5yCVgvVp2gLkFviuNZDAMhJndcN8UPc5"; + const BIP44_EXPECTED_ADDRESS_BTC_1: [&str; 3] = [ + "1FyBjxhfVcsCD93FNgd2d8EVZPVySsbVK5", + "1PV8Sgnh1ZkFKDG6dJB68mtvNiArUtZfeT", + "19BSRTWX1H5eEUmDdk4gsku9K3n3yjhZRp", + ]; - // Version bytes for mainnet xpub - const MAINNET_VERSION: [u8; 4] = [0x04, 0x88, 0xB2, 0x1E]; + /// Test generating BIP44 addresses + #[test] + fn test_bip44_btc_multiple_addresses() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP44, COIN_TYPE).unwrap(); + let addresses = xpub + .derive_bip44_addresses(3, 0, &None) + .expect("BIP44 derivation failed"); + assert_eq!(addresses.len(), 3, "Should generate 3 addresses"); + for (i, addr) in addresses.iter().enumerate() { + assert_eq!( + addr, BIP44_EXPECTED_ADDRESS_BTC[i], + "Address at index {} mismatch", + i + ); + } + } - // Basic Structure Tests + /// Test BIP44 address derivation for Bitcoin (BTC) - internal. + #[test] + fn test_bip44_btc_multiple_addresses_1() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP44_1, COIN_TYPE).unwrap(); + let addresses = xpub + .derive_bip44_addresses(3, 1, &None) + .expect("BIP44 derivation failed"); + assert_eq!(addresses.len(), 3, "Should generate 3 address"); + for (i, addr) in addresses.iter().enumerate() { + assert_eq!( + addr, BIP44_EXPECTED_ADDRESS_BTC_1[i], + "Multiple BIP44 addresses do not match expected" + ); + } + } - #[test] - fn test_bip44_xpub_from_base58() { - // Test valid and invalid xpub parsing - let xpub = Xpub::from_base58(TEST_XPUB); - assert!(xpub.is_ok(), "Failed to parse valid xpub"); + /// Ensure BIP44 derivation for BTC is deterministic (same input yields same output) + #[test] + fn test_bip44_btc_derivation_consistency() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP44, COIN_TYPE).unwrap(); + let addresses1 = xpub.derive_bip44_addresses(3, 0, &None).unwrap(); + let addresses2 = xpub.derive_bip44_addresses(3, 0, &None).unwrap(); + assert_eq!( + addresses1, addresses2, + "BIP44 addresses should be consistent" + ); + } - let test_cases = [ - ("invalid_xpub_string,", "Should fail with invalid xpub"), - ("xpub6CQrEh7fCh2", "Should fail with short xpub"), - ]; + /// Test generating zero BIP44 addresses + #[test] + fn test_bip44_btc_zero_address() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP44, COIN_TYPE).unwrap(); + let addresses = xpub.derive_bip44_addresses(0, 0, &None).unwrap(); + assert!( + addresses.is_empty(), + "Should return an empty vector for zero addresses" + ); + } - for (invalid_xpub, error_msg) in &test_cases { - assert!(Xpub::from_base58(*invalid_xpub).is_err(), "{}", error_msg); + /// Test BIP44 derivation for BTC over a large index range + #[test] + fn test_bip44_btc_large_index_range() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP44, COIN_TYPE).unwrap(); + let count = 10000; + let addresses = xpub + .derive_bip44_addresses(count, 0, &None) + .expect("BIP44 large index derivation failed"); + assert_eq!( + addresses.len(), + count as usize, + "Should generate 10000 addresses" + ); + for (i, addr) in addresses.iter().take(3).enumerate() { + assert_eq!( + addr, BIP44_EXPECTED_ADDRESS_BTC[i], + "BIP44 address at index {} does not match expected", + i + ); + } } - } - #[test] - fn test_bip44_to_base58() { - // Verify Base58 encoding is reversible - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); - let encoded = xpub.to_base58(); - - assert_eq!( - encoded, TEST_XPUB, - "Base58 encoding should match original xpub" - ); - - // Additional encoding checks - let decoded = encoded.from_base58().unwrap(); - - let mut version_check = [0u8; 4]; - version_check.copy_from_slice(&decoded[0..4]); - - assert_eq!( - decoded[4], xpub.depth, - "Depth should be preserved in encoding" - ); - assert_eq!( - version_check, MAINNET_VERSION, - "Version bytes should be preserved" - ); - } + /// Test BIP44 Bitcoin xpub parsing with a short invalid xpub and checks if an errors is returned + #[test] + fn test_bip44_btc_short_invalid_xpub() { + let invalid_xpub = "xpub123"; + let result = Xpub::from_base58(invalid_xpub, CoinType::Bitcoin); + assert!(result.is_err(), "Short xpub fail for BIP44 Bitcoin"); + } - // Format and Structural Checks - - #[test] - fn test_bip44_xpub_version_bytes() { - // Validate mainnet version bytes - let decoded = TEST_XPUB.from_base58().unwrap(); - assert_eq!( - &decoded[0..4], - &MAINNET_VERSION, - "Version bytes should match" - ); - } + /// Test BIP44 Bitcoin address format to ensure it start with '1' and has correct length + #[test] + fn test_bip44_btc_address_format() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP44, CoinType::Bitcoin).unwrap(); + let addresses = xpub.derive_bip44_addresses(3, 0, &None).unwrap(); + for addr in addresses { + assert!( + addr.starts_with("1"), + "BIP44 Bitcoin address should start '1'" + ); + assert!( + addr.len() >= 26 && addr.len() <= 35, + "BIP44 Bitcoin address lenght should be 26-35" + ); + } + } - #[test] - fn test_bip44_xpub_checksum() { - // Verify checksum calculation is correct - let decoded = TEST_XPUB.from_base58().unwrap(); - let main_data = &decoded[0..&decoded.len() - 4]; - let provided_checksum = &decoded[&decoded.len() - 4..]; - - let calculated_checksum = &Sha256::digest(&Sha256::digest(main_data))[..4]; - assert_eq!( - calculated_checksum, provided_checksum, - "Checksum should match" - ); - } + /// Ensure derive_bip44_addresses rejects invalid chain_type values. + #[test] + fn test_bip44_btc_invalid_chain_type() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP44, COIN_TYPE).unwrap(); + let result = xpub.derive_bip44_addresses(3, 2, &None); + assert!(result.is_err(), "Invalid chain_type should fail"); + } - #[test] - fn test_bip44_fingerprint() { - // Test non-zero fingerprint generation - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); - let fingerprint = xpub.fingerprint(); - assert!(fingerprint > 0, "Fingerprint should not be zero"); - } + /// Ensure hardened index derivation with xPub fails as per BIP44 rules. + #[test] + fn test_bip44_btc_hardened_index() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP44, COIN_TYPE).unwrap(); + let result = xpub.derive_non_hardened(0x80000000); + assert!(result.is_err(), "Hardened index derivation should fail"); + } + + /// Derive a child at the maximum non-hardened index (2^31 - 1) and verify the result for Bitcoin (BTC) (chain type = 0) + #[test] + fn test_bip44_btc_max_index_chain_0() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP44, COIN_TYPE).unwrap(); // Parse the BIP44 xPub for Bitcoin (assumed to be at m/44'/0'/0') + let chain_xpub = xpub + .derive_non_hardened(0) + .expect("Failed to derive chain xPub"); // Derive the chain xPub for chain_type = 0 (external addresses) + let max_index = 0x7FFFFFFF; + let child_xpub = chain_xpub + .derive_non_hardened(max_index) + .expect("Failed to derive child max index"); + let address = child_xpub.to_address(&None); + assert!( + address.starts_with("1"), + "Bitcoin address should start with '1'" + ); + } + + /// Derive a child at the maximum non-hardened index (2^31 - 1) and verify the result for Bitcoin (BTC) (chain type = 1) + #[test] + fn test_bip44_btc_max_index_chain_1() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP44, COIN_TYPE).unwrap(); // Parse the BIP44 xPub for Bitcoin (assumed to be at m/44'/0'/0') + let chain_xpub = xpub + .derive_non_hardened(1) + .expect("Failed to derive chain xPub"); // Derive the chain xPub for chain_type = 1 (internal addresses) + let max_index = 0x7FFFFFFF; + let child_xpub = chain_xpub + .derive_non_hardened(max_index) + .expect("Failed to derive child max index"); + let address = child_xpub.to_address(&None); + assert!( + address.starts_with("1"), + "Bitcoin address should start with '1'" + ); + } + + // Tests that BIP44 derivation correctly increments depth and sets parent fingerprint + #[test] + fn test_bip44_btc_depth_progression() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP44, COIN_TYPE).unwrap(); + let chain = xpub.derive_non_hardened(0).unwrap(); + let child = chain.derive_non_hardened(0).unwrap(); + assert_eq!(child.depth, xpub.depth + 2, "Depth should be 2"); + assert_eq!( + child.parent_fingerprint, + chain.fingerprint(), + "Parent fingerprint missmatch" + ); + } + + // Tests that serializing and then deserializing a BIP44 xpub preserves its data + #[test] + fn test_bip44_btc_serialization() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP44, COIN_TYPE).unwrap(); + let serialized = xpub.to_base58(); + let deserialized = Xpub::from_base58(&serialized, COIN_TYPE).unwrap(); + assert_eq!( + xpub.to_base58(), + deserialized.to_base58(), + "Serialization round-trip failed" + ); + } - #[test] - fn test_bip44_bitcoin_address_format() { - // Verify P2PKH address format and length - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); - let addresses = xpub.derive_bip44_addresses(1).unwrap(); - - assert!( - addresses[0].starts_with("1"), - "Invalid P2PKH address format" - ); - assert!( - addresses[0].len() >= 26 && addresses[0].len() <= 35, - "Invalid address length" - ); + // Tests that derived BIP44 public keys are in compressed (33-byte) format + #[test] + fn test_bip44_btc_public_key_compression() { + let xpub = Xpub::from_base58(XPUB_BTC_BIP44, COIN_TYPE).unwrap(); + let child_xpub = xpub + .derive_non_hardened(0) + .expect("Failed to derive child xPub"); + assert_eq!( + child_xpub.public_key.serialize().len(), + 33, + "Derived public key should be compressed (33 bytes)" + ); + } } - // BIP44 Specific Tests + /// Litecoin (LTC) BIP44 Tests + mod litecoin_bip44 { + use bip_tools::{CoinType, Xpub}; - #[test] - fn test_bip44_derivation_paths() { - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); + // Constants + const COIN_TYPE: CoinType = CoinType::Litecoin; + const XPUB_LTC_BIP44: &str = "Ltub2YWxAZMZahMWQnqFeUj44MgVGEwpuSyRGt8hPabhGfc2M7EVLFPgww3ZkAfGVFVLmewXezaqEnV21rE9ZEN6iRy77WtNaVu214hWkdAFtix"; + const BIP44_EXPECTED_ADDRESS_LTC: [&str; 3] = [ + "LPs2CLDRwQuG6NTaYcqLFCAHseKcpred9m", + "LZrrce6ZWkfFWKreefxdX862eyuagabgF8", + "LNwSvqc7uudTKt4Gz8VevVJNJ7hGboxADY", + ]; - // Test external chain (m/0/*) - let external = xpub.derive_non_hardened(0); - assert!(external.is_ok(), "Should derive external chain"); + const XPUB_LTC_BIP44_1: &str = "Ltub2ZRKyakzGNxJqTkH7aPLCxoAKGrwt3r72Kb1AfBc6UEekHq4Y9PM8v4EhD5PPAcJqHARMd2BqJW9cuqSCdnM4LYLJKjSDasRamJg7MxEiVL"; + const BIP44_EXPECTED_ADDRESS_LTC_1: [&str; 3] = [ + "Lbjbea2HnikjZcddSBJjy3vChde9PKF6kD", + "LX2NBLYsmU4b4BbBKzksvDsbKApLB3sGA4", + "LgquNMwiqh1pEyqHsnvScU7CRizTwkko6y", + ]; - // Test internal chain (m/1/*) - let internal = xpub.derive_non_hardened(1); - assert!(internal.is_ok(), "Should derive internal chain"); + /// Test BIP44 derivation for multiple addresses + #[test] + fn test_bip44_ltc_multiple_addresses() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP44, COIN_TYPE).unwrap(); + let addresses = xpub + .derive_bip44_addresses(3, 0, &None) + .expect("BIP44 multiple addresses derivation failed"); + assert_eq!(addresses.len(), 3, "Should generate 3 addresses"); + for (i, addr) in addresses.iter().enumerate() { + assert_eq!( + addr, BIP44_EXPECTED_ADDRESS_LTC[i], + "BIP44 address at index {} does not match expected", + i + ); + } + } - // Different chains should produce different addresses - assert_ne!( - external.unwrap().to_bitcoin_address(), - internal.unwrap().to_bitcoin_address(), - "External and internal chain should produce different addresses" - ); - } + /// Test BIP44 address derivation for Litecoin (LTC) - internal. + #[test] + fn test_bip44_ltc_multiple_addresses_1() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP44_1, COIN_TYPE).unwrap(); + let addresses = xpub + .derive_bip44_addresses(3, 1, &None) + .expect("BIP44 derivation failed"); + assert_eq!(addresses.len(), 3, "Should generate 3 address"); + for (i, addr) in addresses.iter().enumerate() { + assert_eq!( + addr, BIP44_EXPECTED_ADDRESS_LTC_1[i], + "Muliple BIP44 addresses do not match expected" + ) + } + } + + /// Ensure BIP44 derivation for LTC is deterministic (same input yields same output) + #[test] + fn test_bip44_ltc_derivation_consistency() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP44, COIN_TYPE).unwrap(); + let addresses1 = xpub.derive_bip44_addresses(3, 0, &None).unwrap(); + let addresses2 = xpub.derive_bip44_addresses(3, 0, &None).unwrap(); + assert_eq!( + addresses1, addresses2, + "BIP44 addresses should be consistent" + ); + } - #[test] - fn test_bip44_change_addresses() { - // Test external and internal chain address differentiation - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); - let external_chain = xpub.derive_non_hardened(0).unwrap(); // m/44'/0'/0'/0 - let internal_chain = xpub.derive_non_hardened(1).unwrap(); // m/44'/0'/0'/1 + /// Test generating zero BIP44 addresses (Litecoin) + #[test] + fn test_bip44_ltc_zero_address() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP44, COIN_TYPE).unwrap(); + let addresses = xpub.derive_bip44_addresses(0, 0, &None).unwrap(); + assert!( + addresses.is_empty(), + "Should return an empty vector for zero addresses" + ); + } - let external_addr = external_chain.to_bitcoin_address(); - let internal_addr = internal_chain.to_bitcoin_address(); + /// Test BIP44 derivation for LTC over a large index range + #[test] + fn test_bip44_ltc_large_index_range() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP44, COIN_TYPE).unwrap(); + let count = 10000; + let addresses = xpub + .derive_bip44_addresses(count, 0, &None) + .expect("BIP44 large index derivation failed"); + assert_eq!( + addresses.len(), + count as usize, + "Should generate 10000 addresses" + ); + for (i, addr) in addresses.iter().take(3).enumerate() { + assert_eq!( + addr, BIP44_EXPECTED_ADDRESS_LTC[i], + "BIP44 address at index {} does not match expected", + i + ); + } + } - assert_ne!( - external_addr, internal_addr, - "External and internal chain addresses should be different" - ); - } + /// Test error handling for invalid + #[test] + fn test_bip44_ltc_invalid_xpub() { + let invalid_xpub = "invalid_ltc_xpub"; + let result = Xpub::from_base58(invalid_xpub, COIN_TYPE); + assert!( + result.is_err(), + "Invalid xpub should fail for BIP44 derivation" + ); + } - #[test] - fn test_bip44_account_separation() { - // Verify different accounts generate different addresses - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); - - let first_account = xpub.derive_bip44_addresses(1).unwrap(); - let second_account = xpub - .derive_non_hardened(1) - .unwrap() - .derive_non_hardened(0) - .unwrap() - .to_bitcoin_address(); - - assert_ne!( - first_account[0], second_account, - "Different accounts should generate different addresses" - ); - } + /// Test BIP44 Litecoin xpub parsing with a short invalid xpub and checks if an error is returned + #[test] + fn test_bip44_ltc_short_invalid_xpub() { + let invalid_xpub = "xpub123"; + let result = Xpub::from_base58(invalid_xpub, CoinType::Litecoin); + assert!(result.is_err(), "Short xpub fail for BIP32 Litecoin"); + } - // Advanced Derivation Tests + /// Test BIP44 Litecoin address format to ensure it start with 'L' and has correct lenght + #[test] + fn test_bip44_ltc_address_format() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP44, CoinType::Litecoin).unwrap(); + let addresses = xpub.derive_bip44_addresses(3, 0, &None).unwrap(); + for addr in addresses { + assert!( + addr.starts_with("L"), + "BIP44 Litecoin address should start with 'L'" + ); + assert!( + addr.len() >= 26 && addr.len() <= 35, + "BIP44 Litecoin address lenght should be 26-35" + ); + } + } - #[test] - fn test_bip44_address_consistency() { - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); + /// Ensure derive_bip44_addresses rejects invalid chain_type values. + #[test] + fn test_bip44_ltc_invalid_chain_type() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP44, COIN_TYPE).unwrap(); + let result = xpub.derive_bip44_addresses(3, 2, &None); + assert!(result.is_err(), "Invalid chain_type should fail"); + } - // Test consecutive derivations - let first_derivation = xpub.derive_bip44_addresses(2).unwrap(); - let second_derivation = xpub.derive_bip44_addresses(2).unwrap(); + /// Ensure hardened index derivation with xPub fails as per BIP44 rules. + #[test] + fn test_bip44_ltc_hardened_index() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP44, COIN_TYPE).unwrap(); + let result = xpub.derive_non_hardened(0x80000000); + assert!(result.is_err(), "Hardened index derivation should fail"); + } - assert_eq!( - first_derivation[0], second_derivation[0], - "Same index should produce same address" - ); + /// Derive a child at the maximum non-hardened index (2^31 - 1) and verify the result for Litecoin (LTC) (chain type = 0) + #[test] + fn test_bip44_ltc_max_index_chain_0() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP44, COIN_TYPE).unwrap(); // Parse the BIP44 xPub for Litecoin (assumed to be at m/44'/2'/0') + let chain_xpub = xpub + .derive_non_hardened(0) + .expect("Failed to derive chain xPub"); // Derive the chain xPub for chain_type = 0 (external addresses) + let max_index = 0x7FFFFFFF; + let child_xpub = chain_xpub + .derive_non_hardened(max_index) + .expect("Failed to derive child max index"); + let address = child_xpub.to_address(&None); + assert!( + address.starts_with("L"), + "Litecoin address should start with 'L'" + ); + } - assert_ne!( - first_derivation[0], first_derivation[1], - "Different indices should produce different addresses" - ); - } + /// Derive a child at the maximum non-hardened index (2^31 - 1) and verify the result for Litecoin (LTC) (chain type = 1) + #[test] + fn test_bip44_ltc_max_index_chain_1() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP44, COIN_TYPE).unwrap(); // Parse the BIP44 xPub for Litecoin (assumed to be at m/44'/2'/0') + let chain_xpub = xpub + .derive_non_hardened(1) + .expect("Failed to derive chain xPub"); // Derive the chain xPub for chain_type = 1 (internal addresses) + let max_index = 0x7FFFFFFF; + let child_xpub = chain_xpub + .derive_non_hardened(max_index) + .expect("Failed to derive child max index"); + let address = child_xpub.to_address(&None); + assert!( + address.starts_with("L"), + "Litecoin address should start with 'L'" + ); + } + + // Tests that BIP44 derivation correctly increments depth and sets parent fingerprint + #[test] + fn test_bip44_ltc_depth_progression() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP44, COIN_TYPE).unwrap(); + let chain = xpub.derive_non_hardened(0).unwrap(); + let child = chain.derive_non_hardened(0).unwrap(); + assert_eq!(child.depth, xpub.depth + 2, "Depth should be 2"); + assert_eq!( + child.parent_fingerprint, + chain.fingerprint(), + "Parent fingerprint missmatch" + ); + } - #[test] - fn test_bip44_zero_address() { - // Test handling of zero address request - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); - let result = xpub.derive_bip44_addresses(0); - assert!(result.is_ok(), "Should handle zero address request"); - assert_eq!( - result.unwrap().len(), - 0, - "Should return empty vector for zero count" - ); + // Tests that serializing and then deserializing a BIP44 xpub preserves its data + #[test] + fn test_bip44_ltc_serialization() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP44, COIN_TYPE).unwrap(); + let serialized = xpub.to_base58(); + let deserialized = Xpub::from_base58(&serialized, COIN_TYPE).unwrap(); + assert_eq!( + xpub.to_base58(), + deserialized.to_base58(), + "Serialization round-trip failed" + ); + } + + // Tests that derived BIP44 public keys are in compressed (33-byte) format + #[test] + fn test_bip44_ltc_public_key_compression() { + let xpub = Xpub::from_base58(XPUB_LTC_BIP44, COIN_TYPE).unwrap(); + let child_xpub = xpub + .derive_non_hardened(0) + .expect("Failed to derive child xPub"); + assert_eq!( + child_xpub.public_key.serialize().len(), + 33, + "Derived public key should be compressed (33 bytes)" + ); + } } - // Multiple Address Tests + /// Dogecoin (DOGE) BIP44 Tests + mod dogecoin_bip44 { + use bip_tools::{CoinType, Xpub}; - #[test] - fn test_bip44_address_limits() { - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); - let count = 100; + // Constants + const COIN_TYPE: CoinType = CoinType::Dogecoin; + const XPUB_DOGE_BIP44: &str = "dgub8ruYKJZx5Ki82KRujYrp8tvcN5tTYajBKj9sbFeeLqM4xKQGvFcqYntc4BYaXF7WPCY3Y1zdJ1VgdDrcWLyBp5GmobAiGuk672Qn4f4rtms"; + const BIP44_EXPECTED_ADDRESS_DOGE: [&str; 3] = [ + "DJ3U8pgzkU7q349B4kMyhkCH1ZpqnbRHtb", + "DTHWzjtctfj37pbPxBBdNPMZMHPZ4i7phC", + "DREHyEz5bwix16FzR3ALP1XYQiZh4MgVk7", + ]; + + const XPUB_DOGE_BIP44_1: &str = "dgub8rmtrKBGg1ph9NxX1UmDr6sJaUHcwUmHWECHVXQyimyt1Gxkjhh4JaReFgsweqHWphNyEG6n5MtBYCBzfj1Z4FmWiftcUUdc5NTFJB8Sofg"; + const BIP44_EXPECTED_ADDRESS_DOGE_1: [&str; 3] = [ + "DCQRDgEVL55q7CokH86G3gc9YneumK5FDf", + "D6aNhV59wDSE5jUhe7xX1tgvU64orFP3WB", + "D811vA68rTcHnEDLEW4Psve7v5BoDNxtX1", + ]; + + /// Test BIP44 derivation for multiple addresses + #[test] + fn test_bip44_doge_multiple_addresses() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP44, COIN_TYPE).unwrap(); + let addresses = xpub + .derive_bip44_addresses(3, 0, &None) + .expect("BIP44 multiple addresses derivation failed"); + assert_eq!(addresses.len(), 3, "Should generate 3 addresses"); + for (i, addr) in addresses.iter().enumerate() { + assert_eq!( + addr, BIP44_EXPECTED_ADDRESS_DOGE[i], + "BIP44 address at index {} does not match expected", + i + ); + } + } + + /// Test BIP44 address derivation for Dogecoin (DOGE) - internal. + #[test] + fn test_bip44_doge_multiple_addresses_1() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP44_1, COIN_TYPE).unwrap(); + let addresses = xpub + .derive_bip44_addresses(3, 1, &None) + .expect("BIP44 derivaiton failed"); + assert_eq!(addresses.len(), 3, "Should generate address"); + for (i, addr) in addresses.iter().enumerate() { + assert_eq!( + addr, BIP44_EXPECTED_ADDRESS_DOGE_1[i], + "Multiple BIP44 addresses do not match expected" + ); + } + } - if count > 100 { - eprintln!( - "Warning: Attempting to generate more than 100 addresses in test environment" + /// Ensure BIP44 derivation for DOGE is deterministic (same input yields same output) + #[test] + fn test_bip44_doge_derivation_consistency() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP44, COIN_TYPE).unwrap(); + let addresses1 = xpub.derive_bip44_addresses(3, 0, &None).unwrap(); + let addresses2 = xpub.derive_bip44_addresses(3, 0, &None).unwrap(); + assert_eq!( + addresses1, addresses2, + "BIP44 addresses should be consistent" ); } - let result = xpub.derive_bip44_addresses(count); - assert!( - result.is_ok(), - "Should still work for more than 100 addresses" - ); + /// Test generating zero BIP44 addresses (DOGE) + #[test] + fn test_bip44_doge_zero_address() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP44, COIN_TYPE).unwrap(); + let addresses = xpub.derive_bip44_addresses(0, 0, &None).unwrap(); + assert!( + addresses.is_empty(), + "Should return an empty vector for zero addresses" + ); + } - let addresses = result.unwrap(); - eprintln!("Generated {} addresses", addresses.len()); - assert_eq!( - addresses.len(), - count.try_into().unwrap(), - "Should generate exactly {count} addresses" - ); + /// Test BIP44 derivation for DOGE over a large index range + #[test] + fn test_bip44_doge_large_index_range() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP44, COIN_TYPE).unwrap(); + let count = 10000; + let addresses = xpub + .derive_bip44_addresses(count, 0, &None) + .expect("BIP44 large index derivation failed"); + assert_eq!( + addresses.len(), + count as usize, + "Should generate 10000 addresses" + ); + for (i, addr) in addresses.iter().take(3).enumerate() { + assert_eq!( + addr, BIP44_EXPECTED_ADDRESS_DOGE[i], + "BIP44 address at index {} does not match expected", + i + ); + } + } - if count > 100 { - eprintln!("Warning: Successfully generated more than 100 addresses"); + /// Test BIP44 Dogecoin xpub parsing with a short invalid xpub and checks if an error is returned + #[test] + fn test_bip44_doge_short_invalid_xpub() { + let invalid_xpub = "xpub123"; + let result = Xpub::from_base58(invalid_xpub, CoinType::Dogecoin); + assert!(result.is_err(), "Short xpub fail for BIP44 Dogecoin"); } - } - #[test] - fn test_bip44_multiple_addresses_uniqueness() { - // Verify all derived addresses are unique - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); - let count = 100; - let addresses = xpub.derive_bip44_addresses(count).unwrap(); - - for i in 0..addresses.len() { - for j in i + 1..addresses.len() { - assert_ne!( - addresses[i], addresses[j], - "BIP44 addresses should be unique" + /// Test BIP44 Dogecoin address format to ensure it start with 'D' and has correct lenght + #[test] + fn test_bip44_doge_address_format() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP44, CoinType::Dogecoin).unwrap(); + let addresses = xpub.derive_bip44_addresses(3, 0, &None).unwrap(); + for addr in addresses { + assert!( + addr.starts_with("D"), + "BIP44 Dogecoin address should start with 'D'" + ); + assert!( + addr.len() >= 26 && addr.len() <= 35, + "BIP44 Dogecoin address lenght should be 26-35" ); } } + + /// Ensure derive_bip44_addresses rejects invalid chain_type values. + #[test] + fn test_bip44_doge_invalid_chain_type() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP44, COIN_TYPE).unwrap(); + let result = xpub.derive_bip44_addresses(3, 2, &None); + assert!(result.is_err(), "Invalid chain_type should fail"); + } + + /// Ensure hardened index derivation with xPub fails as per BIP44 rules. + #[test] + fn test_bip44_doge_hardened_index() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP44, COIN_TYPE).unwrap(); + let result = xpub.derive_non_hardened(0x80000000); + assert!(result.is_err(), "Hardened index derivation should fail"); + } + + /// Derive a child at the maximum non-hardened index (2^31 - 1) and verify the result for Litecoin (LTC) (chain type = 0) + #[test] + fn test_bip44_doge_max_index_chain0() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP44, COIN_TYPE).unwrap(); // Parse the BIP44 xPub for Dogecoin (assumed to be at m/44'/3'/0') + let chain_xpub = xpub + .derive_non_hardened(0) + .expect("Failed to derive chain xPub"); // Derive the chain xPub for chain_type = 0 (external addresses) + let max_index = 0x7FFFFFFF; + let child_xpub = chain_xpub + .derive_non_hardened(max_index) + .expect("Failed to derive child max index"); + let address = child_xpub.to_address(&None); + assert!( + address.starts_with("D"), + "Dogecoin address should start with 'D'" + ); + } + + /// Derive a child at the maximum non-hardened index (2^31 - 1) and verify the result for Litecoin (LTC) (chain type = 1) + #[test] + fn test_bip44_doge_max_index_chain1() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP44, COIN_TYPE).unwrap(); // Parse the BIP44 xPub for Dogecoin (assumed to be at m/44'/3'/0') + let chain_xpub = xpub + .derive_non_hardened(1) + .expect("Failed to derive chain xPub"); // Derive the chain xPub for chain_type = 1 (internal addresses) + let max_index = 0x7FFFFFFF; + let child_xpub = chain_xpub + .derive_non_hardened(max_index) + .expect("Failed to derive child max index"); + let address = child_xpub.to_address(&None); + assert!( + address.starts_with("D"), + "Dogecoin address should start with 'D'" + ); + } + + // Tests that BIP44 derivation correctly increments depth and sets parent fingerprint + #[test] + fn test_bip44_doge_depth_progression() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP44, COIN_TYPE).unwrap(); + let chain = xpub.derive_non_hardened(0).unwrap(); + let child = chain.derive_non_hardened(0).unwrap(); + assert_eq!(child.depth, xpub.depth + 2, "Depth should be 2"); + assert_eq!( + child.parent_fingerprint, + chain.fingerprint(), + "Parent fingerprint missmatch" + ); + } + + // Tests that serializing and then deserializing a BIP44 xpub preserves its data + #[test] + fn test_bip44_doge_serialization() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP44, COIN_TYPE).unwrap(); + let serialized = xpub.to_base58(); + let deserialized = Xpub::from_base58(&serialized, COIN_TYPE).unwrap(); + assert_eq!( + xpub.to_base58(), + deserialized.to_base58(), + "Serialization round-trip failed" + ); + } + + // Tests that derived BIP44 public keys are in compressed (33-byte) format + #[test] + fn test_bip44_doge_public_key_compression() { + let xpub = Xpub::from_base58(XPUB_DOGE_BIP44, COIN_TYPE).unwrap(); + let child_xpub = xpub + .derive_non_hardened(0) + .expect("Failed to derive child xPub"); + assert_eq!( + child_xpub.public_key.serialize().len(), + 33, + "Derived public key should be compressed (33 bytes)" + ); + } } - #[test] - fn test_bip44_known_addresses() { - let xpub = Xpub::from_base58(TEST_XPUB).unwrap(); - let addresses = xpub.derive_bip44_addresses(3).unwrap(); + /// Bitcoin Cash (BCH) BIP44 Tests + mod bitcoincash_bip44 { + use bip_tools::{utils, CoinType, Xpub}; + use utils::AddressFormat; + + // Constants + const COIN_TYPE: CoinType = CoinType::BitcoinCash; + const XPUB_BCH_BIP44: &str = "xpub6BewxLEmwosTasa2dS9s74Ghiv7oTgTR6RP7kc5Ja4g57orTrZ3PGGfqm1tZTQhM4efmWgaKjJQnSDk6kGaGZufDevBFuajV9tD4tGXASFc"; + + /// Expected addresses for Legacy format (Base58) + const BIP44_EXPECTED_ADDRESS_BCH_LEGACY: [&str; 3] = [ + "1F3XiYNWdoGqmKZR4HkTurx7DjFQt98usy", + "1JrTBgh3mjAEVLdnieqGkCEx8qjs4Q3pGj", + "13932dNkDD3ygCtsQopAKQEgPAuQvJdFtr", + ]; + + /// Expected addresses for CashAddr format (not prefix) + const BIP44_EXPECTED_ADDRESS_BCH_CASHADDR: [&str; 3] = [ + "qzdqcw78ydvlvf3wzl93cshc7ezgz53e6qttgrgm0s", + "qrpagcxqyy0sdxhge9qpvqu5ly6vjfz7dcw5evy5x9", + "qqth23dw483yupmp6q97gvv6vukk0qez0c3uqp3zj0", + ]; + + /// Expected addresses for CashAddr format (with prefix) + const BIP44_EXPECTED_ADDRESS_BCH_CASHADDR_PREFIX: [&str; 3] = [ + "bitcoincash:qzdqcw78ydvlvf3wzl93cshc7ezgz53e6qttgrgm0s", + "bitcoincash:qrpagcxqyy0sdxhge9qpvqu5ly6vjfz7dcw5evy5x9", + "bitcoincash:qqth23dw483yupmp6q97gvv6vukk0qez0c3uqp3zj0", + ]; + + /// Test BIP44 derivation for a multiple legacy address and verify + #[test] + fn test_bip44_bch_multiple_legacy_address() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44, COIN_TYPE).unwrap(); + let addresses = xpub + .derive_bip44_addresses(3, 0, &Some(AddressFormat::Legacy)) + .expect("Failed to derive single Legacy address with BIP44"); + assert_eq!(addresses.len(), 3, "Should generate 3 addresses"); + assert_eq!( + addresses[0], BIP44_EXPECTED_ADDRESS_BCH_LEGACY[0], + "First BIP44 Legacy address does not match expected" + ); + } + + /// Verify BIP44 derivation consistency across format + #[test] + fn test_bip44_bch_format_consistency() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44, COIN_TYPE).unwrap(); + let addresses_legacy = xpub + .derive_bip44_addresses(3, 0, &Some(AddressFormat::Legacy)) + .expect("Failed to derive Legacy addresses"); + let legacy_addresses_again = xpub + .derive_bip44_addresses(3, 0, &Some(AddressFormat::Legacy)) + .expect("Failed to derive Legacy addresses again"); + assert_eq!( + addresses_legacy, legacy_addresses_again, + "Legacy BIP44 addresses format be consistent across derivation" + ); + for (i, addr) in legacy_addresses_again.iter().enumerate() { + assert_eq!( + addr, BIP44_EXPECTED_ADDRESS_BCH_LEGACY[i], + "Legacy BIP44 address at index {} does not match expected", + i + ); + } + } + + /// Test large-scale BIP44 derivation for (1000 addresses, Legacy) + #[test] + fn test_bip44_bch_large_scale_legacy_derivation() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44, COIN_TYPE).unwrap(); + let count = 1000; + let addresses = xpub + .derive_bip44_addresses(count, 0, &Some(AddressFormat::Legacy)) + .expect("Failed to derive large-scale Legacy addresses with BIP44"); + assert_eq!( + addresses.len(), + count as usize, + "Should derive exactly 1000 Legacy addresses" + ); + // Verify the first 3 addresses + for (i, addr) in addresses.iter().take(3).enumerate() { + assert_eq!( + addr, BIP44_EXPECTED_ADDRESS_BCH_LEGACY[i], + "Legacy BIP44 address at index {} does not match expected", + i + ); + } + } + + /// Derive a single CashAddr address and verify + #[test] + fn test_bip44_bch_multiple_cashaddr_address() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44, COIN_TYPE).unwrap(); + let addresses = xpub + .derive_bip44_addresses(3, 0, &Some(AddressFormat::CashAddr)) + .expect("Failed to derive single CashAddr address with BIP44"); + assert_eq!(addresses.len(), 3, "Should generate 3 addresses"); + assert_eq!( + addresses[0], BIP44_EXPECTED_ADDRESS_BCH_CASHADDR[0], + "First BIP44 CashAddr address does not match expected" + ); + } + + // Derive multiple CashAddrWithPrefix addresses and verify + #[test] + fn test_bip44_bch_multiple_cashaddr_prefix_addresses() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44, COIN_TYPE).unwrap(); + let addresses = xpub + .derive_bip44_addresses(3, 0, &Some(AddressFormat::CashAddrWithPrefix)) + .expect("Failed to derive multiple CashAddrWithPrefix addresses with BIP44"); + assert_eq!(addresses.len(), 3, "Should derive exactly 3 addresses"); + for (i, addr) in addresses.iter().enumerate() { + assert_eq!( + addr, BIP44_EXPECTED_ADDRESS_BCH_CASHADDR_PREFIX[i], + "CashAddrWithPrefix BIP44 address at index {} does not match expected", + i + ) + } + } + + /// Ensure BIP44 derivation for BCH is deterministic (same input yields same output) - chain type = 0 (external addresses) + #[test] + fn test_bip44_bch_derivation_consistency() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44, COIN_TYPE).unwrap(); + let addresses1 = xpub.derive_bip44_addresses(3, 0, &None).unwrap(); + let addresses2 = xpub.derive_bip44_addresses(3, 0, &None).unwrap(); + assert_eq!( + addresses1, addresses2, + "BIP44 addresses should be consistent" + ); + } + + /// Test BIP44 derivation for BCH over a large index range + #[test] + fn test_bip44_bch_large_index_range() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44, COIN_TYPE).unwrap(); + let count = 10000; + let addresses = xpub + .derive_bip44_addresses(count, 0, &None) + .expect("BIP44 large index derivation failed"); + assert_eq!( + addresses.len(), + count as usize, + "Should generate 10000 addresses" + ); + for (i, addr) in addresses.iter().take(3).enumerate() { + assert_eq!( + addr, BIP44_EXPECTED_ADDRESS_BCH_CASHADDR[i], + "BIP44 address at index {} does not match expected", + i + ); + } + } + + /// Test generating zero BIP44 addresses (BCH) + #[test] + fn test_bip44_bch_zero_address() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44, COIN_TYPE).unwrap(); + let addresses = xpub.derive_bip44_addresses(0, 0, &None).unwrap(); + assert!( + addresses.is_empty(), + "Should return an empty vector for zero addresses" + ); + } - assert_eq!( - addresses, EXPECTED_BIP44_ADDRESSES, - "Derived addresses should match expected values" - ); + /// Test BIP44 Bitcoin Cash xpub parsing with a short invalid xpub and checks if an error is returned + #[test] + fn test_bip44_bch_short_invalid_xpub() { + let invalid_xpub = "xpub123"; + let result = Xpub::from_base58(invalid_xpub, CoinType::BitcoinCash); + assert!(result.is_err(), " Short xpub fail for BIP44 Bitcoin Cash"); + } + + /// Test BIP44 Bitcoin Cash address format to ensure it start with 'q' (CashAddr) + #[test] + fn test_bip44_bch_address_format() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44, CoinType::BitcoinCash).unwrap(); + let addresses = xpub + .derive_bip44_addresses(3, 0, &Some(AddressFormat::CashAddr)) + .unwrap(); + for addr in addresses { + assert!( + addr.starts_with("q"), + "BIP44 Bitcoin Cash address should start with 'q' (CashAddr)" + ); + } + } + + /// Ensure derive_bip44_addresses rejects invalid chain_type values. + #[test] + fn test_bip44_bch_invalid_chain_type() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44, COIN_TYPE).unwrap(); + let result = xpub.derive_bip44_addresses(3, 2, &None); + assert!(result.is_err(), "Invalid chain_type should fail"); + } + + /// Ensure hardened index derivation with xPub fails as per BIP44 rules. + #[test] + fn test_bip44_bch_hardened_index() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44, COIN_TYPE).unwrap(); + let result = xpub.derive_non_hardened(0x80000000); + assert!(result.is_err(), "Hardened index derivation should fail"); + } + + /// Derive a child at the maximum non-hardened index (2^31 - 1) and verify the result for Bitcoin Cash (BCH) (chain type = 0) - legacy + #[test] + fn test_bip44_bch_max_index_chain_0_legacy() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44, COIN_TYPE).unwrap(); // Parse the BIP44 xPub for Bitcoin Cash (assumed to be at m/44'/145'/0') + let chain_xpub = xpub + .derive_non_hardened(0) + .expect("Failed to derive chain xPub"); // Derive the chain xPub for chain_type = 0 (external addresses) + let max_index = 0x7FFFFFFF; + let child_xpub = chain_xpub + .derive_non_hardened(max_index) + .expect("Failed to derive child max index"); + let address = child_xpub.to_address(&Some(AddressFormat::Legacy)); + assert!( + address.starts_with("1"), + "Bitcoin Cash address should start with '1'" + ); + } + + /// Derive a child at the maximum non-hardened index (2^31 - 1) and verify the result for Bitcoin Cash (BCH) (chain type = 0) - cashaddr + #[test] + fn test_bip44_bch_max_index_chain_0_cashaddr() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44, COIN_TYPE).unwrap(); // Parse the BIP44 xPub for Bitcoin Cash (assumed to be at m/44'/145'/0') + let chain_xpub = xpub + .derive_non_hardened(0) + .expect("Failed to derive chain xPub"); // Derive the chain xPub for chain_type = 0 (external addresses) + let max_index = 0x7FFFFFFF; + let child_xpub = chain_xpub + .derive_non_hardened(max_index) + .expect("Failed to derive child max index"); + let address = child_xpub.to_address(&Some(AddressFormat::CashAddr)); + assert!( + address.starts_with("q"), + "Bitcoin Cash address should start with 'q'" + ); + } + + /// Derive a child at the maximum non-hardened index (2^31 - 1) and verify the result for Bitcoin Cash (BCH) (chain type = 0) - cashaddr_prefix + #[test] + fn test_bip44_bch_max_index_chain_0_cashaddr_prefix() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44, COIN_TYPE).unwrap(); // Parse the BIP44 xPub for Bitcoin Cash (assumed to be at m/44'/145'/0') + let chain_xpub = xpub + .derive_non_hardened(0) + .expect("Failed to derive chain xPub"); // Derive the chain xPub for chain_type = 0 (external addresses) + let max_index = 0x7FFFFFFF; + let child_xpub = chain_xpub + .derive_non_hardened(max_index) + .expect("Failed to derive child max index"); + let address = child_xpub.to_address(&Some(AddressFormat::CashAddrWithPrefix)); + assert!( + address.starts_with("bitcoincash:q"), + "Bitcoin Cash address should start with 'bitcoincash:q'" + ); + } + + // Tests that BIP44 derivation correctly increments depth and sets parent fingerprint + #[test] + fn test_bip44_bch_depth_progression() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44, COIN_TYPE).unwrap(); + let chain = xpub.derive_non_hardened(0).unwrap(); + let child = chain.derive_non_hardened(0).unwrap(); + assert_eq!(child.depth, xpub.depth + 2, "Depth should be 2"); + assert_eq!( + child.parent_fingerprint, + chain.fingerprint(), + "Parent fingerprint missmatch" + ); + } + + // Tests that serializing and then deserializing a BIP44 xpub preserves its data + #[test] + fn test_bip44_bch_serialization() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44, COIN_TYPE).unwrap(); + let serialized = xpub.to_base58(); + let deserialized = Xpub::from_base58(&serialized, COIN_TYPE).unwrap(); + assert_eq!( + xpub.to_base58(), + deserialized.to_base58(), + "Serialization round-trip failed" + ); + } + + // Tests that derived BIP44 public keys are in compressed (33-byte) format + #[test] + fn test_bip44_bch_public_key_compression() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44, COIN_TYPE).unwrap(); + let child_xpub = xpub + .derive_non_hardened(0) + .expect("Failed to derive child xPub"); + assert_eq!( + child_xpub.public_key.serialize().len(), + 33, + "Derived public key should be compressed (33 bytes)" + ); + } + + const XPUB_BCH_BIP44_1: &str = "xpub6D1S8ySBPc2nQtT6LBcfzGyaxvfBsBdFpy1NmNiHJJdBv68JaTyqKpJv7sNLPkZndjo1UcXZLBGxj2gxPdx6EMygzsR3MCEVoqcnqvN8hi5"; + + /// Expected addresses for Legacy format (Base58) + const BIP44_EXPECTED_ADDRESS_BCH_LEGACY_1: [&str; 3] = [ + "1EsDjnG5bH3eKH9frw6J38v2KScAyA8pHr", + "19ynvXx1QbNJg6W8YAUe2P32oPGHJHUBEc", + "1HqNMC9LwhY6vZoPMWuT3hdHY6FEh2yfoJ", + ]; + + /// Expected addresses for CashAddr format (not prefix) + const BIP44_EXPECTED_ADDRESS_BCH_CASHADDR_1: [&str; 3] = [ + "qzvpjy827tsrevqka9a80uus2nwpcpfnygw45fr34c", + "qp38agnu540g3q7cyjvd4qev8eyez9tzju0r0lr5n8", + "qzu20ny6c97hkh9va2u25ngw8sprcdgq7uc4af4z87", + ]; + + /// Expected addresses for CashAddr format (with prefix) + const BIP44_EXPECTED_ADDRESS_BCH_CASHADDR_PREFIX_1: [&str; 3] = [ + "bitcoincash:qzvpjy827tsrevqka9a80uus2nwpcpfnygw45fr34c", + "bitcoincash:qp38agnu540g3q7cyjvd4qev8eyez9tzju0r0lr5n8", + "bitcoincash:qzu20ny6c97hkh9va2u25ngw8sprcdgq7uc4af4z87", + ]; + + /// Test BIP44 address derivation for Bitcoin Cash (BHC / legacy) - internal. + #[test] + fn test_bip44_bch_multiple_legacy_address_1() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44_1, COIN_TYPE).unwrap(); + let addresses = xpub + .derive_bip44_addresses(3, 1, &Some(AddressFormat::Legacy)) + .expect("Failed to derive single Legacy address with BIP44"); + assert_eq!(addresses.len(), 3, "Should generate 3 addresses"); + assert_eq!( + addresses[0], BIP44_EXPECTED_ADDRESS_BCH_LEGACY_1[0], + "First BIP44 Legacy address does not match expected" + ); + } + + /// Test BIP44 address derivation for Bitcoin Cash (BHC / cashaddr) - internal. + #[test] + fn test_bip44_bch_multiple_cashaddr_address_1() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44_1, COIN_TYPE).unwrap(); + let addresses = xpub + .derive_bip44_addresses(3, 1, &Some(AddressFormat::CashAddr)) + .expect("Failed to derive single Legacy address with BIP44"); + assert_eq!(addresses.len(), 3, "Should generate 3 addresses"); + assert_eq!( + addresses[0], BIP44_EXPECTED_ADDRESS_BCH_CASHADDR_1[0], + "First BIP44 Legacy address does not match expected" + ); + } + + /// Test BIP44 address derivation for Bitcoin Cash (BHC / cashaddr with prefix) - internal. + #[test] + fn test_bip44_bch_multiple_cashaddr_prefix_address_1() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44_1, COIN_TYPE).unwrap(); + let addresses = xpub + .derive_bip44_addresses(3, 1, &Some(AddressFormat::CashAddrWithPrefix)) + .expect("Failed to derive single Legacy address with BIP44"); + assert_eq!(addresses.len(), 3, "Should generate 3 addresses"); + assert_eq!( + addresses[0], BIP44_EXPECTED_ADDRESS_BCH_CASHADDR_PREFIX_1[0], + "First BIP44 Legacy address does not match expected" + ); + } + + /// Derive a child at the maximum non-hardened index (2^31 - 1) and verify the result for Bitcoin Cash (BCH) (chain type = 1) - legacy + #[test] + fn test_bip44_bch_max_index_chain_1_legacy() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44, COIN_TYPE).unwrap(); // Parse the BIP44 xPub for Bitcoin Cash (assumed to be at m/44'/145'/0') + let chain_xpub = xpub + .derive_non_hardened(1) + .expect("Failed to derive chain xPub"); // Derive the chain xPub for chain_type = 1 (internal addresses) + let max_index = 0x7FFFFFFF; + let child_xpub = chain_xpub + .derive_non_hardened(max_index) + .expect("Failed to derive child max index"); + let address = child_xpub.to_address(&Some(AddressFormat::Legacy)); + assert!( + address.starts_with("1"), + "Bitcoin Cash address should start with '1'" + ); + } + + /// Derive a child at the maximum non-hardened index (2^31 - 1) and verify the result for Bitcoin Cash (BCH) (chain type = 1) - cashaddr + #[test] + fn test_bip44_bch_max_index_chain_1_cashaddr() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44, COIN_TYPE).unwrap(); // Parse the BIP44 xPub for Bitcoin Cash (assumed to be at m/44'/145'/0') + let chain_xpub = xpub + .derive_non_hardened(1) + .expect("Failed to derive chain xPub"); // Derive the chain xPub for chain_type = 1 (internal addresses) + let max_index = 0x7FFFFFFF; + let child_xpub = chain_xpub + .derive_non_hardened(max_index) + .expect("Failed to derive child max index"); + let address = child_xpub.to_address(&Some(AddressFormat::CashAddr)); + assert!( + address.starts_with("q"), + "Bitcoin Cash address should start with 'q'" + ); + } + + /// Derive a child at the maximum non-hardened index (2^31 - 1) and verify the result for Bitcoin Cash (BCH) (chain type = 1) - cashaddr_prefix + #[test] + fn test_bip44_bch_max_index_chain_1_cashaddr_prefix() { + let xpub = Xpub::from_base58(XPUB_BCH_BIP44, COIN_TYPE).unwrap(); // Parse the BIP44 xPub for Bitcoin Cash (assumed to be at m/44'/145'/0') + let chain_xpub = xpub + .derive_non_hardened(1) + .expect("Failed to derive chain xPub"); // Derive the chain xPub for chain_type = 1 (internal addresses) + let max_index = 0x7FFFFFFF; + let child_xpub = chain_xpub + .derive_non_hardened(max_index) + .expect("Failed to derive child max index"); + let address = child_xpub.to_address(&Some(AddressFormat::CashAddrWithPrefix)); + assert!( + address.starts_with("bitcoincash:q"), + "Bitcoin Cash address should start with 'bitcoincash:q'" + ); + } } }