use bitcoin_payment_instructions::{
PaymentInstructions,
dns_resolver::DNSHrnResolver,
};
#[cfg(feature = "http")]
use bitcoin_payment_instructions::http_resolver::HTTPHrnResolver;
use crate::{
Bip353Error,
config::ResolverConfig,
types::PaymentInfo,
parse_address,
metrics::Bip353Metrics,
};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use std::time::{SystemTime, Duration};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResolverType {
DNS,
#[cfg(feature = "http")]
HTTP,
}
#[derive(Debug, Clone)]
pub struct SafePaymentInfo {
pub payment_info: PaymentInfo,
pub warnings: Vec<AddressWarning>,
pub last_checked: SystemTime,
}
#[derive(Debug, Clone)]
pub enum AddressWarning {
AddressReused { tx_id: String },
StaleRecord { age: Duration },
DnssecWarning { message: String },
}
#[derive(Debug)]
struct AddressCache {
entries: Arc<RwLock<HashMap<String, CacheEntry>>>,
default_ttl: Duration,
}
#[derive(Debug, Clone)]
struct CacheEntry {
payment_info: PaymentInfo,
cached_at: SystemTime,
ttl: Duration,
}
impl AddressCache {
fn new(default_ttl: Duration) -> Self {
Self {
entries: Arc::new(RwLock::new(HashMap::new())),
default_ttl,
}
}
async fn get(&self, hrn: &str) -> Option<PaymentInfo> {
let entries = self.entries.read().await;
if let Some(entry) = entries.get(hrn) {
if entry.cached_at.elapsed().unwrap_or(Duration::MAX) < entry.ttl {
return Some(entry.payment_info.clone());
}
}
None
}
async fn insert(&self, hrn: String, payment_info: PaymentInfo) {
let mut entries = self.entries.write().await;
entries.insert(hrn, CacheEntry {
payment_info,
cached_at: SystemTime::now(),
ttl: self.default_ttl,
});
}
async fn invalidate(&self, hrn: &str) {
let mut entries = self.entries.write().await;
entries.remove(hrn);
}
}
pub struct Bip353Resolver {
dns_resolver: DNSHrnResolver,
#[cfg(feature = "http")]
http_resolver: HTTPHrnResolver,
resolver_type: ResolverType,
config: ResolverConfig,
cache: Option<Arc<AddressCache>>,
metrics: Option<Arc<Bip353Metrics>>,
}
impl Bip353Resolver {
pub fn new() -> Result<Self, Bip353Error> {
Self::with_config(ResolverConfig::default())
}
pub fn with_config(config: ResolverConfig) -> Result<Self, Bip353Error> {
Ok(Self {
dns_resolver: DNSHrnResolver(config.dns_resolver),
#[cfg(feature = "http")]
http_resolver: HTTPHrnResolver,
resolver_type: ResolverType::DNS,
config,
cache: None,
metrics: None,
})
}
pub fn with_type(resolver_type: ResolverType) -> Result<Self, Bip353Error> {
let config = ResolverConfig::default();
Ok(Self {
dns_resolver: DNSHrnResolver(config.dns_resolver),
#[cfg(feature = "http")]
http_resolver: HTTPHrnResolver,
resolver_type,
config,
cache: None,
metrics: None,
})
}
pub fn with_enhanced_config(
config: ResolverConfig,
enable_cache: bool,
cache_ttl: Duration,
enable_metrics: bool,
) -> Result<Self, Bip353Error> {
let cache = if enable_cache {
Some(Arc::new(AddressCache::new(cache_ttl)))
} else {
None
};
let metrics = if enable_metrics {
Some(Arc::new(Bip353Metrics::new()))
} else {
None
};
Ok(Self {
dns_resolver: DNSHrnResolver(config.dns_resolver),
#[cfg(feature = "http")]
http_resolver: HTTPHrnResolver,
resolver_type: ResolverType::DNS,
config,
cache,
metrics,
})
}
pub async fn resolve(&self, user: &str, domain: &str) -> Result<PaymentInfo, Bip353Error> {
let instructions = match self.resolver_type {
ResolverType::DNS => {
PaymentInstructions::parse(
&format!("{}@{}", user, domain),
self.config.network,
&self.dns_resolver,
true, ).await.map_err(Bip353Error::from)?
},
#[cfg(feature = "http")]
ResolverType::HTTP => {
PaymentInstructions::parse(
&format!("{}@{}", user, domain),
self.config.network,
&self.http_resolver,
true, ).await.map_err(Bip353Error::from)?
},
};
let uri = match &instructions {
PaymentInstructions::FixedAmount(fixed) => {
if let Some(method) = fixed.methods().first() {
match method {
bitcoin_payment_instructions::PaymentMethod::OnChain(addr) => {
let mut uri = format!("bitcoin:{}", addr);
if let Some(amount) = fixed.max_amount() {
uri.push_str(&format!("?amount={}", amount.btc_decimal_rounding_up_to_sats()));
}
uri
},
bitcoin_payment_instructions::PaymentMethod::LightningBolt11(invoice) => {
format!("bitcoin:?lightning={}", invoice)
},
bitcoin_payment_instructions::PaymentMethod::LightningBolt12(offer) => {
format!("bitcoin:?lno={}", offer)
},
}
} else {
return Err(Bip353Error::InvalidRecord("No payment methods found".into()));
}
},
PaymentInstructions::ConfigurableAmount(configurable) => {
let mut has_method = false;
let base_uri = if let Some(method) = configurable.methods().next() {
has_method = true;
match method {
bitcoin_payment_instructions::PossiblyResolvedPaymentMethod::LNURLPay { .. } => {
"bitcoin:".to_string()
},
bitcoin_payment_instructions::PossiblyResolvedPaymentMethod::Resolved(method) => {
match method {
bitcoin_payment_instructions::PaymentMethod::OnChain(addr) => {
format!("bitcoin:{}", addr)
},
bitcoin_payment_instructions::PaymentMethod::LightningBolt11(invoice) => {
format!("bitcoin:?lightning={}", invoice)
},
bitcoin_payment_instructions::PaymentMethod::LightningBolt12(offer) => {
format!("bitcoin:?lno={}", offer)
},
}
},
}
} else {
"bitcoin:".to_string()
};
if !has_method {
return Err(Bip353Error::InvalidRecord("No payment methods found".into()));
}
base_uri
},
};
Ok(PaymentInfo::from_instructions(instructions, uri))
}
pub async fn resolve_address(&self, address: &str) -> Result<PaymentInfo, Bip353Error> {
let (user, domain) = parse_address(address)?;
self.resolve(&user, &domain).await
}
pub async fn resolve_with_safety_checks(&self, user: &str, domain: &str) -> Result<SafePaymentInfo, Bip353Error> {
let hrn = format!("{}@{}", user, domain);
if let Some(cache) = &self.cache {
if let Some(cached) = cache.get(&hrn).await {
if let Some(metrics) = &self.metrics {
metrics.record_cache_hit();
}
return Ok(SafePaymentInfo {
payment_info: cached,
warnings: vec![], last_checked: SystemTime::now(),
});
} else if let Some(metrics) = &self.metrics {
metrics.record_cache_miss();
}
}
let start_time = std::time::Instant::now();
let payment_info = self.resolve(user, domain).await?;
let resolution_time = start_time.elapsed();
if let Some(cache) = &self.cache {
cache.insert(hrn.clone(), payment_info.clone()).await;
}
if let Some(metrics) = &self.metrics {
metrics.record_resolution_success(domain, resolution_time).await;
}
let warnings = self.check_basic_warnings(&payment_info).await;
Ok(SafePaymentInfo {
payment_info,
warnings,
last_checked: SystemTime::now(),
})
}
async fn check_basic_warnings(&self, _payment_info: &PaymentInfo) -> Vec<AddressWarning> {
let warnings = vec![];
warnings
}
pub async fn clear_cache(&self) {
if let Some(cache) = &self.cache {
let mut entries = cache.entries.write().await;
entries.clear();
}
}
pub async fn invalidate_cache(&self, hrn: &str) {
if let Some(cache) = &self.cache {
cache.invalidate(hrn).await;
}
}
pub fn get_metrics(&self) -> Option<crate::metrics::ResolutionStats> {
self.metrics.as_ref().map(|m| m.get_resolution_stats())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[ignore]
async fn test_resolve_address() {
let resolver = Bip353Resolver::new().unwrap();
let result = resolver.resolve_address("user@example.com").await;
if result.is_ok() {
let info = result.unwrap();
assert!(info.uri.starts_with("bitcoin:"));
}
}
#[tokio::test]
async fn test_enhanced_resolver() {
let config = ResolverConfig::default();
let resolver = Bip353Resolver::with_enhanced_config(
config,
true, Duration::from_secs(300), true, ).unwrap();
resolver.clear_cache().await;
resolver.invalidate_cache("test@example.com").await;
assert!(resolver.cache.is_some());
assert!(resolver.metrics.is_some());
assert!(resolver.get_metrics().is_some());
}
}