use crate::error::BitcoinError;
use bitcoin::{Address, Network, ScriptBuf};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AddressType {
P2PKH,
P2SH,
P2WPKH,
P2WSH,
P2TR,
}
impl AddressType {
pub fn name(&self) -> &'static str {
match self {
Self::P2PKH => "P2PKH (Legacy)",
Self::P2SH => "P2SH (Script Hash)",
Self::P2WPKH => "P2WPKH (Native SegWit)",
Self::P2WSH => "P2WSH (Native SegWit Script)",
Self::P2TR => "P2TR (Taproot)",
}
}
pub fn is_segwit(&self) -> bool {
matches!(self, Self::P2WPKH | Self::P2WSH | Self::P2TR)
}
pub fn is_legacy(&self) -> bool {
matches!(self, Self::P2PKH | Self::P2SH)
}
pub fn is_taproot(&self) -> bool {
matches!(self, Self::P2TR)
}
pub fn typical_witness_size(&self) -> Option<usize> {
match self {
Self::P2PKH => None, Self::P2SH => None, Self::P2WPKH => Some(107), Self::P2WSH => None, Self::P2TR => Some(65), }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AddressInfo {
pub address: String,
pub address_type: AddressType,
pub network: Network,
pub script_pubkey: ScriptBuf,
pub is_multisig: bool,
pub estimated_input_vsize: usize,
pub supports_rbf: bool,
}
impl AddressInfo {
pub fn analyze(address: &str) -> Result<Self, BitcoinError> {
let unchecked_addr = Address::from_str(address)
.map_err(|e| BitcoinError::InvalidAddress(format!("Invalid address: {}", e)))?;
let network = Self::detect_network(address)?;
let addr = unchecked_addr
.require_network(network)
.map_err(|_| BitcoinError::InvalidAddress("Address network mismatch".to_string()))?;
let script_pubkey = addr.script_pubkey();
let address_type = Self::detect_type(&addr)?;
let estimated_input_vsize = match address_type {
AddressType::P2PKH => 148, AddressType::P2SH => 91, AddressType::P2WPKH => 68, AddressType::P2WSH => 104, AddressType::P2TR => 58, };
let is_multisig = matches!(address_type, AddressType::P2SH | AddressType::P2WSH);
Ok(Self {
address: address.to_string(),
address_type,
network,
script_pubkey,
is_multisig,
estimated_input_vsize,
supports_rbf: true, })
}
fn detect_network(address: &str) -> Result<Network, BitcoinError> {
if address.starts_with("bc1") || address.starts_with('1') || address.starts_with('3') {
Ok(Network::Bitcoin)
} else if address.starts_with("tb1")
|| address.starts_with('m')
|| address.starts_with('n')
|| address.starts_with('2')
{
Ok(Network::Testnet)
} else if address.starts_with("bcrt1") {
Ok(Network::Regtest)
} else {
Err(BitcoinError::InvalidAddress(
"Unable to detect network from address".to_string(),
))
}
}
fn detect_type(addr: &Address) -> Result<AddressType, BitcoinError> {
let script = addr.script_pubkey();
if script.is_p2pkh() {
Ok(AddressType::P2PKH)
} else if script.is_p2sh() {
Ok(AddressType::P2SH)
} else if script.is_p2wpkh() {
Ok(AddressType::P2WPKH)
} else if script.is_p2wsh() {
Ok(AddressType::P2WSH)
} else if script.is_p2tr() {
Ok(AddressType::P2TR)
} else {
Err(BitcoinError::InvalidAddress(
"Unknown address type".to_string(),
))
}
}
pub fn is_network(&self, network: Network) -> bool {
self.network == network
}
pub fn spending_fee_cost(&self, fee_rate: f64) -> u64 {
(self.estimated_input_vsize as f64 * fee_rate).ceil() as u64
}
pub fn is_more_private_than(&self, other: &AddressInfo) -> bool {
match (self.address_type, other.address_type) {
(AddressType::P2TR, AddressType::P2TR) => false,
(AddressType::P2TR, _) => true,
(_, AddressType::P2TR) => false,
_ if self.address_type.is_segwit() && !other.address_type.is_segwit() => true,
_ if !self.address_type.is_segwit() && other.address_type.is_segwit() => false,
_ => false,
}
}
pub fn privacy_score(&self) -> u8 {
match self.address_type {
AddressType::P2TR => 100, AddressType::P2WPKH => 80, AddressType::P2WSH => 75, AddressType::P2SH => 60, AddressType::P2PKH => 40, }
}
}
pub struct AddressComparator;
impl AddressComparator {
pub fn more_efficient<'a>(
addr1: &'a AddressInfo,
addr2: &'a AddressInfo,
fee_rate: f64,
) -> &'a AddressInfo {
let cost1 = addr1.spending_fee_cost(fee_rate);
let cost2 = addr2.spending_fee_cost(fee_rate);
if cost1 <= cost2 { addr1 } else { addr2 }
}
pub fn more_private<'a>(addr1: &'a AddressInfo, addr2: &'a AddressInfo) -> &'a AddressInfo {
if addr1.is_more_private_than(addr2) {
addr1
} else {
addr2
}
}
pub fn recommended_type() -> AddressType {
AddressType::P2TR }
}
pub struct AddressBatchAnalyzer {
addresses: Vec<AddressInfo>,
}
impl AddressBatchAnalyzer {
pub fn new() -> Self {
Self {
addresses: Vec::new(),
}
}
pub fn add(&mut self, address: &str) -> Result<(), BitcoinError> {
let info = AddressInfo::analyze(address)?;
self.addresses.push(info);
Ok(())
}
pub fn statistics(&self) -> AddressBatchStatistics {
let mut stats = AddressBatchStatistics::default();
for addr in &self.addresses {
match addr.address_type {
AddressType::P2PKH => stats.p2pkh_count += 1,
AddressType::P2SH => stats.p2sh_count += 1,
AddressType::P2WPKH => stats.p2wpkh_count += 1,
AddressType::P2WSH => stats.p2wsh_count += 1,
AddressType::P2TR => stats.p2tr_count += 1,
}
if addr.address_type.is_segwit() {
stats.segwit_count += 1;
}
if addr.address_type.is_legacy() {
stats.legacy_count += 1;
}
stats.total_count += 1;
stats.average_privacy_score += addr.privacy_score() as u32;
}
if stats.total_count > 0 {
stats.average_privacy_score /= stats.total_count as u32;
}
stats
}
pub fn find_upgradeable(&self) -> Vec<&AddressInfo> {
self.addresses
.iter()
.filter(|addr| addr.address_type.is_legacy())
.collect()
}
pub fn most_private_type(&self) -> Option<AddressType> {
self.addresses
.iter()
.max_by_key(|addr| addr.privacy_score())
.map(|addr| addr.address_type)
}
}
impl Default for AddressBatchAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AddressBatchStatistics {
pub total_count: usize,
pub p2pkh_count: usize,
pub p2sh_count: usize,
pub p2wpkh_count: usize,
pub p2wsh_count: usize,
pub p2tr_count: usize,
pub segwit_count: usize,
pub legacy_count: usize,
pub average_privacy_score: u32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_address_type_properties() {
assert_eq!(AddressType::P2PKH.name(), "P2PKH (Legacy)");
assert!(!AddressType::P2PKH.is_segwit());
assert!(AddressType::P2PKH.is_legacy());
assert!(AddressType::P2WPKH.is_segwit());
assert!(!AddressType::P2WPKH.is_legacy());
assert!(AddressType::P2TR.is_taproot());
assert!(AddressType::P2TR.is_segwit());
}
#[test]
fn test_address_analysis_p2wpkh() {
let info = AddressInfo::analyze("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh").unwrap();
assert_eq!(info.address_type, AddressType::P2WPKH);
assert_eq!(info.network, Network::Bitcoin);
assert!(info.supports_rbf);
assert_eq!(info.estimated_input_vsize, 68);
}
#[test]
fn test_spending_fee_cost() {
let info = AddressInfo::analyze("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh").unwrap();
let fee = info.spending_fee_cost(10.0); assert_eq!(fee, 680); }
#[test]
fn test_privacy_score() {
let p2pkh = AddressInfo::analyze("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa").unwrap();
let p2wpkh = AddressInfo::analyze("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh").unwrap();
assert!(p2wpkh.privacy_score() > p2pkh.privacy_score());
}
#[test]
fn test_batch_analyzer() {
let mut analyzer = AddressBatchAnalyzer::new();
analyzer
.add("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh")
.unwrap();
analyzer.add("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa").unwrap();
let stats = analyzer.statistics();
assert_eq!(stats.total_count, 2);
assert_eq!(stats.p2wpkh_count, 1);
assert_eq!(stats.p2pkh_count, 1);
assert_eq!(stats.segwit_count, 1);
assert_eq!(stats.legacy_count, 1);
}
#[test]
fn test_find_upgradeable() {
let mut analyzer = AddressBatchAnalyzer::new();
analyzer
.add("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh")
.unwrap();
analyzer.add("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa").unwrap();
let upgradeable = analyzer.find_upgradeable();
assert_eq!(upgradeable.len(), 1);
assert_eq!(upgradeable[0].address_type, AddressType::P2PKH);
}
#[test]
fn test_address_comparator() {
let recommended = AddressComparator::recommended_type();
assert_eq!(recommended, AddressType::P2TR);
}
#[test]
fn test_witness_size() {
assert_eq!(AddressType::P2WPKH.typical_witness_size(), Some(107));
assert_eq!(AddressType::P2TR.typical_witness_size(), Some(65));
assert_eq!(AddressType::P2PKH.typical_witness_size(), None);
}
}