kaccy_bitcoin/
address_utils.rs

1//! Bitcoin Address Utilities
2//!
3//! This module provides utilities for analyzing and working with Bitcoin addresses,
4//! including address type detection, script analysis, and address metadata extraction.
5
6use crate::error::BitcoinError;
7use bitcoin::{Address, Network, ScriptBuf};
8use serde::{Deserialize, Serialize};
9use std::str::FromStr;
10
11/// Bitcoin address type
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13pub enum AddressType {
14    /// Pay-to-Public-Key-Hash (Legacy, starts with 1)
15    P2PKH,
16    /// Pay-to-Script-Hash (Legacy, starts with 3)
17    P2SH,
18    /// Pay-to-Witness-Public-Key-Hash (Native SegWit, starts with bc1q)
19    P2WPKH,
20    /// Pay-to-Witness-Script-Hash (Native SegWit, starts with bc1q)
21    P2WSH,
22    /// Pay-to-Taproot (Taproot, starts with bc1p)
23    P2TR,
24}
25
26impl AddressType {
27    /// Get a human-readable name for the address type
28    pub fn name(&self) -> &'static str {
29        match self {
30            Self::P2PKH => "P2PKH (Legacy)",
31            Self::P2SH => "P2SH (Script Hash)",
32            Self::P2WPKH => "P2WPKH (Native SegWit)",
33            Self::P2WSH => "P2WSH (Native SegWit Script)",
34            Self::P2TR => "P2TR (Taproot)",
35        }
36    }
37
38    /// Check if this is a SegWit address type
39    pub fn is_segwit(&self) -> bool {
40        matches!(self, Self::P2WPKH | Self::P2WSH | Self::P2TR)
41    }
42
43    /// Check if this is a legacy address type
44    pub fn is_legacy(&self) -> bool {
45        matches!(self, Self::P2PKH | Self::P2SH)
46    }
47
48    /// Check if this is a Taproot address
49    pub fn is_taproot(&self) -> bool {
50        matches!(self, Self::P2TR)
51    }
52
53    /// Get the typical witness size for this address type
54    pub fn typical_witness_size(&self) -> Option<usize> {
55        match self {
56            Self::P2PKH => None,       // No witness
57            Self::P2SH => None,        // Variable, depends on script
58            Self::P2WPKH => Some(107), // 1 + 1 + 72 + 1 + 33
59            Self::P2WSH => None,       // Variable, depends on script
60            Self::P2TR => Some(65),    // 1 + 1 + 64 (Schnorr signature)
61        }
62    }
63}
64
65/// Address metadata and analysis information
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct AddressInfo {
68    /// The original address string
69    pub address: String,
70
71    /// Address type
72    pub address_type: AddressType,
73
74    /// Network (mainnet, testnet, etc.)
75    pub network: Network,
76
77    /// Script public key
78    pub script_pubkey: ScriptBuf,
79
80    /// Whether this is a multisig address (for P2SH/P2WSH)
81    pub is_multisig: bool,
82
83    /// Estimated input size in vbytes when spending from this address
84    pub estimated_input_vsize: usize,
85
86    /// Whether this address supports RBF by default
87    pub supports_rbf: bool,
88}
89
90impl AddressInfo {
91    /// Analyze a Bitcoin address and extract information
92    pub fn analyze(address: &str) -> Result<Self, BitcoinError> {
93        let unchecked_addr = Address::from_str(address)
94            .map_err(|e| BitcoinError::InvalidAddress(format!("Invalid address: {}", e)))?;
95
96        // Detect network from address string
97        let network = Self::detect_network(address)?;
98
99        // Check the address against the detected network
100        let addr = unchecked_addr
101            .require_network(network)
102            .map_err(|_| BitcoinError::InvalidAddress("Address network mismatch".to_string()))?;
103
104        let script_pubkey = addr.script_pubkey();
105        let address_type = Self::detect_type(&addr)?;
106
107        // Estimate input size based on address type
108        let estimated_input_vsize = match address_type {
109            AddressType::P2PKH => 148, // ~148 vbytes
110            AddressType::P2SH => 91,   // ~91 vbytes for P2SH-P2WPKH
111            AddressType::P2WPKH => 68, // ~68 vbytes
112            AddressType::P2WSH => 104, // ~104 vbytes (variable)
113            AddressType::P2TR => 58,   // ~58 vbytes (key path spend)
114        };
115
116        // P2SH and P2WSH can be multisig, but we can't tell without the script
117        let is_multisig = matches!(address_type, AddressType::P2SH | AddressType::P2WSH);
118
119        Ok(Self {
120            address: address.to_string(),
121            address_type,
122            network,
123            script_pubkey,
124            is_multisig,
125            estimated_input_vsize,
126            supports_rbf: true, // All address types support RBF
127        })
128    }
129
130    /// Detect network from address string
131    fn detect_network(address: &str) -> Result<Network, BitcoinError> {
132        if address.starts_with("bc1") || address.starts_with('1') || address.starts_with('3') {
133            Ok(Network::Bitcoin)
134        } else if address.starts_with("tb1")
135            || address.starts_with('m')
136            || address.starts_with('n')
137            || address.starts_with('2')
138        {
139            Ok(Network::Testnet)
140        } else if address.starts_with("bcrt1") {
141            Ok(Network::Regtest)
142        } else {
143            Err(BitcoinError::InvalidAddress(
144                "Unable to detect network from address".to_string(),
145            ))
146        }
147    }
148
149    /// Detect the address type
150    fn detect_type(addr: &Address) -> Result<AddressType, BitcoinError> {
151        let script = addr.script_pubkey();
152
153        if script.is_p2pkh() {
154            Ok(AddressType::P2PKH)
155        } else if script.is_p2sh() {
156            Ok(AddressType::P2SH)
157        } else if script.is_p2wpkh() {
158            Ok(AddressType::P2WPKH)
159        } else if script.is_p2wsh() {
160            Ok(AddressType::P2WSH)
161        } else if script.is_p2tr() {
162            Ok(AddressType::P2TR)
163        } else {
164            Err(BitcoinError::InvalidAddress(
165                "Unknown address type".to_string(),
166            ))
167        }
168    }
169
170    /// Check if this address is on the given network
171    pub fn is_network(&self, network: Network) -> bool {
172        self.network == network
173    }
174
175    /// Get the fee cost (in sats) for spending from this address at a given fee rate
176    ///
177    /// # Arguments
178    /// * `fee_rate` - Fee rate in sat/vbyte
179    pub fn spending_fee_cost(&self, fee_rate: f64) -> u64 {
180        (self.estimated_input_vsize as f64 * fee_rate).ceil() as u64
181    }
182
183    /// Check if this address is more private than another
184    ///
185    /// SegWit and Taproot addresses are generally more private
186    pub fn is_more_private_than(&self, other: &AddressInfo) -> bool {
187        match (self.address_type, other.address_type) {
188            // Taproot is most private
189            (AddressType::P2TR, AddressType::P2TR) => false,
190            (AddressType::P2TR, _) => true,
191            (_, AddressType::P2TR) => false,
192
193            // SegWit is more private than legacy
194            _ if self.address_type.is_segwit() && !other.address_type.is_segwit() => true,
195            _ if !self.address_type.is_segwit() && other.address_type.is_segwit() => false,
196
197            // Same privacy level
198            _ => false,
199        }
200    }
201
202    /// Get a privacy score (0-100, higher is better)
203    pub fn privacy_score(&self) -> u8 {
204        match self.address_type {
205            AddressType::P2TR => 100, // Best privacy (looks like any other Taproot spend)
206            AddressType::P2WPKH => 80, // Good privacy (SegWit)
207            AddressType::P2WSH => 75, // Good privacy but reveals script hash
208            AddressType::P2SH => 60,  // Moderate (could be multisig or SegWit wrapper)
209            AddressType::P2PKH => 40, // Poor (legacy, larger on-chain footprint)
210        }
211    }
212}
213
214/// Address comparison utilities
215pub struct AddressComparator;
216
217impl AddressComparator {
218    /// Compare two addresses for cost efficiency
219    ///
220    /// Returns the more cost-efficient address (cheaper to spend from)
221    pub fn more_efficient<'a>(
222        addr1: &'a AddressInfo,
223        addr2: &'a AddressInfo,
224        fee_rate: f64,
225    ) -> &'a AddressInfo {
226        let cost1 = addr1.spending_fee_cost(fee_rate);
227        let cost2 = addr2.spending_fee_cost(fee_rate);
228
229        if cost1 <= cost2 { addr1 } else { addr2 }
230    }
231
232    /// Compare two addresses for privacy
233    ///
234    /// Returns the more private address
235    pub fn more_private<'a>(addr1: &'a AddressInfo, addr2: &'a AddressInfo) -> &'a AddressInfo {
236        if addr1.is_more_private_than(addr2) {
237            addr1
238        } else {
239            addr2
240        }
241    }
242
243    /// Get the recommended address type for new wallets
244    pub fn recommended_type() -> AddressType {
245        AddressType::P2TR // Taproot is the most modern and private
246    }
247}
248
249/// Batch address analyzer
250pub struct AddressBatchAnalyzer {
251    addresses: Vec<AddressInfo>,
252}
253
254impl AddressBatchAnalyzer {
255    /// Create a new batch analyzer
256    pub fn new() -> Self {
257        Self {
258            addresses: Vec::new(),
259        }
260    }
261
262    /// Add an address to analyze
263    pub fn add(&mut self, address: &str) -> Result<(), BitcoinError> {
264        let info = AddressInfo::analyze(address)?;
265        self.addresses.push(info);
266        Ok(())
267    }
268
269    /// Get statistics about the addresses
270    pub fn statistics(&self) -> AddressBatchStatistics {
271        let mut stats = AddressBatchStatistics::default();
272
273        for addr in &self.addresses {
274            match addr.address_type {
275                AddressType::P2PKH => stats.p2pkh_count += 1,
276                AddressType::P2SH => stats.p2sh_count += 1,
277                AddressType::P2WPKH => stats.p2wpkh_count += 1,
278                AddressType::P2WSH => stats.p2wsh_count += 1,
279                AddressType::P2TR => stats.p2tr_count += 1,
280            }
281
282            if addr.address_type.is_segwit() {
283                stats.segwit_count += 1;
284            }
285
286            if addr.address_type.is_legacy() {
287                stats.legacy_count += 1;
288            }
289
290            stats.total_count += 1;
291            stats.average_privacy_score += addr.privacy_score() as u32;
292        }
293
294        if stats.total_count > 0 {
295            stats.average_privacy_score /= stats.total_count as u32;
296        }
297
298        stats
299    }
300
301    /// Find addresses that should be upgraded (legacy to SegWit/Taproot)
302    pub fn find_upgradeable(&self) -> Vec<&AddressInfo> {
303        self.addresses
304            .iter()
305            .filter(|addr| addr.address_type.is_legacy())
306            .collect()
307    }
308
309    /// Get the most private address type in the batch
310    pub fn most_private_type(&self) -> Option<AddressType> {
311        self.addresses
312            .iter()
313            .max_by_key(|addr| addr.privacy_score())
314            .map(|addr| addr.address_type)
315    }
316}
317
318impl Default for AddressBatchAnalyzer {
319    fn default() -> Self {
320        Self::new()
321    }
322}
323
324/// Statistics from batch address analysis
325#[derive(Debug, Clone, Default, Serialize, Deserialize)]
326pub struct AddressBatchStatistics {
327    pub total_count: usize,
328    pub p2pkh_count: usize,
329    pub p2sh_count: usize,
330    pub p2wpkh_count: usize,
331    pub p2wsh_count: usize,
332    pub p2tr_count: usize,
333    pub segwit_count: usize,
334    pub legacy_count: usize,
335    pub average_privacy_score: u32,
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341
342    #[test]
343    fn test_address_type_properties() {
344        assert_eq!(AddressType::P2PKH.name(), "P2PKH (Legacy)");
345        assert!(!AddressType::P2PKH.is_segwit());
346        assert!(AddressType::P2PKH.is_legacy());
347
348        assert!(AddressType::P2WPKH.is_segwit());
349        assert!(!AddressType::P2WPKH.is_legacy());
350
351        assert!(AddressType::P2TR.is_taproot());
352        assert!(AddressType::P2TR.is_segwit());
353    }
354
355    #[test]
356    fn test_address_analysis_p2wpkh() {
357        let info = AddressInfo::analyze("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh").unwrap();
358        assert_eq!(info.address_type, AddressType::P2WPKH);
359        assert_eq!(info.network, Network::Bitcoin);
360        assert!(info.supports_rbf);
361        assert_eq!(info.estimated_input_vsize, 68);
362    }
363
364    #[test]
365    fn test_spending_fee_cost() {
366        let info = AddressInfo::analyze("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh").unwrap();
367        let fee = info.spending_fee_cost(10.0); // 10 sat/vbyte
368        assert_eq!(fee, 680); // 68 vbytes * 10 sat/vbyte
369    }
370
371    #[test]
372    fn test_privacy_score() {
373        let p2pkh = AddressInfo::analyze("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa").unwrap();
374        let p2wpkh = AddressInfo::analyze("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh").unwrap();
375
376        assert!(p2wpkh.privacy_score() > p2pkh.privacy_score());
377    }
378
379    #[test]
380    fn test_batch_analyzer() {
381        let mut analyzer = AddressBatchAnalyzer::new();
382        analyzer
383            .add("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh")
384            .unwrap();
385        analyzer.add("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa").unwrap();
386
387        let stats = analyzer.statistics();
388        assert_eq!(stats.total_count, 2);
389        assert_eq!(stats.p2wpkh_count, 1);
390        assert_eq!(stats.p2pkh_count, 1);
391        assert_eq!(stats.segwit_count, 1);
392        assert_eq!(stats.legacy_count, 1);
393    }
394
395    #[test]
396    fn test_find_upgradeable() {
397        let mut analyzer = AddressBatchAnalyzer::new();
398        analyzer
399            .add("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh")
400            .unwrap();
401        analyzer.add("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa").unwrap();
402
403        let upgradeable = analyzer.find_upgradeable();
404        assert_eq!(upgradeable.len(), 1);
405        assert_eq!(upgradeable[0].address_type, AddressType::P2PKH);
406    }
407
408    #[test]
409    fn test_address_comparator() {
410        let recommended = AddressComparator::recommended_type();
411        assert_eq!(recommended, AddressType::P2TR);
412    }
413
414    #[test]
415    fn test_witness_size() {
416        assert_eq!(AddressType::P2WPKH.typical_witness_size(), Some(107));
417        assert_eq!(AddressType::P2TR.typical_witness_size(), Some(65));
418        assert_eq!(AddressType::P2PKH.typical_witness_size(), None);
419    }
420}