| 
					
				 | 
			
			
				@@ -0,0 +1,286 @@ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+//! BIP-353: Human Readable Bitcoin Payment Instructions 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+//! 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+//! This module provides functionality for resolving human-readable Bitcoin addresses 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+//! according to BIP-353. It allows users to share simple email-like addresses such as 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+//! `user@domain.com` instead of complex Bitcoin addresses or Lightning invoices. 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+use std::collections::HashMap; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+use std::str::FromStr; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+use anyhow::{bail, Result}; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+use trust_dns_resolver::config::{ResolverConfig, ResolverOpts}; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+use trust_dns_resolver::TokioAsyncResolver; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+/// BIP-353 human-readable Bitcoin address 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+#[derive(Debug, Clone, PartialEq, Eq, Hash)] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+pub struct Bip353Address { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// The user part of the address (before @) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    pub user: String, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// The domain part of the address (after @) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    pub domain: String, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+impl Bip353Address { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// Resolve a human-readable Bitcoin address to payment instructions 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// This method performs the following steps: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// 1. Constructs the DNS name according to BIP-353 format 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// 2. Queries TXT records with DNSSEC validation 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// 3. Extracts Bitcoin URIs from the records 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// 4. Parses the URIs into payment instructions 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// # Errors 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// This method will return an error if: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// - DNS resolution fails 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// - DNSSEC validation fails 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// - No Bitcoin URI is found 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// - Multiple Bitcoin URIs are found (BIP-353 requires exactly one) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// - The URI format is invalid 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    pub(crate) async fn resolve(self) -> Result<PaymentInstruction> { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        // Construct DNS name according to BIP-353 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let dns_name = format!("{}.user._bitcoin-payment.{}", self.user, self.domain); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        // Create a new resolver with DNSSEC validation 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let mut opts = ResolverOpts::default(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        opts.validate = true; // Enable DNSSEC validation 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let resolver = TokioAsyncResolver::tokio(ResolverConfig::default(), opts); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        // Query TXT records - with opts.validate=true, this will fail if DNSSEC validation fails 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let response = resolver.txt_lookup(&dns_name).await?; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        // Extract and concatenate TXT record strings 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let mut bitcoin_uris = Vec::new(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        for txt in response.iter() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            let txt_data: Vec<String> = txt 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                .txt_data() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                .iter() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                .map(|bytes| String::from_utf8_lossy(bytes).into_owned()) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                .collect(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            let concatenated = txt_data.join(""); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            if concatenated.to_lowercase().starts_with("bitcoin:") { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                bitcoin_uris.push(concatenated); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        // BIP-353 requires exactly one Bitcoin URI 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        match bitcoin_uris.len() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            0 => bail!("No Bitcoin URI found"), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            1 => PaymentInstruction::from_uri(&bitcoin_uris[0]), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            _ => bail!("Multiple Bitcoin URIs found"), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+impl FromStr for Bip353Address { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    type Err = anyhow::Error; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// Parse a human-readable Bitcoin address from string format 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// Accepts formats: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// - `user@domain.com` 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// - `₿user@domain.com` (with Bitcoin symbol prefix) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// # Errors 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// Returns an error if: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// - The format is not `user@domain` 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// - User or domain parts are empty 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    fn from_str(address: &str) -> Result<Self, Self::Err> { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let addr = address.trim(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        // Remove Bitcoin prefix if present 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let addr = addr.strip_prefix("₿").unwrap_or(addr); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        // Split by @ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let parts: Vec<&str> = addr.split('@').collect(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if parts.len() != 2 { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            bail!("Address is not formatted correctly") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let user = parts[0].trim(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let domain = parts[1].trim(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if user.is_empty() || domain.is_empty() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            bail!("User name and domain must not be empty") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        Ok(Self { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            user: user.to_string(), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            domain: domain.to_string(), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        }) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+impl std::fmt::Display for Bip353Address { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        write!(f, "{}@{}", self.user, self.domain) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+/// Payment instruction type 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+pub enum PaymentType { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// On-chain Bitcoin address 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    OnChain, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// Lightning Offer (BOLT12) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    LightningOffer, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+/// BIP-353 payment instruction containing parsed payment methods 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+#[derive(Debug, Clone)] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+pub struct PaymentInstruction { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// Map of payment types to their corresponding values 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    pub parameters: HashMap<PaymentType, String>, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+impl PaymentInstruction { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// Create a new empty payment instruction 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    pub fn new() -> Self { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        Self { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            parameters: HashMap::new(), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// Parse a payment instruction from a Bitcoin URI 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// Extracts various payment methods from the URI: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// - Lightning offers (parameters containing "lno") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// - On-chain addresses (address part of the URI) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// # Errors 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// Returns an error if the URI doesn't start with "bitcoin:" 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    pub fn from_uri(uri: &str) -> Result<Self> { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if !uri.to_lowercase().starts_with("bitcoin:") { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            bail!("URI must start with 'bitcoin:'") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let mut parameters = HashMap::new(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        // Parse URI parameters 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if let Some(query_start) = uri.find('?') { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            let query = &uri[query_start + 1..]; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            for pair in query.split('&') { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                if let Some(eq_pos) = pair.find('=') { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                    let key = pair[..eq_pos].to_string(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                    let value = pair[eq_pos + 1..].to_string(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                    // Determine payment type based on parameter key 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                    if key.contains("lno") { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                        parameters.insert(PaymentType::LightningOffer, value); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                    // Could add more payment types here as needed 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        // Check if we have an on-chain address (address part after bitcoin:) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if let Some(query_start) = uri.find('?') { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            let addr_part = &uri[8..query_start]; // Skip "bitcoin:" 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            if !addr_part.is_empty() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                parameters.insert(PaymentType::OnChain, addr_part.to_string()); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } else { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            // No query parameters, check if there's just an address 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            let addr_part = &uri[8..]; // Skip "bitcoin:" 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            if !addr_part.is_empty() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                parameters.insert(PaymentType::OnChain, addr_part.to_string()); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        Ok(PaymentInstruction { parameters }) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /// Get a payment method by type 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    pub fn get(&self, payment_type: &PaymentType) -> Option<&String> { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self.parameters.get(payment_type) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+impl Default for PaymentInstruction { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    fn default() -> Self { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        Self::new() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+#[cfg(test)] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+mod tests { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    use super::*; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    impl PaymentInstruction { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        /// Check if a payment type is available 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        pub fn has_payment_type(&self, payment_type: &PaymentType) -> bool { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            self.parameters.contains_key(payment_type) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    #[test] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    fn test_bip353_address_parsing() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        // Test basic parsing 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let addr = Bip353Address::from_str("alice@example.com").unwrap(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        assert_eq!(addr.user, "alice"); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        assert_eq!(addr.domain, "example.com"); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        // Test with Bitcoin symbol 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let addr = Bip353Address::from_str("₿bob@bitcoin.org").unwrap(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        assert_eq!(addr.user, "bob"); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        assert_eq!(addr.domain, "bitcoin.org"); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        // Test with whitespace 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let addr = Bip353Address::from_str("  charlie@test.net  ").unwrap(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        assert_eq!(addr.user, "charlie"); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        assert_eq!(addr.domain, "test.net"); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        // Test display 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        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() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        // Test invalid formats 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        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_payment_instruction_parsing() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        // Test Lightning offer URI 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let uri = "bitcoin:?lno=lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pxqrjszs5v2a5m5xwc4mxv6rdjdcn2d3kxccnjdgecf7fz3rf5g4t7gdxhkzm8mpsq5q"; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let instruction = PaymentInstruction::from_uri(uri).unwrap(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        assert!(instruction.has_payment_type(&PaymentType::LightningOffer)); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        // Test on-chain address URI 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let uri = "bitcoin:bc1qexampleaddress"; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let instruction = PaymentInstruction::from_uri(uri).unwrap(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        assert!(instruction.has_payment_type(&PaymentType::OnChain)); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        assert_eq!( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            instruction.get(&PaymentType::OnChain).unwrap(), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            "bc1qexampleaddress" 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        ); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        // Test combined URI 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let uri = "bitcoin:bc1qexampleaddress?lno=lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pxqrjszs5v2a5m5xwc4mxv6rdjdcn2d3kxccnjdgecf7fz3rf5g4t7gdxhkzm8mpsq5q"; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let instruction = PaymentInstruction::from_uri(uri).unwrap(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        assert!(instruction.has_payment_type(&PaymentType::OnChain)); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        assert!(instruction.has_payment_type(&PaymentType::LightningOffer)); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    #[test] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    fn test_payment_instruction_errors() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        // Test invalid URI 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        assert!(PaymentInstruction::from_uri("invalid:uri").is_err()); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        assert!(PaymentInstruction::from_uri("").is_err()); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 |