1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ResolverType {
27 DNS,
29
30 #[cfg(feature = "http")]
32 HTTP,
33}
34
35#[derive(Debug, Clone)]
37pub struct SafePaymentInfo {
38 pub payment_info: PaymentInfo,
39 pub warnings: Vec<AddressWarning>,
40 pub last_checked: SystemTime,
41}
42
43#[derive(Debug, Clone)]
45pub enum AddressWarning {
46 AddressReused { tx_id: String },
48 StaleRecord { age: Duration },
50 DnssecWarning { message: String },
52}
53
54#[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
101pub 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 }
112
113impl Bip353Resolver {
114 pub fn new() -> Result<Self, Bip353Error> {
116 Self::with_config(ResolverConfig::default())
117 }
118
119 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 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 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 pub async fn resolve(&self, user: &str, domain: &str) -> Result<PaymentInfo, Bip353Error> {
179 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, ).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, ).await.map_err(Bip353Error::from)?
197 },
198 };
199
200 let uri = match &instructions {
202 PaymentInstructions::FixedAmount(fixed) => {
203 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 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 Ok(PaymentInfo::from_instructions(instructions, uri))
261 }
262
263 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 pub async fn resolve_with_safety_checks(&self, user: &str, domain: &str) -> Result<SafePaymentInfo, Bip353Error> {
271 let hrn = format!("{}@{}", user, domain);
272
273 if let Some(cache) = &self.cache {
275 if let Some(cached) = cache.get(&hrn).await {
276 if let Some(metrics) = &self.metrics {
278 metrics.record_cache_hit();
279 }
280
281 return Ok(SafePaymentInfo {
282 payment_info: cached,
283 warnings: vec![], last_checked: SystemTime::now(),
285 });
286 } else if let Some(metrics) = &self.metrics {
287 metrics.record_cache_miss();
288 }
289 }
290
291 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 if let Some(cache) = &self.cache {
298 cache.insert(hrn.clone(), payment_info.clone()).await;
299 }
300
301 if let Some(metrics) = &self.metrics {
303 metrics.record_resolution_success(domain, resolution_time).await;
304 }
305
306 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 async fn check_basic_warnings(&self, _payment_info: &PaymentInfo) -> Vec<AddressWarning> {
318 let warnings = vec![];
319
320 warnings
326 }
327
328 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 pub async fn invalidate_cache(&self, hrn: &str) {
338 if let Some(cache) = &self.cache {
339 cache.invalidate(hrn).await;
340 }
341 }
342
343 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, Duration::from_secs(300), true, ).unwrap();
374
375 resolver.clear_cache().await;
377 resolver.invalidate_cache("test@example.com").await;
378
379 assert!(resolver.cache.is_some());
381 assert!(resolver.metrics.is_some());
382 assert!(resolver.get_metrics().is_some());
383 }
384}