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.
This commit is contained in:
C
2025-08-30 04:54:48 -03:00
committed by GitHub
parent 14473d8051
commit 2131f89068
13 changed files with 439 additions and 179 deletions

View File

@@ -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

View File

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

View File

@@ -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");

View File

@@ -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");

View File

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

View File

@@ -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");

View File

@@ -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");

View File

@@ -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]]

View File

@@ -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(())
}

View File

@@ -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
}

View File

@@ -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))]

View File

@@ -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(),
}
})
}
}

View File

@@ -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;