瀏覽代碼

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.

TODO:
-  [ ] Implement a reqwest sync client
Cesar Rodas 2 月之前
父節點
當前提交
93c8fda690

+ 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

+ 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?;

+ 64 - 155
crates/cdk/src/wallet/mint_connector/http_client.rs

@@ -6,17 +6,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 +27,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 +61,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 +85,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 +94,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 +118,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 +146,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,14 +182,17 @@ 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
+            .transport
             .http_get::<_, KeysResponse>(url, None)
             .await?
             .keysets)
@@ -311,7 +205,10 @@ 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 +217,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 +238,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 +258,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 +296,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 +316,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 +364,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 +406,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 +420,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 +441,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 +461,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 +480,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 +500,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 +529,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 +558,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 +574,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 +586,10 @@ 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 +607,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
     }

+ 5 - 2
crates/cdk/src/wallet/mint_connector/mod.rs

@@ -16,10 +16,13 @@ use crate::nuts::{
 use crate::wallet::AuthWallet;
 
 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))]

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

@@ -0,0 +1,183 @@
+//! HTTP Transport trait with a default implementation
+use std::fmt::Debug;
+
+use cdk_common::AuthToken;
+use reqwest::{Client, IntoUrl};
+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 {
+    fn with_proxy(
+        &mut self,
+        proxy: Url,
+        host_matcher: Option<&str>,
+        accept_invalid_certs: bool,
+    ) -> Result<(), Error>;
+
+    async fn http_get<U, R>(&self, url: U, auth: Option<AuthToken>) -> Result<R, Error>
+    where
+        U: IntoUrl + Send,
+        R: DeserializeOwned;
+
+    async fn http_post<U, P, R>(
+        &self,
+        url: U,
+        auth_token: Option<AuthToken>,
+        payload: &P,
+    ) -> Result<R, Error>
+    where
+        U: IntoUrl + Send + Sync,
+        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<U, R>(&self, url: U, auth: Option<AuthToken>) -> Result<R, Error>
+    where
+        U: IntoUrl + Send,
+        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<U, P, R>(
+        &self,
+        url: U,
+        auth_token: Option<AuthToken>,
+        payload: &P,
+    ) -> Result<R, Error>
+    where
+        U: IntoUrl + Send + Sync,
+        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(),
+            }
+        })
+    }
+}