1use crate::error::BitcoinError;
7use bitcoin::{Address, Network, ScriptBuf};
8use serde::{Deserialize, Serialize};
9use std::str::FromStr;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13pub enum AddressType {
14 P2PKH,
16 P2SH,
18 P2WPKH,
20 P2WSH,
22 P2TR,
24}
25
26impl AddressType {
27 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 pub fn is_segwit(&self) -> bool {
40 matches!(self, Self::P2WPKH | Self::P2WSH | Self::P2TR)
41 }
42
43 pub fn is_legacy(&self) -> bool {
45 matches!(self, Self::P2PKH | Self::P2SH)
46 }
47
48 pub fn is_taproot(&self) -> bool {
50 matches!(self, Self::P2TR)
51 }
52
53 pub fn typical_witness_size(&self) -> Option<usize> {
55 match self {
56 Self::P2PKH => None, Self::P2SH => None, Self::P2WPKH => Some(107), Self::P2WSH => None, Self::P2TR => Some(65), }
62 }
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct AddressInfo {
68 pub address: String,
70
71 pub address_type: AddressType,
73
74 pub network: Network,
76
77 pub script_pubkey: ScriptBuf,
79
80 pub is_multisig: bool,
82
83 pub estimated_input_vsize: usize,
85
86 pub supports_rbf: bool,
88}
89
90impl AddressInfo {
91 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 let network = Self::detect_network(address)?;
98
99 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 let estimated_input_vsize = match address_type {
109 AddressType::P2PKH => 148, AddressType::P2SH => 91, AddressType::P2WPKH => 68, AddressType::P2WSH => 104, AddressType::P2TR => 58, };
115
116 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, })
128 }
129
130 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 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 pub fn is_network(&self, network: Network) -> bool {
172 self.network == network
173 }
174
175 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 pub fn is_more_private_than(&self, other: &AddressInfo) -> bool {
187 match (self.address_type, other.address_type) {
188 (AddressType::P2TR, AddressType::P2TR) => false,
190 (AddressType::P2TR, _) => true,
191 (_, AddressType::P2TR) => false,
192
193 _ 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 _ => false,
199 }
200 }
201
202 pub fn privacy_score(&self) -> u8 {
204 match self.address_type {
205 AddressType::P2TR => 100, AddressType::P2WPKH => 80, AddressType::P2WSH => 75, AddressType::P2SH => 60, AddressType::P2PKH => 40, }
211 }
212}
213
214pub struct AddressComparator;
216
217impl AddressComparator {
218 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 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 pub fn recommended_type() -> AddressType {
245 AddressType::P2TR }
247}
248
249pub struct AddressBatchAnalyzer {
251 addresses: Vec<AddressInfo>,
252}
253
254impl AddressBatchAnalyzer {
255 pub fn new() -> Self {
257 Self {
258 addresses: Vec::new(),
259 }
260 }
261
262 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 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 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 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#[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); assert_eq!(fee, 680); }
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}