use bech32::{FromBase32, ToBase32, Variant};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AddressError {
InvalidWitnessVersion,
InvalidWitnessLength,
InvalidEncoding,
UnsupportedVariant,
InvalidHRP,
}
impl std::fmt::Display for AddressError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AddressError::InvalidWitnessVersion => write!(f, "Invalid witness version"),
AddressError::InvalidWitnessLength => write!(f, "Invalid witness data length"),
AddressError::InvalidEncoding => write!(f, "Invalid address encoding"),
AddressError::UnsupportedVariant => write!(f, "Unsupported address variant"),
AddressError::InvalidHRP => write!(f, "Invalid human-readable part"),
}
}
}
impl std::error::Error for AddressError {}
pub use blvm_consensus::types::Network;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BitcoinAddress {
pub network: Network,
pub witness_version: u8,
pub witness_program: Vec<u8>,
}
impl BitcoinAddress {
pub fn new(
network: Network,
witness_version: u8,
witness_program: Vec<u8>,
) -> Result<Self, AddressError> {
if witness_version > 16 {
return Err(AddressError::InvalidWitnessVersion);
}
match witness_version {
0 => {
if witness_program.len() != 20 && witness_program.len() != 32 {
return Err(AddressError::InvalidWitnessLength);
}
}
1 => {
if witness_program.len() != 32 {
return Err(AddressError::InvalidWitnessLength);
}
}
_ => {
if witness_program.len() < 2 || witness_program.len() > 40 {
return Err(AddressError::InvalidWitnessLength);
}
}
}
Ok(BitcoinAddress {
network,
witness_version,
witness_program,
})
}
pub fn encode(&self) -> Result<String, AddressError> {
let hrp = self.network.hrp();
let program_base32 = witness_program_to_base32(&self.witness_program);
let mut data = vec![bech32::u5::try_from_u8(self.witness_version)
.map_err(|_| AddressError::InvalidWitnessVersion)?];
data.extend_from_slice(&program_base32);
let encoded = if self.witness_version == 0 {
bech32::encode(hrp, &data, Variant::Bech32)
.map_err(|_| AddressError::InvalidEncoding)?
} else {
bech32::encode(hrp, &data, Variant::Bech32m)
.map_err(|_| AddressError::InvalidEncoding)?
};
Ok(encoded)
}
pub fn decode(encoded: &str) -> Result<Self, AddressError> {
let (hrp, data, variant) =
bech32::decode(encoded).map_err(|_| AddressError::InvalidEncoding)?;
let network = match hrp.as_str() {
"bc" => Network::Mainnet,
"tb" => Network::Testnet,
"bcrt" => Network::Regtest,
_ => return Err(AddressError::InvalidHRP),
};
if data.is_empty() {
return Err(AddressError::InvalidEncoding);
}
let witness_version_u5 = data[0];
let witness_version = witness_version_u5.to_u8();
if witness_version > 16 {
return Err(AddressError::InvalidWitnessVersion);
}
let program_base32 = &data[1..];
let witness_program = base32_to_witness_program(program_base32)?;
match (witness_version, variant) {
(0, Variant::Bech32) => {
}
(1..=16, Variant::Bech32m) => {
}
_ => {
return Err(AddressError::UnsupportedVariant);
}
}
Ok(BitcoinAddress {
network,
witness_version,
witness_program,
})
}
pub fn is_taproot(&self) -> bool {
self.witness_version == 1 && self.witness_program.len() == 32
}
pub fn is_segwit(&self) -> bool {
self.witness_version == 0
}
pub fn address_type(&self) -> &'static str {
match (self.witness_version, self.witness_program.len()) {
(0, 20) => "P2WPKH",
(0, 32) => "P2WSH",
(1, 32) => "P2TR",
_ => "Unknown",
}
}
}
fn witness_program_to_base32(program: &[u8]) -> Vec<bech32::u5> {
program.to_base32()
}
fn base32_to_witness_program(data: &[bech32::u5]) -> Result<Vec<u8>, AddressError> {
Vec::<u8>::from_base32(data).map_err(|_| AddressError::InvalidEncoding)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_segwit_p2wpkh() {
let program = vec![0x75; 20]; let addr = BitcoinAddress::new(Network::Mainnet, 0, program).unwrap();
let encoded = addr.encode().unwrap();
assert!(encoded.starts_with("bc1"));
assert_eq!(addr.witness_version, 0);
assert_eq!(addr.witness_program.len(), 20);
}
#[test]
fn test_encode_segwit_p2wsh() {
let program = vec![0x75; 32]; let addr = BitcoinAddress::new(Network::Mainnet, 0, program).unwrap();
let encoded = addr.encode().unwrap();
assert!(encoded.starts_with("bc1"));
assert_eq!(addr.witness_version, 0);
assert_eq!(addr.witness_program.len(), 32);
}
#[test]
fn test_encode_taproot_p2tr() {
let program = vec![0x75; 32]; let addr = BitcoinAddress::new(Network::Mainnet, 1, program).unwrap();
let encoded = addr.encode().unwrap();
assert!(encoded.starts_with("bc1p"));
assert!(addr.is_taproot());
assert_eq!(addr.witness_version, 1);
assert_eq!(addr.witness_program.len(), 32);
}
#[test]
fn test_decode_segwit() {
let program = vec![0x75; 20];
let addr = BitcoinAddress::new(Network::Mainnet, 0, program.clone()).unwrap();
let encoded = addr.encode().unwrap();
let decoded = BitcoinAddress::decode(&encoded).unwrap();
assert_eq!(decoded.witness_version, 0);
assert_eq!(decoded.witness_program, program);
}
#[test]
fn test_decode_taproot() {
let program = vec![0x75; 32];
let addr = BitcoinAddress::new(Network::Mainnet, 1, program.clone()).unwrap();
let encoded = addr.encode().unwrap();
let decoded = BitcoinAddress::decode(&encoded).unwrap();
assert!(decoded.is_taproot());
assert_eq!(decoded.witness_version, 1);
assert_eq!(decoded.witness_program, program);
}
#[test]
fn test_invalid_witness_version() {
let program = vec![0x75; 20];
let result = BitcoinAddress::new(Network::Mainnet, 17, program);
assert!(result.is_err());
}
#[test]
fn test_invalid_witness_length_taproot() {
let program = vec![0x75; 20]; let result = BitcoinAddress::new(Network::Mainnet, 1, program);
assert!(result.is_err());
}
#[test]
fn test_network_hrp() {
assert_eq!(Network::Mainnet.hrp(), "bc");
assert_eq!(Network::Testnet.hrp(), "tb");
assert_eq!(Network::Regtest.hrp(), "bcrt");
}
#[test]
fn test_address_types() {
let p2wpkh = BitcoinAddress::new(Network::Mainnet, 0, vec![0; 20]).unwrap();
assert_eq!(p2wpkh.address_type(), "P2WPKH");
assert!(p2wpkh.is_segwit());
let p2wsh = BitcoinAddress::new(Network::Mainnet, 0, vec![0; 32]).unwrap();
assert_eq!(p2wsh.address_type(), "P2WSH");
assert!(p2wsh.is_segwit());
let p2tr = BitcoinAddress::new(Network::Mainnet, 1, vec![0; 32]).unwrap();
assert_eq!(p2tr.address_type(), "P2TR");
assert!(p2tr.is_taproot());
}
}