Skip to main content

bip353/
resolver.rs

1//! BIP-353 core resolver implementation
2
3use bitcoin_payment_instructions::{
4    PaymentInstructions,
5    dns_resolver::DNSHrnResolver,
6};
7
8#[cfg(feature = "http")]
9use bitcoin_payment_instructions::http_resolver::HTTPHrnResolver;
10
11use crate::{
12    Bip353Error,
13    config::ResolverConfig,
14    types::PaymentInfo,
15    parse_address,
16    metrics::Bip353Metrics,
17};
18
19use std::collections::HashMap;
20use std::sync::Arc;
21use tokio::sync::RwLock;
22use std::time::{SystemTime, Duration};
23
24/// Type of resolver to use
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ResolverType {
27    /// DNS resolver using DNS-over-TCP
28    DNS,
29    
30    /// HTTP resolver using HTTPS
31    #[cfg(feature = "http")]
32    HTTP,
33}
34
35/// Enhanced payment info with safety warnings
36#[derive(Debug, Clone)]
37pub struct SafePaymentInfo {
38    pub payment_info: PaymentInfo,
39    pub warnings: Vec<AddressWarning>,
40    pub last_checked: SystemTime,
41}
42
43/// Address usage warning
44#[derive(Debug, Clone)]
45pub enum AddressWarning {
46    /// Address was used in a previous transaction
47    AddressReused { tx_id: String },
48    /// DNS record is stale
49    StaleRecord { age: Duration },
50    /// DNSSEC validation issues
51    DnssecWarning { message: String },
52}
53
54/// Simple address cache with TTL
55#[derive(Debug)]
56struct AddressCache {
57    entries: Arc<RwLock<HashMap<String, CacheEntry>>>,
58    default_ttl: Duration,
59}
60
61#[derive(Debug, Clone)]
62struct CacheEntry {
63    payment_info: PaymentInfo,
64    cached_at: SystemTime,
65    ttl: Duration,
66}
67
68impl AddressCache {
69    fn new(default_ttl: Duration) -> Self {
70        Self {
71            entries: Arc::new(RwLock::new(HashMap::new())),
72            default_ttl,
73        }
74    }
75    
76    async fn get(&self, hrn: &str) -> Option<PaymentInfo> {
77        let entries = self.entries.read().await;
78        if let Some(entry) = entries.get(hrn) {
79            if entry.cached_at.elapsed().unwrap_or(Duration::MAX) < entry.ttl {
80                return Some(entry.payment_info.clone());
81            }
82        }
83        None
84    }
85    
86    async fn insert(&self, hrn: String, payment_info: PaymentInfo) {
87        let mut entries = self.entries.write().await;
88        entries.insert(hrn, CacheEntry {
89            payment_info,
90            cached_at: SystemTime::now(),
91            ttl: self.default_ttl,
92        });
93    }
94    
95    async fn invalidate(&self, hrn: &str) {
96        let mut entries = self.entries.write().await;
97        entries.remove(hrn);
98    }
99}
100
101/// BIP-353 resolver - (what's actually needed)
102pub struct Bip353Resolver {
103    dns_resolver: DNSHrnResolver,
104    #[cfg(feature = "http")]
105    http_resolver: HTTPHrnResolver,
106    resolver_type: ResolverType,
107    config: ResolverConfig,
108    cache: Option<Arc<AddressCache>>,
109    metrics: Option<Arc<Bip353Metrics>>,
110    // Removed: chain_monitor here (not used yet but will be considered in later versions)
111}
112
113impl Bip353Resolver {
114    /// Create a new resolver with default configuration
115    pub fn new() -> Result<Self, Bip353Error> {
116        Self::with_config(ResolverConfig::default())
117    }
118    
119    /// Create a new resolver with custom configuration
120    pub fn with_config(config: ResolverConfig) -> Result<Self, Bip353Error> {
121        Ok(Self { 
122            dns_resolver: DNSHrnResolver(config.dns_resolver),
123            #[cfg(feature = "http")]
124            http_resolver: HTTPHrnResolver,
125            resolver_type: ResolverType::DNS,
126            config,
127            cache: None,        
128            metrics: None,      
129        })
130    }
131    
132    /// Create a new resolver with a specific type
133    pub fn with_type(resolver_type: ResolverType) -> Result<Self, Bip353Error> {
134        let config = ResolverConfig::default();
135        
136        Ok(Self { 
137            dns_resolver: DNSHrnResolver(config.dns_resolver),
138            #[cfg(feature = "http")]
139            http_resolver: HTTPHrnResolver,
140            resolver_type,
141            config,
142            cache: None,        
143            metrics: None,      
144        })
145    }
146    
147    /// Create a new resolver with enhanced features (only cache and metrics)
148    pub fn with_enhanced_config(
149        config: ResolverConfig,
150        enable_cache: bool,
151        cache_ttl: Duration,
152        enable_metrics: bool,
153    ) -> Result<Self, Bip353Error> {
154        let cache = if enable_cache {
155            Some(Arc::new(AddressCache::new(cache_ttl)))
156        } else {
157            None
158        };
159        
160        let metrics = if enable_metrics {
161            Some(Arc::new(Bip353Metrics::new()))
162        } else {
163            None
164        };
165        
166        Ok(Self { 
167            dns_resolver: DNSHrnResolver(config.dns_resolver),
168            #[cfg(feature = "http")]
169            http_resolver: HTTPHrnResolver,
170            resolver_type: ResolverType::DNS,
171            config,
172            cache,
173            metrics,
174        })
175    }
176    
177    /// Resolve a human-readable Bitcoin address
178    pub async fn resolve(&self, user: &str, domain: &str) -> Result<PaymentInfo, Bip353Error> {
179        // Parse the payment instructions using the appropriate resolver
180        let instructions = match self.resolver_type {
181            ResolverType::DNS => {
182                PaymentInstructions::parse(
183                    &format!("{}@{}", user, domain),
184                    self.config.network,
185                    &self.dns_resolver,
186                    true, // Support proof-of-payment callbacks
187                ).await.map_err(Bip353Error::from)?
188            },
189            #[cfg(feature = "http")]
190            ResolverType::HTTP => {
191                PaymentInstructions::parse(
192                    &format!("{}@{}", user, domain),
193                    self.config.network,
194                    &self.http_resolver,
195                    true, // Support proof-of-payment callbacks
196                ).await.map_err(Bip353Error::from)?
197            },
198        };
199        
200        // Extract the URI based on the payment instructions
201        let uri = match &instructions {
202            PaymentInstructions::FixedAmount(fixed) => {
203                // For fixed amount instructions, we should have a concrete URI
204                if let Some(method) = fixed.methods().first() {
205                    match method {
206                        bitcoin_payment_instructions::PaymentMethod::OnChain(addr) => {
207                            let mut uri = format!("bitcoin:{}", addr);
208                            if let Some(amount) = fixed.max_amount() {
209                                uri.push_str(&format!("?amount={}", amount.btc_decimal_rounding_up_to_sats()));
210                            }
211                            uri
212                        },
213                        bitcoin_payment_instructions::PaymentMethod::LightningBolt11(invoice) => {
214                            format!("bitcoin:?lightning={}", invoice)
215                        },
216                        bitcoin_payment_instructions::PaymentMethod::LightningBolt12(offer) => {
217                            format!("bitcoin:?lno={}", offer)
218                        },
219                    }
220                } else {
221                    return Err(Bip353Error::InvalidRecord("No payment methods found".into()));
222                }
223            },
224            PaymentInstructions::ConfigurableAmount(configurable) => {
225                // For configurable amount instructions, we'll use a BIP-21 URI with the first method
226                let mut has_method = false;
227                let base_uri = if let Some(method) = configurable.methods().next() {
228                    has_method = true;
229                    match method {
230                        bitcoin_payment_instructions::PossiblyResolvedPaymentMethod::LNURLPay { .. } => {
231                            "bitcoin:".to_string()
232                        },
233                        bitcoin_payment_instructions::PossiblyResolvedPaymentMethod::Resolved(method) => {
234                            match method {
235                                bitcoin_payment_instructions::PaymentMethod::OnChain(addr) => {
236                                    format!("bitcoin:{}", addr)
237                                },
238                                bitcoin_payment_instructions::PaymentMethod::LightningBolt11(invoice) => {
239                                    format!("bitcoin:?lightning={}", invoice)
240                                },
241                                bitcoin_payment_instructions::PaymentMethod::LightningBolt12(offer) => {
242                                    format!("bitcoin:?lno={}", offer)
243                                },
244                            }
245                        },
246                    }
247                } else {
248                    "bitcoin:".to_string()
249                };
250                
251                if !has_method {
252                    return Err(Bip353Error::InvalidRecord("No payment methods found".into()));
253                }
254                
255                base_uri
256            },
257        };
258        
259        // Create payment info
260        Ok(PaymentInfo::from_instructions(instructions, uri))
261    }
262    
263    /// Resolve a human-readable Bitcoin address string
264    pub async fn resolve_address(&self, address: &str) -> Result<PaymentInfo, Bip353Error> {
265        let (user, domain) = parse_address(address)?;
266        self.resolve(&user, &domain).await
267    }
268    
269    /// Resolve with basic safety checks (cache + warnings)
270    pub async fn resolve_with_safety_checks(&self, user: &str, domain: &str) -> Result<SafePaymentInfo, Bip353Error> {
271        let hrn = format!("{}@{}", user, domain);
272        
273        // Check cache first
274        if let Some(cache) = &self.cache {
275            if let Some(cached) = cache.get(&hrn).await {
276                // Record cache hit
277                if let Some(metrics) = &self.metrics {
278                    metrics.record_cache_hit();
279                }
280                
281                return Ok(SafePaymentInfo {
282                    payment_info: cached,
283                    warnings: vec![], // No warnings for cached results for now
284                    last_checked: SystemTime::now(),
285                });
286            } else if let Some(metrics) = &self.metrics {
287                metrics.record_cache_miss();
288            }
289        }
290        
291        // Resolve using main impl
292        let start_time = std::time::Instant::now();
293        let payment_info = self.resolve(user, domain).await?;
294        let resolution_time = start_time.elapsed();
295        
296        // Cache the result
297        if let Some(cache) = &self.cache {
298            cache.insert(hrn.clone(), payment_info.clone()).await;
299        }
300        
301        // Record metrics
302        if let Some(metrics) = &self.metrics {
303            metrics.record_resolution_success(domain, resolution_time).await;
304        }
305        
306        // Basic warnings (can be extended later)
307        let warnings = self.check_basic_warnings(&payment_info).await;
308        
309        Ok(SafePaymentInfo {
310            payment_info,
311            warnings,
312            last_checked: SystemTime::now(),
313        })
314    }
315    
316    /// Basic warning checks that don't require blockchain integration
317    async fn check_basic_warnings(&self, _payment_info: &PaymentInfo) -> Vec<AddressWarning> {
318        let warnings = vec![];
319        
320        // Future: Adding basic checks like:
321        // - URI format validation
322        // - Parameter validation
323        // - Network compatibility checks
324        
325        warnings
326    }
327    
328    /// Clear cache
329    pub async fn clear_cache(&self) {
330        if let Some(cache) = &self.cache {
331            let mut entries = cache.entries.write().await;
332            entries.clear();
333        }
334    }
335    
336    /// Invalidate specific cache entry
337    pub async fn invalidate_cache(&self, hrn: &str) {
338        if let Some(cache) = &self.cache {
339            cache.invalidate(hrn).await;
340        }
341    }
342    
343    /// Get metrics if enabled
344    pub fn get_metrics(&self) -> Option<crate::metrics::ResolutionStats> {
345        self.metrics.as_ref().map(|m| m.get_resolution_stats())
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352    
353    #[tokio::test]
354    #[ignore]
355    async fn test_resolve_address() {
356        let resolver = Bip353Resolver::new().unwrap();
357        let result = resolver.resolve_address("user@example.com").await;
358        
359        if result.is_ok() {
360            let info = result.unwrap();
361            assert!(info.uri.starts_with("bitcoin:"));
362        }
363    }
364    
365    #[tokio::test]
366    async fn test_enhanced_resolver() {
367        let config = ResolverConfig::default();
368        let resolver = Bip353Resolver::with_enhanced_config(
369            config,
370            true, // enable cache
371            Duration::from_secs(300), // cache TTL
372            true, // enable metrics
373        ).unwrap();
374        
375        // Test cache functionality
376        resolver.clear_cache().await;
377        resolver.invalidate_cache("test@example.com").await;
378        
379        // Verify enhanced features are enabled
380        assert!(resolver.cache.is_some());
381        assert!(resolver.metrics.is_some());
382        assert!(resolver.get_metrics().is_some());
383    }
384}