bip353.rs 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. use std::collections::HashMap;
  2. use std::str::FromStr;
  3. use anyhow::{bail, Result};
  4. use trust_dns_resolver::config::{ResolverConfig, ResolverOpts};
  5. use trust_dns_resolver::TokioAsyncResolver;
  6. #[derive(Debug, Clone, PartialEq, Eq, Hash)]
  7. pub struct Bip353Address {
  8. pub user: String,
  9. pub domain: String,
  10. }
  11. impl Bip353Address {
  12. /// Resolve a human-readable Bitcoin address
  13. pub async fn resolve(self) -> Result<PaymentInstruction> {
  14. // Construct DNS name
  15. let dns_name = format!("{}.user._bitcoin-payment.{}", self.user, self.domain);
  16. // Create a new resolver with DNSSEC validation
  17. let mut opts = ResolverOpts::default();
  18. opts.validate = true; // Enable DNSSEC validation
  19. let resolver = TokioAsyncResolver::tokio(ResolverConfig::default(), opts);
  20. // Query TXT records - with opts.validate=true, this will fail if DNSSEC validation fails
  21. let response = resolver.txt_lookup(&dns_name).await?;
  22. // Extract and concatenate TXT record strings
  23. let mut bitcoin_uris = Vec::new();
  24. for txt in response.iter() {
  25. let txt_data: Vec<String> = txt
  26. .txt_data()
  27. .iter()
  28. .map(|bytes| String::from_utf8_lossy(bytes).into_owned())
  29. .collect();
  30. let concatenated = txt_data.join("");
  31. if concatenated.to_lowercase().starts_with("bitcoin:") {
  32. bitcoin_uris.push(concatenated);
  33. }
  34. }
  35. // BIP-353 requires exactly one Bitcoin URI
  36. match bitcoin_uris.len() {
  37. 0 => bail!("No Bitcoin URI found"),
  38. 1 => PaymentInstruction::from_uri(&bitcoin_uris[0]),
  39. _ => bail!("Multiple Bitcoin URIs found"),
  40. }
  41. }
  42. }
  43. impl FromStr for Bip353Address {
  44. type Err = anyhow::Error;
  45. /// Parse a human-readable Bitcoin address
  46. fn from_str(address: &str) -> Result<Self, Self::Err> {
  47. let addr = address.trim();
  48. // Remove Bitcoin prefix if present
  49. let addr = addr.strip_prefix("₿").unwrap_or(addr);
  50. // Split by @
  51. let parts: Vec<&str> = addr.split('@').collect();
  52. if parts.len() != 2 {
  53. bail!("Address is not formatted correctly")
  54. }
  55. let user = parts[0].trim();
  56. let domain = parts[1].trim();
  57. if user.is_empty() || domain.is_empty() {
  58. bail!("User name and domain must not be empty")
  59. }
  60. Ok(Self {
  61. user: user.to_string(),
  62. domain: domain.to_string(),
  63. })
  64. }
  65. }
  66. /// Payment instruction type
  67. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
  68. pub enum PaymentType {
  69. OnChain,
  70. LightningOffer,
  71. }
  72. /// BIP-353 payment instruction
  73. #[derive(Debug, Clone)]
  74. pub struct PaymentInstruction {
  75. pub parameters: HashMap<PaymentType, String>,
  76. }
  77. impl PaymentInstruction {
  78. /// Parse a payment instruction from a Bitcoin URI
  79. pub fn from_uri(uri: &str) -> Result<Self> {
  80. if !uri.to_lowercase().starts_with("bitcoin:") {
  81. bail!("URI must start with 'bitcoin:'")
  82. }
  83. let mut parameters = HashMap::new();
  84. // Parse URI parameters
  85. if let Some(query_start) = uri.find('?') {
  86. let query = &uri[query_start + 1..];
  87. for pair in query.split('&') {
  88. if let Some(eq_pos) = pair.find('=') {
  89. let key = pair[..eq_pos].to_string();
  90. let value = pair[eq_pos + 1..].to_string();
  91. let payment_type;
  92. // Determine payment type
  93. if key.contains("lno") {
  94. payment_type = PaymentType::LightningOffer;
  95. } else if !uri[8..].contains('?') && uri.len() > 8 {
  96. // Simple on-chain address
  97. payment_type = PaymentType::OnChain;
  98. } else {
  99. continue;
  100. }
  101. parameters.insert(payment_type, value);
  102. }
  103. }
  104. }
  105. Ok(PaymentInstruction { parameters })
  106. }
  107. }