瀏覽代碼

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

* Add NUT-19 support in the wallet
C 2 月之前
父節點
當前提交
8e0c44248b
共有 2 個文件被更改,包括 121 次插入14 次删除
  1. 2 2
      crates/cashu/src/nuts/nut19.rs
  2. 119 12
      crates/cdk/src/wallet/mint_connector/http_client.rs

+ 2 - 2
crates/cashu/src/nuts/nut19.rs

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

+ 119 - 12
crates/cdk/src/wallet/mint_connector/http_client.rs

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