diff --git a/crates/cashu/src/nuts/nut18/payment_request.rs b/crates/cashu/src/nuts/nut18/payment_request.rs index 73597012..0d828726 100644 --- a/crates/cashu/src/nuts/nut18/payment_request.rs +++ b/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 diff --git a/crates/cdk-integration-tests/tests/bolt12.rs b/crates/cdk-integration-tests/tests/bolt12.rs index 44873f74..514a169f 100644 --- a/crates/cdk-integration-tests/tests/bolt12.rs +++ b/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?; diff --git a/crates/cdk-integration-tests/tests/fake_auth.rs b/crates/cdk-integration-tests/tests/fake_auth.rs index ddf700a6..75c306c0 100644 --- a/crates/cdk-integration-tests/tests/fake_auth.rs +++ b/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"); diff --git a/crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs b/crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs index 0db6cf25..ba2a06bd 100644 --- a/crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs +++ b/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"); diff --git a/crates/cdk-integration-tests/tests/ldk_node.rs b/crates/cdk-integration-tests/tests/ldk_node.rs index 51f5327c..6f8b8bf4 100644 --- a/crates/cdk-integration-tests/tests/ldk_node.rs +++ b/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("e_request) .send() .await?; diff --git a/crates/cdk-integration-tests/tests/regtest.rs b/crates/cdk-integration-tests/tests/regtest.rs index fb66ce88..e726ab25 100644 --- a/crates/cdk-integration-tests/tests/regtest.rs +++ b/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"); diff --git a/crates/cdk-integration-tests/tests/test_fees.rs b/crates/cdk-integration-tests/tests/test_fees.rs index b6fca782..f63dbe06 100644 --- a/crates/cdk-integration-tests/tests/test_fees.rs +++ b/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"); diff --git a/crates/cdk/Cargo.toml b/crates/cdk/Cargo.toml index 77a5de43..dec29d19 100644 --- a/crates/cdk/Cargo.toml +++ b/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]] diff --git a/crates/cdk/examples/mint-token-bolt12-with-custom-http.rs b/crates/cdk/examples/mint-token-bolt12-with-custom-http.rs new file mode 100644 index 00000000..31d16b30 --- /dev/null +++ b/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(&self, url: Url, _auth: Option) -> Result + 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( + &self, + url: Url, + _auth_token: Option, + payload: &P, + ) -> Result + 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; + +#[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(()) +} diff --git a/crates/cdk/src/wallet/mint_connector/http_client.rs b/crates/cdk/src/wallet/mint_connector/http_client.rs index 577d012f..93cb752c 100644 --- a/crates/cdk/src/wallet/mint_connector/http_client.rs +++ b/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( - &self, - url: U, - auth: Option, - ) -> Result { - 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::(&response).map_err(|err| { - tracing::warn!("Http Response error: {}", err); - match ErrorResponse::from_json(&response) { - Ok(ok) => >::into(ok), - Err(err) => err.into(), - } - }) - } - - async fn http_post( - &self, - url: U, - auth_token: Option, - payload: &P, - ) -> Result { - 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::(&response).map_err(|err| { - tracing::warn!("Http Response error: {}", err); - match ErrorResponse::from_json(&response) { - Ok(ok) => >::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 +where + T: Transport + Send + Sync + 'static, +{ + transport: Arc, mint_url: MintUrl, cache_support: Arc>, #[cfg(feature = "auth")] auth_wallet: Arc>>, } -impl HttpClient { +impl HttpClient +where + T: Transport + Send + Sync + 'static, +{ /// Create new [`HttpClient`] #[cfg(feature = "auth")] pub fn new(mint_url: MintUrl, auth_wallet: Option) -> 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 { - 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 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 MintConnector for HttpClient +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, Error> { let url = self.mint_url.join_paths(&["v1", "keys"])?; Ok(self - .core - .http_get::<_, KeysResponse>(url, None) + .transport + .http_get::(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::(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 { 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 { 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 +where + T: Transport + Send + Sync + 'static, +{ + transport: Arc, mint_url: MintUrl, cat: Arc>, } #[cfg(feature = "auth")] -impl AuthHttpClient { +impl AuthHttpClient +where + T: Transport + Send + Sync + 'static, +{ /// Create new [`AuthHttpClient`] pub fn new(mint_url: MintUrl, cat: Option) -> 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 AuthMintConnector for AuthHttpClient +where + T: Transport + Send + Sync + 'static, +{ async fn get_auth_token(&self) -> Result { 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 { 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::(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::(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 { 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 } diff --git a/crates/cdk/src/wallet/mint_connector/mod.rs b/crates/cdk/src/wallet/mint_connector/mod.rs index eb88944a..8675d294 100644 --- a/crates/cdk/src/wallet/mint_connector/mod.rs +++ b/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; +/// Http Client with async transport +pub type HttpClient = http_client::HttpClient; /// Interface that connects a wallet to a mint. Typically represents an [HttpClient]. #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] diff --git a/crates/cdk/src/wallet/mint_connector/transport.rs b/crates/cdk/src/wallet/mint_connector/transport.rs new file mode 100644 index 00000000..abd3b55e --- /dev/null +++ b/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(&self, url: Url, auth: Option) -> Result + where + R: DeserializeOwned; + + /// HTTP Post request + async fn http_post( + &self, + url: Url, + auth_token: Option, + payload: &P, + ) -> Result + 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(&self, url: Url, auth: Option) -> Result + 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::(&response).map_err(|err| { + tracing::warn!("Http Response error: {}", err); + match ErrorResponse::from_json(&response) { + Ok(ok) => >::into(ok), + Err(err) => err.into(), + } + }) + } + + async fn http_post( + &self, + url: Url, + auth_token: Option, + payload: &P, + ) -> Result + 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::(&response).map_err(|err| { + tracing::warn!("Http Response error: {}", err); + match ErrorResponse::from_json(&response) { + Ok(ok) => >::into(ok), + Err(err) => err.into(), + } + }) + } +} diff --git a/crates/cdk/src/wallet/mod.rs b/crates/cdk/src/wallet/mod.rs index 1bb41c32..3209be7d 100644 --- a/crates/cdk/src/wallet/mod.rs +++ b/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;