Bladeren bron

Add `resolve_dns_txt` to HttpTransport and MintConnector (#1068)

* Add `resolve_dns_txt` to HttpTransport and MintConnector

Fixes #1036

* Use `hickory_resolver` to resolve DNS entries

* Remote default implementation of methods

* Fix build for wasm
C 1 maand geleden
bovenliggende
commit
c3c8e87164

+ 1 - 1
crates/cdk-ffi/src/wallet.rs

@@ -404,7 +404,7 @@ impl Wallet {
 }
 
 /// BIP353 methods for Wallet
-#[cfg(feature = "bip353")]
+#[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
 #[uniffi::export(async_runtime = "tokio")]
 impl Wallet {
     /// Get a quote for a BIP353 melt

+ 1 - 2
crates/cdk-integration-tests/Cargo.toml

@@ -20,7 +20,7 @@ rand.workspace = true
 bip39 = { workspace = true, features = ["rand"] }
 anyhow.workspace = true
 cashu = { workspace = true, features = ["mint", "wallet"] }
-cdk = { workspace = true, features = ["mint", "wallet", "auth"] }
+cdk = { workspace = true, features = ["mint", "wallet", "auth", "bip353"] }
 cdk-cln = { workspace = true }
 cdk-lnd = { workspace = true }
 cdk-ldk-node = { workspace = true }
@@ -63,7 +63,6 @@ uuid = { workspace = true, features = ["js"] }
 [dev-dependencies]
 bip39 = { workspace = true, features = ["rand"] }
 anyhow.workspace = true
-cdk = { workspace = true, features = ["mint", "wallet"] }
 cdk-axum = { workspace = true }
 cdk-fake-wallet = { workspace = true }
 tower-http = { workspace = true, features = ["cors"] }

+ 4 - 0
crates/cdk-integration-tests/src/init_pure_tests.rs

@@ -55,6 +55,10 @@ impl Debug for DirectMintConnection {
 /// Convert the requests and responses between the [String] and [Uuid] variants as necessary.
 #[async_trait]
 impl MintConnector for DirectMintConnection {
+    async fn resolve_dns_txt(&self, _domain: &str) -> Result<Vec<String>, Error> {
+        panic!("Not implemented");
+    }
+
     async fn get_mint_keys(&self) -> Result<Vec<KeySet>, Error> {
         Ok(self.mint.pubkeys().keysets)
     }

+ 3 - 3
crates/cdk/Cargo.toml

@@ -11,12 +11,12 @@ license.workspace = true
 
 
 [features]
-default = ["mint", "wallet", "auth", "nostr"]
+default = ["mint", "wallet", "auth", "nostr", "bip353"]
 wallet = ["dep:futures", "dep:reqwest", "cdk-common/wallet", "dep:rustls"]
 nostr = ["wallet", "dep:nostr-sdk"]
 mint = ["dep:futures", "dep:reqwest", "cdk-common/mint", "cdk-signatory"]
 auth = ["dep:jsonwebtoken", "cdk-common/auth", "cdk-common/auth"]
-bip353 = ["dep:trust-dns-resolver"]
+bip353 = ["dep:hickory-resolver"]
 # We do not commit to a MSRV with swagger enabled
 swagger = ["mint", "dep:utoipa", "cdk-common/swagger"]
 bench = []
@@ -44,7 +44,6 @@ url.workspace = true
 utoipa = { workspace = true, optional = true }
 uuid.workspace = true
 jsonwebtoken = { workspace = true, optional = true }
-trust-dns-resolver = { version = "0.23.2", optional = true }
 nostr-sdk = { optional = true, version = "0.43.0", default-features = false, features = [
     "nip04",
     "nip44",
@@ -60,6 +59,7 @@ zeroize = "1"
 tokio-util.workspace = true
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
+hickory-resolver = { version = "0.25.2", optional = true, features = ["dnssec-ring"] }
 tokio = { workspace = true, features = [
     "rt-multi-thread",
     "time",

+ 5 - 0
crates/cdk/examples/mint-token-bolt12-with-custom-http.rs

@@ -40,6 +40,11 @@ impl Default for CustomHttp {
 #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
 #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
 impl HttpTransport for CustomHttp {
+    #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
+    async fn resolve_dns_txt(&self, _domain: &str) -> Result<Vec<String>, Error> {
+        panic!("Not supported");
+    }
+
     fn with_proxy(
         &mut self,
         _proxy: Url,

+ 13 - 28
crates/cdk/src/bip353.rs

@@ -6,10 +6,11 @@
 
 use std::collections::HashMap;
 use std::str::FromStr;
+use std::sync::Arc;
 
 use anyhow::{bail, Result};
-use trust_dns_resolver::config::{ResolverConfig, ResolverOpts};
-use trust_dns_resolver::TokioAsyncResolver;
+
+use crate::wallet::MintConnector;
 
 /// BIP-353 human-readable Bitcoin address
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -37,35 +38,19 @@ impl Bip353Address {
     /// - 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> {
+    pub(crate) async fn resolve(
+        self,
+        client: &Arc<dyn MintConnector + Send + Sync>,
+    ) -> 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);
-            }
-        }
+        let bitcoin_uris = client
+            .resolve_dns_txt(&dns_name)
+            .await?
+            .into_iter()
+            .filter(|txt_data| txt_data.to_lowercase().starts_with("bitcoin:"))
+            .collect::<Vec<_>>();
 
         // BIP-353 requires exactly one Bitcoin URI
         match bitcoin_uris.len() {

+ 1 - 1
crates/cdk/src/lib.rs

@@ -22,7 +22,7 @@ pub mod mint;
 #[cfg(feature = "wallet")]
 pub mod wallet;
 
-#[cfg(feature = "bip353")]
+#[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
 mod bip353;
 
 #[cfg(all(any(feature = "wallet", feature = "mint"), feature = "auth"))]

+ 3 - 3
crates/cdk/src/wallet/melt/melt_bip353.rs

@@ -7,7 +7,7 @@ use std::str::FromStr;
 use cdk_common::wallet::MeltQuote;
 use tracing::instrument;
 
-#[cfg(feature = "bip353")]
+#[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
 use crate::bip353::{Bip353Address, PaymentType};
 use crate::nuts::MeltOptions;
 use crate::{Amount, Error, Wallet};
@@ -47,7 +47,7 @@ impl Wallet {
     /// # Ok(())
     /// # }
     /// ```
-    #[cfg(feature = "bip353")]
+    #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
     #[instrument(skip(self, amount_msat), fields(address = %bip353_address))]
     pub async fn melt_bip353_quote(
         &self,
@@ -66,7 +66,7 @@ impl Wallet {
         let address_string = address.to_string();
 
         // Resolve the address to get payment instructions
-        let payment_instructions = address.resolve().await.map_err(|e| {
+        let payment_instructions = address.resolve(&self.client).await.map_err(|e| {
             tracing::error!(
                 "Failed to resolve BIP353 address '{}': {}",
                 address_string,

+ 1 - 1
crates/cdk/src/wallet/melt/mod.rs

@@ -7,7 +7,7 @@ use tracing::instrument;
 
 use crate::Wallet;
 
-#[cfg(feature = "bip353")]
+#[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
 mod melt_bip353;
 mod melt_bolt11;
 mod melt_bolt12;

+ 6 - 0
crates/cdk/src/wallet/mint_connector/http_client.rs

@@ -187,6 +187,12 @@ impl<T> MintConnector for HttpClient<T>
 where
     T: Transport + Send + Sync + 'static,
 {
+    #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
+    #[instrument(skip(self), fields(mint_url = %self.mint_url))]
+    async fn resolve_dns_txt(&self, domain: &str) -> Result<Vec<String>, Error> {
+        self.transport.resolve_dns_txt(domain).await
+    }
+
     /// Get Active Mint Keys [NUT-01]
     #[instrument(skip(self), fields(mint_url = %self.mint_url))]
     async fn get_mint_keys(&self) -> Result<Vec<KeySet>, Error> {

+ 4 - 0
crates/cdk/src/wallet/mint_connector/mod.rs

@@ -28,6 +28,10 @@ pub type HttpClient = http_client::HttpClient<transport::Async>;
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 pub trait MintConnector: Debug {
+    #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
+    /// Resolve the DNS record getting the TXT value
+    async fn resolve_dns_txt(&self, _domain: &str) -> Result<Vec<String>, Error>;
+
     /// Get Active Mint Keys [NUT-01]
     async fn get_mint_keys(&self) -> Result<Vec<KeySet>, Error>;
     /// Get Keyset Keys [NUT-01]

+ 34 - 0
crates/cdk/src/wallet/mint_connector/transport.rs

@@ -2,6 +2,12 @@
 use std::fmt::Debug;
 
 use cdk_common::AuthToken;
+#[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
+use hickory_resolver::config::ResolverConfig;
+#[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
+use hickory_resolver::name_server::TokioConnectionProvider;
+#[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
+use hickory_resolver::Resolver;
 use reqwest::Client;
 use serde::de::DeserializeOwned;
 use serde::Serialize;
@@ -14,6 +20,10 @@ use crate::error::ErrorResponse;
 #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
 #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
 pub trait Transport: Default + Send + Sync + Debug + Clone {
+    #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
+    /// DNS resolver to get a TXT record from a domain name
+    async fn resolve_dns_txt(&self, _domain: &str) -> Result<Vec<String>, Error>;
+
     /// Make the transport to use a given proxy
     fn with_proxy(
         &mut self,
@@ -102,6 +112,30 @@ impl Transport for Async {
         Ok(())
     }
 
+    /// DNS resolver to get a TXT record from a domain name
+    #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
+    async fn resolve_dns_txt(&self, domain: &str) -> Result<Vec<String>, Error> {
+        let resolver = Resolver::builder_with_config(
+            ResolverConfig::default(),
+            TokioConnectionProvider::default(),
+        )
+        .build();
+
+        Ok(resolver
+            .txt_lookup(domain)
+            .await
+            .map_err(|e| Error::Custom(e.to_string()))?
+            .into_iter()
+            .map(|txt| {
+                txt.txt_data()
+                    .iter()
+                    .map(|bytes| String::from_utf8_lossy(bytes).into_owned())
+                    .collect::<Vec<_>>()
+                    .join("")
+            })
+            .collect::<Vec<_>>())
+    }
+
     async fn http_get<R>(&self, url: Url, auth: Option<AuthToken>) -> Result<R, Error>
     where
         R: DeserializeOwned,