|
| 1 | +//! Base58Check encoding/decoding for traditional Bitcoin addresses (P2PKH, P2SH). |
| 2 | +
|
| 3 | +use super::{AddressCodec, AddressError, Result}; |
| 4 | +use crate::bitcoin::hashes::Hash; |
| 5 | +use crate::bitcoin::{base58, PubkeyHash, Script, ScriptBuf, ScriptHash}; |
| 6 | + |
| 7 | +/// Base58Check codec with network-specific version bytes |
| 8 | +#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 9 | +pub struct Base58CheckCodec { |
| 10 | + /// Base58Check P2PKH version byte(s) |
| 11 | + pub pub_key_hash: u32, |
| 12 | + /// Base58Check P2SH version byte(s) |
| 13 | + pub script_hash: u32, |
| 14 | +} |
| 15 | + |
| 16 | +impl Base58CheckCodec { |
| 17 | + /// Create a new Base58Check codec with specified version bytes |
| 18 | + pub const fn new(pub_key_hash: u32, script_hash: u32) -> Self { |
| 19 | + Self { |
| 20 | + pub_key_hash, |
| 21 | + script_hash, |
| 22 | + } |
| 23 | + } |
| 24 | +} |
| 25 | + |
| 26 | +/// Encode a hash with version bytes to Base58Check format using bitcoin crate |
| 27 | +fn to_base58_check(hash: &[u8], version: u32) -> Result<String> { |
| 28 | + let mut data = Vec::new(); |
| 29 | + |
| 30 | + // Encode version bytes (1-4 bytes depending on size) |
| 31 | + if version <= 0xff { |
| 32 | + data.push(version as u8); |
| 33 | + } else if version <= 0xffff { |
| 34 | + data.extend_from_slice(&(version as u16).to_be_bytes()); |
| 35 | + } else { |
| 36 | + // For Zcash (up to 4 bytes) |
| 37 | + let bytes = version.to_be_bytes(); |
| 38 | + let start = bytes.iter().position(|&b| b != 0).unwrap_or(0); |
| 39 | + data.extend_from_slice(&bytes[start..]); |
| 40 | + } |
| 41 | + |
| 42 | + data.extend_from_slice(hash); |
| 43 | + |
| 44 | + // Use bitcoin crate's base58 encode_check which adds the checksum |
| 45 | + Ok(base58::encode_check(&data)) |
| 46 | +} |
| 47 | + |
| 48 | +/// Decode a Base58Check address to (hash, version) using bitcoin crate |
| 49 | +fn from_base58_check(address: &str) -> Result<(Vec<u8>, u32)> { |
| 50 | + // Use bitcoin crate's base58 decode_check which verifies the checksum |
| 51 | + let payload = |
| 52 | + base58::decode_check(address).map_err(|e| AddressError::Base58Error(e.to_string()))?; |
| 53 | + |
| 54 | + if payload.is_empty() { |
| 55 | + return Err(AddressError::Base58Error("Empty payload".to_string())); |
| 56 | + } |
| 57 | + |
| 58 | + // Extract version and hash |
| 59 | + // Try different version byte lengths |
| 60 | + let (version, hash) = if payload.len() >= 21 && (payload[0] == 0x1c || payload[0] == 0x1d) { |
| 61 | + // Zcash uses 2-byte versions starting with 0x1c or 0x1d |
| 62 | + if payload.len() >= 22 { |
| 63 | + let version = u32::from_be_bytes([0, 0, payload[0], payload[1]]); |
| 64 | + let hash = payload[2..].to_vec(); |
| 65 | + (version, hash) |
| 66 | + } else { |
| 67 | + // Single byte version |
| 68 | + let version = payload[0] as u32; |
| 69 | + let hash = payload[1..].to_vec(); |
| 70 | + (version, hash) |
| 71 | + } |
| 72 | + } else { |
| 73 | + // Standard single-byte version |
| 74 | + let version = payload[0] as u32; |
| 75 | + let hash = payload[1..].to_vec(); |
| 76 | + (version, hash) |
| 77 | + }; |
| 78 | + |
| 79 | + Ok((hash, version)) |
| 80 | +} |
| 81 | + |
| 82 | +impl AddressCodec for Base58CheckCodec { |
| 83 | + fn encode(&self, script: &Script) -> Result<String> { |
| 84 | + if script.is_p2pkh() { |
| 85 | + if script.len() != 25 { |
| 86 | + return Err(AddressError::InvalidScript( |
| 87 | + "Invalid P2PKH script length".to_string(), |
| 88 | + )); |
| 89 | + } |
| 90 | + let hash = &script.as_bytes()[3..23]; |
| 91 | + to_base58_check(hash, self.pub_key_hash) |
| 92 | + } else if script.is_p2sh() { |
| 93 | + if script.len() != 23 { |
| 94 | + return Err(AddressError::InvalidScript( |
| 95 | + "Invalid P2SH script length".to_string(), |
| 96 | + )); |
| 97 | + } |
| 98 | + let hash = &script.as_bytes()[2..22]; |
| 99 | + to_base58_check(hash, self.script_hash) |
| 100 | + } else { |
| 101 | + Err(AddressError::UnsupportedScriptType( |
| 102 | + "Base58Check only supports P2PKH and P2SH".to_string(), |
| 103 | + )) |
| 104 | + } |
| 105 | + } |
| 106 | + |
| 107 | + fn decode(&self, address: &str) -> Result<ScriptBuf> { |
| 108 | + let (hash, version) = from_base58_check(address)?; |
| 109 | + |
| 110 | + if version == self.pub_key_hash { |
| 111 | + let hash_array: [u8; 20] = hash.try_into().map_err(|_| { |
| 112 | + AddressError::InvalidAddress("Invalid pubkey hash length".to_string()) |
| 113 | + })?; |
| 114 | + let pubkey_hash = PubkeyHash::from_byte_array(hash_array); |
| 115 | + Ok(ScriptBuf::new_p2pkh(&pubkey_hash)) |
| 116 | + } else if version == self.script_hash { |
| 117 | + let hash_array: [u8; 20] = hash.try_into().map_err(|_| { |
| 118 | + AddressError::InvalidAddress("Invalid script hash length".to_string()) |
| 119 | + })?; |
| 120 | + let script_hash = ScriptHash::from_byte_array(hash_array); |
| 121 | + Ok(ScriptBuf::new_p2sh(&script_hash)) |
| 122 | + } else { |
| 123 | + Err(AddressError::InvalidAddress(format!( |
| 124 | + "Version mismatch: expected {} or {}, got {}", |
| 125 | + self.pub_key_hash, self.script_hash, version |
| 126 | + ))) |
| 127 | + } |
| 128 | + } |
| 129 | +} |
0 commit comments