use core::fmt;
use std::str::FromStr;
use std::sync::Arc;
use crate::wallet::MintConnector;
#[derive(Debug)]
pub enum Bip353Error {
InvalidFormat,
EmptyUserOrDomain,
NoBitcoinUri,
MultipleBitcoinUris,
DnsResolution(String),
}
impl fmt::Display for Bip353Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidFormat => write!(f, "Address is not formatted correctly"),
Self::EmptyUserOrDomain => {
write!(f, "User name and domain must not be empty")
}
Self::NoBitcoinUri => write!(f, "No Bitcoin URI found"),
Self::MultipleBitcoinUris => write!(f, "Multiple Bitcoin URIs found"),
Self::DnsResolution(e) => write!(f, "DNS resolution failed: {e}"),
}
}
}
impl std::error::Error for Bip353Error {}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Bip353Address {
pub user: String,
pub domain: String,
}
impl Bip353Address {
pub(crate) async fn resolve(
self,
client: &Arc<dyn MintConnector + Send + Sync>,
) -> Result<String, Bip353Error> {
let dns_name = format!("{}.user._bitcoin-payment.{}", self.user, self.domain);
let mut bitcoin_uris: Vec<String> = client
.resolve_dns_txt(&dns_name)
.await
.map_err(|e| Bip353Error::DnsResolution(e.to_string()))?
.into_iter()
.filter(|txt_data| {
txt_data
.get(..8)
.map(|p| p.eq_ignore_ascii_case("bitcoin:"))
.unwrap_or(false)
})
.collect();
match bitcoin_uris.len() {
0 => Err(Bip353Error::NoBitcoinUri),
1 => Ok(bitcoin_uris.swap_remove(0)),
_ => Err(Bip353Error::MultipleBitcoinUris),
}
}
}
impl FromStr for Bip353Address {
type Err = Bip353Error;
fn from_str(address: &str) -> Result<Self, Self::Err> {
let addr = address.trim();
let addr = addr.strip_prefix("₿").unwrap_or(addr);
let parts: Vec<&str> = addr.split('@').collect();
if parts.len() != 2 {
return Err(Bip353Error::InvalidFormat);
}
let user = parts[0].trim();
let domain = parts[1].trim();
if user.is_empty() || domain.is_empty() {
return Err(Bip353Error::EmptyUserOrDomain);
}
Ok(Self {
user: user.to_string(),
domain: domain.to_string(),
})
}
}
impl fmt::Display for Bip353Address {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}@{}", self.user, self.domain)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bip353_address_parsing() {
let addr = Bip353Address::from_str("alice@example.com").unwrap();
assert_eq!(addr.user, "alice");
assert_eq!(addr.domain, "example.com");
let addr = Bip353Address::from_str("₿bob@bitcoin.org").unwrap();
assert_eq!(addr.user, "bob");
assert_eq!(addr.domain, "bitcoin.org");
let addr = Bip353Address::from_str(" charlie@test.net ").unwrap();
assert_eq!(addr.user, "charlie");
assert_eq!(addr.domain, "test.net");
let addr = Bip353Address {
user: "test".to_string(),
domain: "example.com".to_string(),
};
assert_eq!(addr.to_string(), "test@example.com");
}
#[test]
fn test_bip353_address_parsing_errors() {
assert!(Bip353Address::from_str("invalid").is_err());
assert!(Bip353Address::from_str("@example.com").is_err());
assert!(Bip353Address::from_str("user@").is_err());
assert!(Bip353Address::from_str("user@domain@extra").is_err());
assert!(Bip353Address::from_str("").is_err());
}
#[test]
fn test_bip353_address_with_subdomain() {
let addr = Bip353Address::from_str("alice@sub.domain.co.uk").unwrap();
assert_eq!(addr.user, "alice");
assert_eq!(addr.domain, "sub.domain.co.uk");
assert_eq!(addr.to_string(), "alice@sub.domain.co.uk");
}
}