mirror of
https://github.com/aljazceru/cdk.git
synced 2026-02-03 20:26:13 +01:00
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:
@@ -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
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
crates/cdk/examples/mint-token-bolt12-with-custom-http.rs
Normal file
161
crates/cdk/examples/mint-token-bolt12-with-custom-http.rs
Normal 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(())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
crates/cdk/src/wallet/mint_connector/transport.rs
Normal file
182
crates/cdk/src/wallet/mint_connector/transport.rs
Normal 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(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user