Selaa lähdekoodia

Abstract the HTTP Transport (#1012)

* Abstract the HTTP Transport

This PR allows replacing the HTTP transport layer with another library,
allowing wallet ffi to provide a better-suited HTTP library that would be used
instead of Reqwest.
C 2 kuukautta sitten
vanhempi
säilyke
2131f89068

+ 1 - 1
crates/cashu/src/nuts/nut18/payment_request.rs

@@ -288,7 +288,7 @@ mod tests {
         assert_eq!(request.unit.clone().unwrap(), CurrencyUnit::Sat);
         assert_eq!(request.mints.clone().unwrap(), vec![mint_url]);
 
-        let t = request.transports.first().clone().unwrap();
+        let t = request.transports.first().unwrap();
         assert_eq!(&transport, t);
 
         // Test serialization and deserialization

+ 5 - 5
crates/cdk-integration-tests/tests/bolt12.rs

@@ -120,7 +120,7 @@ async fn test_regtest_bolt12_mint_multiple() -> Result<()> {
             mint_quote.clone(),
             SplitTarget::default(),
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         .await?;
 
@@ -136,7 +136,7 @@ async fn test_regtest_bolt12_mint_multiple() -> Result<()> {
             mint_quote.clone(),
             SplitTarget::default(),
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         .await?;
 
@@ -187,7 +187,7 @@ async fn test_regtest_bolt12_multiple_wallets() -> Result<()> {
             quote_one.clone(),
             SplitTarget::default(),
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         .await?;
 
@@ -206,7 +206,7 @@ async fn test_regtest_bolt12_multiple_wallets() -> Result<()> {
             quote_two.clone(),
             SplitTarget::default(),
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         .await?;
 
@@ -283,7 +283,7 @@ async fn test_regtest_bolt12_melt() -> Result<()> {
             mint_quote.clone(),
             SplitTarget::default(),
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         .await?;
 

+ 1 - 1
crates/cdk-integration-tests/tests/fake_auth.rs

@@ -336,7 +336,7 @@ async fn test_mint_with_auth() {
             quote.clone(),
             SplitTarget::default(),
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         .await
         .expect("payment");

+ 5 - 5
crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs

@@ -114,7 +114,7 @@ async fn test_happy_mint_melt_round_trip() {
             mint_quote.clone(),
             SplitTarget::default(),
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         .await
         .expect("payment");
@@ -236,7 +236,7 @@ async fn test_happy_mint() {
             mint_quote.clone(),
             SplitTarget::default(),
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         .await
         .expect("payment");
@@ -284,7 +284,7 @@ async fn test_restore() {
             mint_quote.clone(),
             SplitTarget::default(),
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         .await
         .expect("payment");
@@ -364,7 +364,7 @@ async fn test_fake_melt_change_in_quote() {
             mint_quote.clone(),
             SplitTarget::default(),
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         .await
         .expect("payment");
@@ -434,7 +434,7 @@ async fn test_pay_invoice_twice() {
             mint_quote.clone(),
             SplitTarget::default(),
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         .await
         .expect("payment");

+ 2 - 2
crates/cdk-integration-tests/tests/ldk_node.rs

@@ -10,7 +10,7 @@ async fn test_ldk_node_mint_info() -> Result<()> {
     let client = reqwest::Client::new();
 
     // Make a request to the info endpoint
-    let response = client.get(&format!("{}/v1/info", mint_url)).send().await?;
+    let response = client.get(format!("{}/v1/info", mint_url)).send().await?;
 
     // Check that we got a successful response
     assert_eq!(response.status(), 200);
@@ -44,7 +44,7 @@ async fn test_ldk_node_mint_quote() -> Result<()> {
 
     // Make a request to create a mint quote
     let response = client
-        .post(&format!("{}/v1/mint/quote/bolt11", mint_url))
+        .post(format!("{}/v1/mint/quote/bolt11", mint_url))
         .json(&quote_request)
         .send()
         .await?;

+ 4 - 4
crates/cdk-integration-tests/tests/regtest.rs

@@ -56,7 +56,7 @@ async fn test_internal_payment() {
             mint_quote.clone(),
             SplitTarget::default(),
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         .await
         .expect("payment");
@@ -88,7 +88,7 @@ async fn test_internal_payment() {
             mint_quote.clone(),
             SplitTarget::default(),
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         .await
         .expect("payment");
@@ -236,7 +236,7 @@ async fn test_multimint_melt() {
             quote.clone(),
             SplitTarget::default(),
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         .await
         .expect("payment");
@@ -252,7 +252,7 @@ async fn test_multimint_melt() {
             quote.clone(),
             SplitTarget::default(),
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         .await
         .expect("payment");

+ 2 - 2
crates/cdk-integration-tests/tests/test_fees.rs

@@ -32,7 +32,7 @@ async fn test_swap() {
             mint_quote.clone(),
             SplitTarget::default(),
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         .await
         .expect("payment");
@@ -92,7 +92,7 @@ async fn test_fake_melt_change_in_quote() {
             mint_quote.clone(),
             SplitTarget::default(),
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         .await
         .expect("payment");

+ 6 - 0
crates/cdk/Cargo.toml

@@ -106,6 +106,11 @@ required-features = ["wallet", "bip353"]
 [[example]]
 name = "mint-token-bolt12-with-stream"
 required-features = ["wallet"]
+
+[[example]]
+name = "mint-token-bolt12-with-custom-http"
+required-features = ["wallet"]
+
 [[example]]
 name = "mint-token-bolt12"
 required-features = ["wallet"]
@@ -118,6 +123,7 @@ tracing-subscriber.workspace = true
 criterion = "0.6.0"
 reqwest = { workspace = true }
 anyhow.workspace = true
+ureq = { version = "3.1.0", features = ["json"] }
 
 
 [[bench]]

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

@@ -0,0 +1,161 @@
+use std::str::FromStr;
+use std::sync::Arc;
+use std::time::Duration;
+
+use cdk::error::Error;
+use cdk::nuts::nut00::ProofsMethods;
+use cdk::nuts::CurrencyUnit;
+use cdk::wallet::{BaseHttpClient, HttpTransport, SendOptions, WalletBuilder};
+use cdk::{Amount, StreamExt};
+use cdk_common::mint_url::MintUrl;
+use cdk_common::AuthToken;
+use cdk_sqlite::wallet::memory;
+use rand::random;
+use serde::de::DeserializeOwned;
+use serde::Serialize;
+use tracing_subscriber::EnvFilter;
+use ureq::config::Config;
+use ureq::Agent;
+use url::Url;
+
+#[derive(Debug, Clone)]
+pub struct CustomHttp {
+    agent: Agent,
+}
+
+impl Default for CustomHttp {
+    fn default() -> Self {
+        Self {
+            agent: Agent::new_with_config(
+                Config::builder()
+                    .timeout_global(Some(Duration::from_secs(5)))
+                    .no_delay(true)
+                    .user_agent("Custom HTTP Transport")
+                    .build(),
+            ),
+        }
+    }
+}
+
+#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
+impl HttpTransport for CustomHttp {
+    fn with_proxy(
+        &mut self,
+        _proxy: Url,
+        _host_matcher: Option<&str>,
+        _accept_invalid_certs: bool,
+    ) -> Result<(), Error> {
+        panic!("Not supported");
+    }
+
+    async fn http_get<R>(&self, url: Url, _auth: Option<AuthToken>) -> Result<R, Error>
+    where
+        R: DeserializeOwned,
+    {
+        self.agent
+            .get(url.as_str())
+            .call()
+            .map_err(|e| Error::HttpError(None, e.to_string()))?
+            .body_mut()
+            .read_json()
+            .map_err(|e| Error::HttpError(None, e.to_string()))
+    }
+
+    /// HTTP Post request
+    async fn http_post<P, R>(
+        &self,
+        url: Url,
+        _auth_token: Option<AuthToken>,
+        payload: &P,
+    ) -> Result<R, Error>
+    where
+        P: Serialize + ?Sized + Send + Sync,
+        R: DeserializeOwned,
+    {
+        self.agent
+            .post(url.as_str())
+            .send_json(payload)
+            .map_err(|e| Error::HttpError(None, e.to_string()))?
+            .body_mut()
+            .read_json()
+            .map_err(|e| Error::HttpError(None, e.to_string()))
+    }
+}
+
+type CustomConnector = BaseHttpClient<CustomHttp>;
+
+#[tokio::main]
+async fn main() -> Result<(), Error> {
+    let default_filter = "debug";
+
+    let sqlx_filter = "sqlx=warn,hyper_util=warn,reqwest=warn,rustls=warn";
+
+    let env_filter = EnvFilter::new(format!("{},{}", default_filter, sqlx_filter));
+
+    // Parse input
+    tracing_subscriber::fmt().with_env_filter(env_filter).init();
+
+    // Initialize the memory store for the wallet
+    let localstore = Arc::new(memory::empty().await?);
+
+    // Generate a random seed for the wallet
+    let seed = random::<[u8; 64]>();
+
+    // Define the mint URL and currency unit
+    let mint_url = "https://fake.thesimplekid.dev";
+    let unit = CurrencyUnit::Sat;
+    let amount = Amount::from(10);
+
+    let mint_url = MintUrl::from_str(mint_url)?;
+    #[cfg(feature = "auth")]
+    let http_client = CustomConnector::new(mint_url.clone(), None);
+
+    #[cfg(not(feature = "auth"))]
+    let http_client = CustomConnector::new(mint_url.clone());
+
+    // Create a new wallet
+    let wallet = WalletBuilder::new()
+        .mint_url(mint_url)
+        .unit(unit)
+        .localstore(localstore)
+        .seed(seed)
+        .target_proof_count(3)
+        .client(http_client)
+        .build()?;
+
+    let quotes = vec![
+        wallet.mint_bolt12_quote(None, None).await?,
+        wallet.mint_bolt12_quote(None, None).await?,
+        wallet.mint_bolt12_quote(None, None).await?,
+    ];
+
+    let mut stream = wallet.mints_proof_stream(quotes, Default::default(), None);
+
+    let stop = stream.get_cancel_token();
+
+    let mut processed = 0;
+
+    while let Some(proofs) = stream.next().await {
+        let (mint_quote, proofs) = proofs?;
+
+        // Mint the received amount
+        let receive_amount = proofs.total_amount()?;
+        tracing::info!("Received {} from mint {}", receive_amount, mint_quote.id);
+
+        // Send a token with the specified amount
+        let prepared_send = wallet.prepare_send(amount, SendOptions::default()).await?;
+        let token = prepared_send.confirm(None).await?;
+        tracing::info!("Token: {}", token);
+
+        processed += 1;
+
+        if processed == 3 {
+            stop.cancel()
+        }
+    }
+
+    tracing::info!("Stopped the loop after {} quotes being minted", processed);
+
+    Ok(())
+}

+ 60 - 156
crates/cdk/src/wallet/mint_connector/http_client.rs

@@ -1,3 +1,4 @@
+//! HTTP Mint client with pluggable transport
 use std::collections::HashSet;
 use std::sync::{Arc, RwLock as StdRwLock};
 use std::time::{Duration, Instant};
@@ -6,17 +7,15 @@ use async_trait::async_trait;
 use cdk_common::{nut19, MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};
 #[cfg(feature = "auth")]
 use cdk_common::{Method, ProtectedEndpoint, RoutePath};
-use reqwest::{Client, IntoUrl};
 use serde::de::DeserializeOwned;
 use serde::Serialize;
 #[cfg(feature = "auth")]
 use tokio::sync::RwLock;
 use tracing::instrument;
-#[cfg(not(target_arch = "wasm32"))]
 use url::Url;
 
+use super::transport::Transport;
 use super::{Error, MintConnector};
-use crate::error::ErrorResponse;
 use crate::mint_url::MintUrl;
 #[cfg(feature = "auth")]
 use crate::nuts::nut22::MintAuthRequest;
@@ -29,119 +28,30 @@ use crate::nuts::{
 #[cfg(feature = "auth")]
 use crate::wallet::auth::{AuthMintConnector, AuthWallet};
 
-#[derive(Debug, Clone)]
-struct HttpClientCore {
-    inner: Client,
-}
-
-impl HttpClientCore {
-    fn new() -> Self {
-        #[cfg(not(target_arch = "wasm32"))]
-        if rustls::crypto::CryptoProvider::get_default().is_none() {
-            let _ = rustls::crypto::ring::default_provider().install_default();
-        }
-
-        Self {
-            inner: Client::new(),
-        }
-    }
-
-    fn client(&self) -> &Client {
-        &self.inner
-    }
-
-    async fn http_get<U: IntoUrl + Send, R: DeserializeOwned>(
-        &self,
-        url: U,
-        auth: Option<AuthToken>,
-    ) -> Result<R, Error> {
-        let mut request = self.client().get(url);
-
-        if let Some(auth) = auth {
-            request = request.header(auth.header_key(), auth.to_string());
-        }
-
-        let response = request
-            .send()
-            .await
-            .map_err(|e| {
-                Error::HttpError(
-                    e.status().map(|status_code| status_code.as_u16()),
-                    e.to_string(),
-                )
-            })?
-            .text()
-            .await
-            .map_err(|e| {
-                Error::HttpError(
-                    e.status().map(|status_code| status_code.as_u16()),
-                    e.to_string(),
-                )
-            })?;
-
-        serde_json::from_str::<R>(&response).map_err(|err| {
-            tracing::warn!("Http Response error: {}", err);
-            match ErrorResponse::from_json(&response) {
-                Ok(ok) => <ErrorResponse as Into<Error>>::into(ok),
-                Err(err) => err.into(),
-            }
-        })
-    }
-
-    async fn http_post<U: IntoUrl + Send, P: Serialize + ?Sized, R: DeserializeOwned>(
-        &self,
-        url: U,
-        auth_token: Option<AuthToken>,
-        payload: &P,
-    ) -> Result<R, Error> {
-        let mut request = self.client().post(url).json(&payload);
-
-        if let Some(auth) = auth_token {
-            request = request.header(auth.header_key(), auth.to_string());
-        }
-
-        let response = request.send().await.map_err(|e| {
-            Error::HttpError(
-                e.status().map(|status_code| status_code.as_u16()),
-                e.to_string(),
-            )
-        })?;
-
-        let response = response.text().await.map_err(|e| {
-            Error::HttpError(
-                e.status().map(|status_code| status_code.as_u16()),
-                e.to_string(),
-            )
-        })?;
-
-        serde_json::from_str::<R>(&response).map_err(|err| {
-            tracing::warn!("Http Response error: {}", err);
-            match ErrorResponse::from_json(&response) {
-                Ok(ok) => <ErrorResponse as Into<Error>>::into(ok),
-                Err(err) => err.into(),
-            }
-        })
-    }
-}
-
 type Cache = (u64, HashSet<(nut19::Method, nut19::Path)>);
 
 /// Http Client
 #[derive(Debug, Clone)]
-pub struct HttpClient {
-    core: HttpClientCore,
+pub struct HttpClient<T>
+where
+    T: Transport + Send + Sync + 'static,
+{
+    transport: Arc<T>,
     mint_url: MintUrl,
     cache_support: Arc<StdRwLock<Cache>>,
     #[cfg(feature = "auth")]
     auth_wallet: Arc<RwLock<Option<AuthWallet>>>,
 }
 
-impl HttpClient {
+impl<T> HttpClient<T>
+where
+    T: Transport + Send + Sync + 'static,
+{
     /// Create new [`HttpClient`]
     #[cfg(feature = "auth")]
     pub fn new(mint_url: MintUrl, auth_wallet: Option<AuthWallet>) -> Self {
         Self {
-            core: HttpClientCore::new(),
+            transport: T::default().into(),
             mint_url,
             auth_wallet: Arc::new(RwLock::new(auth_wallet)),
             cache_support: Default::default(),
@@ -152,7 +62,7 @@ impl HttpClient {
     /// Create new [`HttpClient`]
     pub fn new(mint_url: MintUrl) -> Self {
         Self {
-            core: HttpClientCore::new(),
+            transport: T::default().into(),
             cache_support: Default::default(),
             mint_url,
         }
@@ -176,7 +86,6 @@ impl HttpClient {
         }
     }
 
-    #[cfg(not(target_arch = "wasm32"))]
     /// Create new [`HttpClient`] with a proxy for specific TLDs.
     /// Specifying `None` for `host_matcher` will use the proxy for all
     /// requests.
@@ -186,32 +95,11 @@ impl HttpClient {
         host_matcher: Option<&str>,
         accept_invalid_certs: bool,
     ) -> Result<Self, Error> {
-        let regex = host_matcher
-            .map(regex::Regex::new)
-            .transpose()
-            .map_err(|e| Error::Custom(e.to_string()))?;
-        let client = reqwest::Client::builder()
-            .proxy(reqwest::Proxy::custom(move |url| {
-                if let Some(matcher) = regex.as_ref() {
-                    if let Some(host) = url.host_str() {
-                        if matcher.is_match(host) {
-                            return Some(proxy.clone());
-                        }
-                    }
-                }
-                None
-            }))
-            .danger_accept_invalid_certs(accept_invalid_certs) // Allow self-signed certs
-            .build()
-            .map_err(|e| {
-                Error::HttpError(
-                    e.status().map(|status_code| status_code.as_u16()),
-                    e.to_string(),
-                )
-            })?;
+        let mut transport = T::default();
+        transport.with_proxy(proxy, host_matcher, accept_invalid_certs)?;
 
         Ok(Self {
-            core: HttpClientCore { inner: client },
+            transport: transport.into(),
             mint_url,
             #[cfg(feature = "auth")]
             auth_wallet: Arc::new(RwLock::new(None)),
@@ -231,7 +119,7 @@ impl HttpClient {
         payload: &P,
     ) -> Result<R, Error>
     where
-        P: Serialize + ?Sized,
+        P: Serialize + ?Sized + Send + Sync,
         R: DeserializeOwned,
     {
         let started = Instant::now();
@@ -259,8 +147,12 @@ impl HttpClient {
             })?;
 
             let result = match method {
-                nut19::Method::Get => self.core.http_get(url, auth_token.clone()).await,
-                nut19::Method::Post => self.core.http_post(url, auth_token.clone(), payload).await,
+                nut19::Method::Get => self.transport.http_get(url, auth_token.clone()).await,
+                nut19::Method::Post => {
+                    self.transport
+                        .http_post(url, auth_token.clone(), payload)
+                        .await
+                }
             };
 
             if result.is_ok() {
@@ -291,15 +183,18 @@ impl HttpClient {
 
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
-impl MintConnector for HttpClient {
+impl<T> MintConnector for HttpClient<T>
+where
+    T: Transport + Send + Sync + 'static,
+{
     /// 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> {
         let url = self.mint_url.join_paths(&["v1", "keys"])?;
 
         Ok(self
-            .core
-            .http_get::<_, KeysResponse>(url, None)
+            .transport
+            .http_get::<KeysResponse>(url, None)
             .await?
             .keysets)
     }
@@ -311,7 +206,7 @@ impl MintConnector for HttpClient {
             .mint_url
             .join_paths(&["v1", "keys", &keyset_id.to_string()])?;
 
-        let keys_response = self.core.http_get::<_, KeysResponse>(url, None).await?;
+        let keys_response = self.transport.http_get::<KeysResponse>(url, None).await?;
 
         Ok(keys_response.keysets.first().unwrap().clone())
     }
@@ -320,7 +215,7 @@ impl MintConnector for HttpClient {
     #[instrument(skip(self), fields(mint_url = %self.mint_url))]
     async fn get_mint_keysets(&self) -> Result<KeysetResponse, Error> {
         let url = self.mint_url.join_paths(&["v1", "keysets"])?;
-        self.core.http_get(url, None).await
+        self.transport.http_get(url, None).await
     }
 
     /// Mint Quote [NUT-04]
@@ -341,7 +236,7 @@ impl MintConnector for HttpClient {
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
 
-        self.core.http_post(url, auth_token, &request).await
+        self.transport.http_post(url, auth_token, &request).await
     }
 
     /// Mint Quote status
@@ -361,7 +256,7 @@ impl MintConnector for HttpClient {
 
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
-        self.core.http_get(url, auth_token).await
+        self.transport.http_get(url, auth_token).await
     }
 
     /// Mint Tokens [NUT-04]
@@ -399,7 +294,7 @@ impl MintConnector for HttpClient {
 
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
-        self.core.http_post(url, auth_token, &request).await
+        self.transport.http_post(url, auth_token, &request).await
     }
 
     /// Melt Quote Status
@@ -419,7 +314,7 @@ impl MintConnector for HttpClient {
 
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
-        self.core.http_get(url, auth_token).await
+        self.transport.http_get(url, auth_token).await
     }
 
     /// Melt [NUT-05]
@@ -467,7 +362,7 @@ impl MintConnector for HttpClient {
     /// Helper to get mint info
     async fn get_mint_info(&self) -> Result<MintInfo, Error> {
         let url = self.mint_url.join_paths(&["v1", "info"])?;
-        let info: MintInfo = self.core.http_get(url, None).await?;
+        let info: MintInfo = self.transport.http_get(url, None).await?;
 
         if let Ok(mut cache_support) = self.cache_support.write() {
             *cache_support = (
@@ -509,7 +404,7 @@ impl MintConnector for HttpClient {
 
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
-        self.core.http_post(url, auth_token, &request).await
+        self.transport.http_post(url, auth_token, &request).await
     }
 
     /// Restore request [NUT-13]
@@ -523,7 +418,7 @@ impl MintConnector for HttpClient {
 
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
-        self.core.http_post(url, auth_token, &request).await
+        self.transport.http_post(url, auth_token, &request).await
     }
 
     /// Mint Quote Bolt12 [NUT-23]
@@ -544,7 +439,7 @@ impl MintConnector for HttpClient {
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
 
-        self.core.http_post(url, auth_token, &request).await
+        self.transport.http_post(url, auth_token, &request).await
     }
 
     /// Mint Quote Bolt12 status
@@ -564,7 +459,7 @@ impl MintConnector for HttpClient {
 
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
-        self.core.http_get(url, auth_token).await
+        self.transport.http_get(url, auth_token).await
     }
 
     /// Melt Quote Bolt12 [NUT-23]
@@ -583,7 +478,7 @@ impl MintConnector for HttpClient {
 
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
-        self.core.http_post(url, auth_token, &request).await
+        self.transport.http_post(url, auth_token, &request).await
     }
 
     /// Melt Quote Bolt12 Status [NUT-23]
@@ -603,7 +498,7 @@ impl MintConnector for HttpClient {
 
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
-        self.core.http_get(url, auth_token).await
+        self.transport.http_get(url, auth_token).await
     }
 
     /// Melt Bolt12 [NUT-23]
@@ -632,18 +527,24 @@ impl MintConnector for HttpClient {
 /// Http Client
 #[derive(Debug, Clone)]
 #[cfg(feature = "auth")]
-pub struct AuthHttpClient {
-    core: HttpClientCore,
+pub struct AuthHttpClient<T>
+where
+    T: Transport + Send + Sync + 'static,
+{
+    transport: Arc<T>,
     mint_url: MintUrl,
     cat: Arc<RwLock<AuthToken>>,
 }
 
 #[cfg(feature = "auth")]
-impl AuthHttpClient {
+impl<T> AuthHttpClient<T>
+where
+    T: Transport + Send + Sync + 'static,
+{
     /// Create new [`AuthHttpClient`]
     pub fn new(mint_url: MintUrl, cat: Option<AuthToken>) -> Self {
         Self {
-            core: HttpClientCore::new(),
+            transport: T::default().into(),
             mint_url,
             cat: Arc::new(RwLock::new(
                 cat.unwrap_or(AuthToken::ClearAuth("".to_string())),
@@ -655,7 +556,10 @@ impl AuthHttpClient {
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 #[cfg(feature = "auth")]
-impl AuthMintConnector for AuthHttpClient {
+impl<T> AuthMintConnector for AuthHttpClient<T>
+where
+    T: Transport + Send + Sync + 'static,
+{
     async fn get_auth_token(&self) -> Result<AuthToken, Error> {
         Ok(self.cat.read().await.clone())
     }
@@ -668,7 +572,7 @@ impl AuthMintConnector for AuthHttpClient {
     /// Get Mint Info [NUT-06]
     async fn get_mint_info(&self) -> Result<MintInfo, Error> {
         let url = self.mint_url.join_paths(&["v1", "info"])?;
-        let mint_info: MintInfo = self.core.http_get::<_, MintInfo>(url, None).await?;
+        let mint_info: MintInfo = self.transport.http_get::<MintInfo>(url, None).await?;
 
         Ok(mint_info)
     }
@@ -680,7 +584,7 @@ impl AuthMintConnector for AuthHttpClient {
             self.mint_url
                 .join_paths(&["v1", "auth", "blind", "keys", &keyset_id.to_string()])?;
 
-        let mut keys_response = self.core.http_get::<_, KeysResponse>(url, None).await?;
+        let mut keys_response = self.transport.http_get::<KeysResponse>(url, None).await?;
 
         let keyset = keys_response
             .keysets
@@ -698,14 +602,14 @@ impl AuthMintConnector for AuthHttpClient {
             .mint_url
             .join_paths(&["v1", "auth", "blind", "keysets"])?;
 
-        self.core.http_get(url, None).await
+        self.transport.http_get(url, None).await
     }
 
     /// Mint Tokens [NUT-22]
     #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
     async fn post_mint_blind_auth(&self, request: MintAuthRequest) -> Result<MintResponse, Error> {
         let url = self.mint_url.join_paths(&["v1", "auth", "blind", "mint"])?;
-        self.core
+        self.transport
             .http_post(url, Some(self.cat.read().await.clone()), &request)
             .await
     }

+ 6 - 3
crates/cdk/src/wallet/mint_connector/mod.rs

@@ -15,11 +15,14 @@ use crate::nuts::{
 #[cfg(feature = "auth")]
 use crate::wallet::AuthWallet;
 
-mod http_client;
+pub mod http_client;
+pub mod transport;
 
+/// Auth HTTP Client with async transport
 #[cfg(feature = "auth")]
-pub use http_client::AuthHttpClient;
-pub use http_client::HttpClient;
+pub type AuthHttpClient = http_client::AuthHttpClient<transport::Async>;
+/// Http Client with async transport
+pub type HttpClient = http_client::HttpClient<transport::Async>;
 
 /// Interface that connects a wallet to a mint. Typically represents an [HttpClient].
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]

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

@@ -0,0 +1,182 @@
+//! HTTP Transport trait with a default implementation
+use std::fmt::Debug;
+
+use cdk_common::AuthToken;
+use reqwest::Client;
+use serde::de::DeserializeOwned;
+use serde::Serialize;
+use url::Url;
+
+use super::Error;
+use crate::error::ErrorResponse;
+
+/// Expected HTTP Transport
+#[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 {
+    /// Make the transport to use a given proxy
+    fn with_proxy(
+        &mut self,
+        proxy: Url,
+        host_matcher: Option<&str>,
+        accept_invalid_certs: bool,
+    ) -> Result<(), Error>;
+
+    /// HTTP Get request
+    async fn http_get<R>(&self, url: Url, auth: Option<AuthToken>) -> Result<R, Error>
+    where
+        R: DeserializeOwned;
+
+    /// HTTP Post request
+    async fn http_post<P, R>(
+        &self,
+        url: Url,
+        auth_token: Option<AuthToken>,
+        payload: &P,
+    ) -> Result<R, Error>
+    where
+        P: Serialize + ?Sized + Send + Sync,
+        R: DeserializeOwned;
+}
+
+/// Async transport for Http
+#[derive(Debug, Clone)]
+pub struct Async {
+    inner: Client,
+}
+
+impl Default for Async {
+    fn default() -> Self {
+        #[cfg(not(target_arch = "wasm32"))]
+        if rustls::crypto::CryptoProvider::get_default().is_none() {
+            let _ = rustls::crypto::ring::default_provider().install_default();
+        }
+
+        Self {
+            inner: Client::new(),
+        }
+    }
+}
+
+#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
+impl Transport for Async {
+    #[cfg(target_arch = "wasm32")]
+    fn with_proxy(
+        &mut self,
+        _proxy: Url,
+        _host_matcher: Option<&str>,
+        _accept_invalid_certs: bool,
+    ) -> Result<(), Error> {
+        panic!("Not supported in wasm");
+    }
+
+    #[cfg(not(target_arch = "wasm32"))]
+    fn with_proxy(
+        &mut self,
+        proxy: Url,
+        host_matcher: Option<&str>,
+        accept_invalid_certs: bool,
+    ) -> Result<(), Error> {
+        let regex = host_matcher
+            .map(regex::Regex::new)
+            .transpose()
+            .map_err(|e| Error::Custom(e.to_string()))?;
+        self.inner = reqwest::Client::builder()
+            .proxy(reqwest::Proxy::custom(move |url| {
+                if let Some(matcher) = regex.as_ref() {
+                    if let Some(host) = url.host_str() {
+                        if matcher.is_match(host) {
+                            return Some(proxy.clone());
+                        }
+                    }
+                }
+                None
+            }))
+            .danger_accept_invalid_certs(accept_invalid_certs) // Allow self-signed certs
+            .build()
+            .map_err(|e| {
+                Error::HttpError(
+                    e.status().map(|status_code| status_code.as_u16()),
+                    e.to_string(),
+                )
+            })?;
+        Ok(())
+    }
+
+    async fn http_get<R>(&self, url: Url, auth: Option<AuthToken>) -> Result<R, Error>
+    where
+        R: DeserializeOwned,
+    {
+        let mut request = self.inner.get(url);
+
+        if let Some(auth) = auth {
+            request = request.header(auth.header_key(), auth.to_string());
+        }
+
+        let response = request
+            .send()
+            .await
+            .map_err(|e| {
+                Error::HttpError(
+                    e.status().map(|status_code| status_code.as_u16()),
+                    e.to_string(),
+                )
+            })?
+            .text()
+            .await
+            .map_err(|e| {
+                Error::HttpError(
+                    e.status().map(|status_code| status_code.as_u16()),
+                    e.to_string(),
+                )
+            })?;
+
+        serde_json::from_str::<R>(&response).map_err(|err| {
+            tracing::warn!("Http Response error: {}", err);
+            match ErrorResponse::from_json(&response) {
+                Ok(ok) => <ErrorResponse as Into<Error>>::into(ok),
+                Err(err) => err.into(),
+            }
+        })
+    }
+
+    async fn http_post<P, R>(
+        &self,
+        url: Url,
+        auth_token: Option<AuthToken>,
+        payload: &P,
+    ) -> Result<R, Error>
+    where
+        P: Serialize + ?Sized + Send + Sync,
+        R: DeserializeOwned,
+    {
+        let mut request = self.inner.post(url).json(&payload);
+
+        if let Some(auth) = auth_token {
+            request = request.header(auth.header_key(), auth.to_string());
+        }
+
+        let response = request.send().await.map_err(|e| {
+            Error::HttpError(
+                e.status().map(|status_code| status_code.as_u16()),
+                e.to_string(),
+            )
+        })?;
+
+        let response = response.text().await.map_err(|e| {
+            Error::HttpError(
+                e.status().map(|status_code| status_code.as_u16()),
+                e.to_string(),
+            )
+        })?;
+
+        serde_json::from_str::<R>(&response).map_err(|err| {
+            tracing::warn!("Http Response error: {}", err);
+            match ErrorResponse::from_json(&response) {
+                Ok(ok) => <ErrorResponse as Into<Error>>::into(ok),
+                Err(err) => err.into(),
+            }
+        })
+    }
+}

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

@@ -54,6 +54,10 @@ pub use auth::{AuthMintConnector, AuthWallet};
 pub use builder::WalletBuilder;
 pub use cdk_common::wallet as types;
 #[cfg(feature = "auth")]
+pub use mint_connector::http_client::AuthHttpClient as BaseAuthHttpClient;
+pub use mint_connector::http_client::HttpClient as BaseHttpClient;
+pub use mint_connector::transport::Transport as HttpTransport;
+#[cfg(feature = "auth")]
 pub use mint_connector::AuthHttpClient;
 pub use mint_connector::{HttpClient, MintConnector};
 pub use multi_mint_wallet::MultiMintWallet;