Add NUT-19 support in the wallet (#912)

* Add NUT-19 support in the wallet
This commit is contained in:
C
2025-07-29 22:45:30 -03:00
committed by GitHub
parent 72887341ca
commit 8e0c44248b
2 changed files with 121 additions and 14 deletions

View File

@@ -32,7 +32,7 @@ impl CachedEndpoint {
}
/// HTTP method
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub enum Method {
@@ -43,7 +43,7 @@ pub enum Method {
}
/// Route path
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub enum Path {
/// Bolt11 Mint

View File

@@ -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 cdk_common::{MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};
use cdk_common::{nut19, MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};
#[cfg(feature = "auth")]
use cdk_common::{Method, ProtectedEndpoint, RoutePath};
use reqwest::{Client, IntoUrl};
@@ -109,11 +110,14 @@ impl HttpClientCore {
}
}
type Cache = (u64, HashSet<(nut19::Method, nut19::Path)>);
/// Http Client
#[derive(Debug, Clone)]
pub struct HttpClient {
core: HttpClientCore,
mint_url: MintUrl,
cache_support: Arc<StdRwLock<Cache>>,
#[cfg(feature = "auth")]
auth_wallet: Arc<RwLock<Option<AuthWallet>>>,
}
@@ -126,6 +130,7 @@ impl HttpClient {
core: HttpClientCore::new(),
mint_url,
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 {
Self {
core: HttpClientCore::new(),
cache_support: Default::default(),
mint_url,
}
}
@@ -190,8 +196,72 @@ impl HttpClient {
mint_url,
#[cfg(feature = "auth")]
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))]
@@ -272,7 +342,6 @@ impl MintConnector for HttpClient {
/// Mint Tokens [NUT-04]
#[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
async fn post_mint(&self, request: MintRequest<String>) -> Result<MintResponse, Error> {
let url = self.mint_url.join_paths(&["v1", "mint", "bolt11"])?;
#[cfg(feature = "auth")]
let auth_token = self
.get_auth_token(Method::Post, RoutePath::MintBolt11)
@@ -280,7 +349,13 @@ impl MintConnector for HttpClient {
#[cfg(not(feature = "auth"))]
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]
@@ -329,7 +404,6 @@ impl MintConnector for HttpClient {
&self,
request: MeltRequest<String>,
) -> Result<MeltQuoteBolt11Response<String>, Error> {
let url = self.mint_url.join_paths(&["v1", "melt", "bolt11"])?;
#[cfg(feature = "auth")]
let auth_token = self
.get_auth_token(Method::Post, RoutePath::MeltBolt11)
@@ -337,25 +411,53 @@ impl MintConnector for HttpClient {
#[cfg(not(feature = "auth"))]
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]
#[instrument(skip(self, swap_request), fields(mint_url = %self.mint_url))]
async fn post_swap(&self, swap_request: SwapRequest) -> Result<SwapResponse, Error> {
let url = self.mint_url.join_paths(&["v1", "swap"])?;
#[cfg(feature = "auth")]
let auth_token = self.get_auth_token(Method::Post, RoutePath::Swap).await?;
#[cfg(not(feature = "auth"))]
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
async fn get_mint_info(&self) -> Result<MintInfo, Error> {
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")]
@@ -485,7 +587,6 @@ impl MintConnector for HttpClient {
&self,
request: MeltRequest<String>,
) -> Result<MeltQuoteBolt11Response<String>, Error> {
let url = self.mint_url.join_paths(&["v1", "melt", "bolt12"])?;
#[cfg(feature = "auth")]
let auth_token = self
.get_auth_token(Method::Post, RoutePath::MeltBolt12)
@@ -493,7 +594,13 @@ impl MintConnector for HttpClient {
#[cfg(not(feature = "auth"))]
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
}
}