|
@@ -1,8 +1,9 @@
|
|
-#[cfg(feature = "auth")]
|
|
|
|
-use std::sync::Arc;
|
|
|
|
|
|
+use std::collections::HashSet;
|
|
|
|
+use std::sync::{Arc, RwLock as StdRwLock};
|
|
|
|
+use std::time::{Duration, Instant};
|
|
|
|
|
|
use async_trait::async_trait;
|
|
use async_trait::async_trait;
|
|
-use cdk_common::{MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};
|
|
|
|
|
|
+use cdk_common::{nut19, MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};
|
|
#[cfg(feature = "auth")]
|
|
#[cfg(feature = "auth")]
|
|
use cdk_common::{Method, ProtectedEndpoint, RoutePath};
|
|
use cdk_common::{Method, ProtectedEndpoint, RoutePath};
|
|
use reqwest::{Client, IntoUrl};
|
|
use reqwest::{Client, IntoUrl};
|
|
@@ -109,11 +110,14 @@ impl HttpClientCore {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+type Cache = (u64, HashSet<(nut19::Method, nut19::Path)>);
|
|
|
|
+
|
|
/// Http Client
|
|
/// Http Client
|
|
#[derive(Debug, Clone)]
|
|
#[derive(Debug, Clone)]
|
|
pub struct HttpClient {
|
|
pub struct HttpClient {
|
|
core: HttpClientCore,
|
|
core: HttpClientCore,
|
|
mint_url: MintUrl,
|
|
mint_url: MintUrl,
|
|
|
|
+ cache_support: Arc<StdRwLock<Cache>>,
|
|
#[cfg(feature = "auth")]
|
|
#[cfg(feature = "auth")]
|
|
auth_wallet: Arc<RwLock<Option<AuthWallet>>>,
|
|
auth_wallet: Arc<RwLock<Option<AuthWallet>>>,
|
|
}
|
|
}
|
|
@@ -126,6 +130,7 @@ impl HttpClient {
|
|
core: HttpClientCore::new(),
|
|
core: HttpClientCore::new(),
|
|
mint_url,
|
|
mint_url,
|
|
auth_wallet: Arc::new(RwLock::new(auth_wallet)),
|
|
auth_wallet: Arc::new(RwLock::new(auth_wallet)),
|
|
|
|
+ cache_support: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
@@ -134,6 +139,7 @@ impl HttpClient {
|
|
pub fn new(mint_url: MintUrl) -> Self {
|
|
pub fn new(mint_url: MintUrl) -> Self {
|
|
Self {
|
|
Self {
|
|
core: HttpClientCore::new(),
|
|
core: HttpClientCore::new(),
|
|
|
|
+ cache_support: Default::default(),
|
|
mint_url,
|
|
mint_url,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@@ -190,8 +196,72 @@ impl HttpClient {
|
|
mint_url,
|
|
mint_url,
|
|
#[cfg(feature = "auth")]
|
|
#[cfg(feature = "auth")]
|
|
auth_wallet: Arc::new(RwLock::new(None)),
|
|
auth_wallet: Arc::new(RwLock::new(None)),
|
|
|
|
+ cache_support: Default::default(),
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ /// Generic implementation of a retriable http request
|
|
|
|
+ ///
|
|
|
|
+ /// The retry only happens if the mint supports replay through the Caching of NUT-19.
|
|
|
|
+ #[inline(always)]
|
|
|
|
+ async fn retriable_http_request<P, R>(
|
|
|
|
+ &self,
|
|
|
|
+ method: nut19::Method,
|
|
|
|
+ path: nut19::Path,
|
|
|
|
+ auth_token: Option<AuthToken>,
|
|
|
|
+ payload: &P,
|
|
|
|
+ ) -> Result<R, Error>
|
|
|
|
+ where
|
|
|
|
+ P: Serialize + ?Sized,
|
|
|
|
+ R: DeserializeOwned,
|
|
|
|
+ {
|
|
|
|
+ let started = Instant::now();
|
|
|
|
+
|
|
|
|
+ let retriable_window = self
|
|
|
|
+ .cache_support
|
|
|
|
+ .read()
|
|
|
|
+ .map(|cache_support| {
|
|
|
|
+ cache_support
|
|
|
|
+ .1
|
|
|
|
+ .get(&(method, path))
|
|
|
|
+ .map(|_| cache_support.0)
|
|
|
|
+ })
|
|
|
|
+ .unwrap_or_default()
|
|
|
|
+ .map(Duration::from_secs)
|
|
|
|
+ .unwrap_or_default();
|
|
|
|
+
|
|
|
|
+ loop {
|
|
|
|
+ let url = self.mint_url.join_paths(&match path {
|
|
|
|
+ nut19::Path::MintBolt11 => vec!["v1", "mint", "bolt11"],
|
|
|
|
+ nut19::Path::MeltBolt11 => vec!["v1", "melt", "bolt11"],
|
|
|
|
+ nut19::Path::MintBolt12 => vec!["v1", "mint", "bolt12"],
|
|
|
|
+ nut19::Path::MeltBolt12 => vec!["v1", "melt", "bolt12"],
|
|
|
|
+ nut19::Path::Swap => vec!["v1", "swap"],
|
|
|
|
+ })?;
|
|
|
|
+
|
|
|
|
+ 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,
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ if result.is_ok() {
|
|
|
|
+ return result;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ match result.as_ref() {
|
|
|
|
+ Err(Error::Database(_) | Error::HttpError(_) | Error::Custom(_)) => {
|
|
|
|
+ // retry request, if possible
|
|
|
|
+ tracing::error!("Failed http_request {:?}", result.as_ref().err());
|
|
|
|
+
|
|
|
|
+ if retriable_window < started.elapsed() {
|
|
|
|
+ return result;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ Err(_) => return result,
|
|
|
|
+ _ => unreachable!(),
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
|
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
|
@@ -272,7 +342,6 @@ impl MintConnector for HttpClient {
|
|
/// Mint Tokens [NUT-04]
|
|
/// Mint Tokens [NUT-04]
|
|
#[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
|
|
#[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
|
|
async fn post_mint(&self, request: MintRequest<String>) -> Result<MintResponse, Error> {
|
|
async fn post_mint(&self, request: MintRequest<String>) -> Result<MintResponse, Error> {
|
|
- let url = self.mint_url.join_paths(&["v1", "mint", "bolt11"])?;
|
|
|
|
#[cfg(feature = "auth")]
|
|
#[cfg(feature = "auth")]
|
|
let auth_token = self
|
|
let auth_token = self
|
|
.get_auth_token(Method::Post, RoutePath::MintBolt11)
|
|
.get_auth_token(Method::Post, RoutePath::MintBolt11)
|
|
@@ -280,7 +349,13 @@ impl MintConnector for HttpClient {
|
|
|
|
|
|
#[cfg(not(feature = "auth"))]
|
|
#[cfg(not(feature = "auth"))]
|
|
let auth_token = None;
|
|
let auth_token = None;
|
|
- self.core.http_post(url, auth_token, &request).await
|
|
|
|
|
|
+ self.retriable_http_request(
|
|
|
|
+ nut19::Method::Post,
|
|
|
|
+ nut19::Path::MintBolt11,
|
|
|
|
+ auth_token,
|
|
|
|
+ &request,
|
|
|
|
+ )
|
|
|
|
+ .await
|
|
}
|
|
}
|
|
|
|
|
|
/// Melt Quote [NUT-05]
|
|
/// Melt Quote [NUT-05]
|
|
@@ -329,7 +404,6 @@ impl MintConnector for HttpClient {
|
|
&self,
|
|
&self,
|
|
request: MeltRequest<String>,
|
|
request: MeltRequest<String>,
|
|
) -> Result<MeltQuoteBolt11Response<String>, Error> {
|
|
) -> Result<MeltQuoteBolt11Response<String>, Error> {
|
|
- let url = self.mint_url.join_paths(&["v1", "melt", "bolt11"])?;
|
|
|
|
#[cfg(feature = "auth")]
|
|
#[cfg(feature = "auth")]
|
|
let auth_token = self
|
|
let auth_token = self
|
|
.get_auth_token(Method::Post, RoutePath::MeltBolt11)
|
|
.get_auth_token(Method::Post, RoutePath::MeltBolt11)
|
|
@@ -337,25 +411,53 @@ impl MintConnector for HttpClient {
|
|
|
|
|
|
#[cfg(not(feature = "auth"))]
|
|
#[cfg(not(feature = "auth"))]
|
|
let auth_token = None;
|
|
let auth_token = None;
|
|
- self.core.http_post(url, auth_token, &request).await
|
|
|
|
|
|
+
|
|
|
|
+ self.retriable_http_request(
|
|
|
|
+ nut19::Method::Post,
|
|
|
|
+ nut19::Path::MeltBolt11,
|
|
|
|
+ auth_token,
|
|
|
|
+ &request,
|
|
|
|
+ )
|
|
|
|
+ .await
|
|
}
|
|
}
|
|
|
|
|
|
/// Swap Token [NUT-03]
|
|
/// Swap Token [NUT-03]
|
|
#[instrument(skip(self, swap_request), fields(mint_url = %self.mint_url))]
|
|
#[instrument(skip(self, swap_request), fields(mint_url = %self.mint_url))]
|
|
async fn post_swap(&self, swap_request: SwapRequest) -> Result<SwapResponse, Error> {
|
|
async fn post_swap(&self, swap_request: SwapRequest) -> Result<SwapResponse, Error> {
|
|
- let url = self.mint_url.join_paths(&["v1", "swap"])?;
|
|
|
|
#[cfg(feature = "auth")]
|
|
#[cfg(feature = "auth")]
|
|
let auth_token = self.get_auth_token(Method::Post, RoutePath::Swap).await?;
|
|
let auth_token = self.get_auth_token(Method::Post, RoutePath::Swap).await?;
|
|
|
|
|
|
#[cfg(not(feature = "auth"))]
|
|
#[cfg(not(feature = "auth"))]
|
|
let auth_token = None;
|
|
let auth_token = None;
|
|
- self.core.http_post(url, auth_token, &swap_request).await
|
|
|
|
|
|
+
|
|
|
|
+ self.retriable_http_request(
|
|
|
|
+ nut19::Method::Post,
|
|
|
|
+ nut19::Path::Swap,
|
|
|
|
+ auth_token,
|
|
|
|
+ &swap_request,
|
|
|
|
+ )
|
|
|
|
+ .await
|
|
}
|
|
}
|
|
|
|
|
|
/// Helper to get mint info
|
|
/// Helper to get mint info
|
|
async fn get_mint_info(&self) -> Result<MintInfo, Error> {
|
|
async fn get_mint_info(&self) -> Result<MintInfo, Error> {
|
|
let url = self.mint_url.join_paths(&["v1", "info"])?;
|
|
let url = self.mint_url.join_paths(&["v1", "info"])?;
|
|
- self.core.http_get(url, None).await
|
|
|
|
|
|
+ let info: MintInfo = self.core.http_get(url, None).await?;
|
|
|
|
+
|
|
|
|
+ if let Ok(mut cache_support) = self.cache_support.write() {
|
|
|
|
+ *cache_support = (
|
|
|
|
+ info.nuts.nut19.ttl.unwrap_or(300),
|
|
|
|
+ info.nuts
|
|
|
|
+ .nut19
|
|
|
|
+ .cached_endpoints
|
|
|
|
+ .clone()
|
|
|
|
+ .into_iter()
|
|
|
|
+ .map(|cached_endpoint| (cached_endpoint.method, cached_endpoint.path))
|
|
|
|
+ .collect(),
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ Ok(info)
|
|
}
|
|
}
|
|
|
|
|
|
#[cfg(feature = "auth")]
|
|
#[cfg(feature = "auth")]
|
|
@@ -485,7 +587,6 @@ impl MintConnector for HttpClient {
|
|
&self,
|
|
&self,
|
|
request: MeltRequest<String>,
|
|
request: MeltRequest<String>,
|
|
) -> Result<MeltQuoteBolt11Response<String>, Error> {
|
|
) -> Result<MeltQuoteBolt11Response<String>, Error> {
|
|
- let url = self.mint_url.join_paths(&["v1", "melt", "bolt12"])?;
|
|
|
|
#[cfg(feature = "auth")]
|
|
#[cfg(feature = "auth")]
|
|
let auth_token = self
|
|
let auth_token = self
|
|
.get_auth_token(Method::Post, RoutePath::MeltBolt12)
|
|
.get_auth_token(Method::Post, RoutePath::MeltBolt12)
|
|
@@ -493,7 +594,13 @@ impl MintConnector for HttpClient {
|
|
|
|
|
|
#[cfg(not(feature = "auth"))]
|
|
#[cfg(not(feature = "auth"))]
|
|
let auth_token = None;
|
|
let auth_token = None;
|
|
- self.core.http_post(url, auth_token, &request).await
|
|
|
|
|
|
+ self.retriable_http_request(
|
|
|
|
+ nut19::Method::Post,
|
|
|
|
+ nut19::Path::MeltBolt12,
|
|
|
|
+ auth_token,
|
|
|
|
+ &request,
|
|
|
|
+ )
|
|
|
|
+ .await
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|