Forráskód Böngészése

Introduce pluggable backend cache for the HTTP layer. (#495)

---------

Co-authored-by: thesimplekid <tsk@thesimplekid.com>
C 2 hónapja
szülő
commit
dcca57dbd1

+ 8 - 0
.github/workflows/ci.yml

@@ -56,6 +56,10 @@ jobs:
             -p cdk --no-default-features --features "mint swagger",
             -p cdk-redb,
             -p cdk-sqlite,
+            -p cdk-axum --no-default-features,
+            -p cdk-axum --no-default-features --features swagger,
+            -p cdk-axum --no-default-features --features redis,
+            -p cdk-axum --no-default-features --features "redis swagger",
             -p cdk-axum,
             -p cdk-cln,
             -p cdk-lnd,
@@ -65,6 +69,9 @@ jobs:
             -p cdk-fake-wallet,
             --bin cdk-cli,
             --bin cdk-mintd,
+            --bin cdk-mintd --no-default-features --features swagger,
+            --bin cdk-mintd --no-default-features --features redis,
+            --bin cdk-mintd --no-default-features --features "redis swagger",
           ]
     steps:
       - name: checkout
@@ -152,6 +159,7 @@ jobs:
             -p cdk --no-default-features --features wallet,
             -p cdk --no-default-features --features mint,
             -p cdk-axum,
+            -p cdk-axum --no-default-features --features redis,
             -p cdk-strike,
             -p cdk-lnbits,
             -p cdk-phoenixd,

+ 1 - 1
.helix/languages.toml

@@ -1,2 +1,2 @@
 [language-server.rust-analyzer.config]
-cargo = { features = ["wallet", "mint", "swagger"] }
+cargo = { features = ["wallet", "mint", "swagger", "redis"] }

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 213 - 302
Cargo.lock


+ 1 - 1
Dockerfile

@@ -10,7 +10,7 @@ COPY Cargo.toml ./Cargo.toml
 COPY crates ./crates
 
 # Start the Nix daemon and develop the environment
-RUN nix develop --extra-experimental-features nix-command --extra-experimental-features flakes --command cargo build --release --bin cdk-mintd
+RUN nix develop --extra-experimental-features nix-command --extra-experimental-features flakes --command cargo build --release --bin cdk-mintd --features redis
 
 # Create a runtime stage
 FROM debian:bookworm-slim

+ 5 - 0
crates/cdk-axum/Cargo.toml

@@ -30,6 +30,11 @@ serde_json = "1"
 paste = "1.0.15"
 serde = { version = "1.0.210", features = ["derive"] }
 uuid = { version = "1", features = ["v4", "serde"] }
+sha2 = "0.10.8"
+redis = { version = "0.23.3", features = [
+    "tokio-rustls-comp",
+], optional = true }
 
 [features]
+redis = ["dep:redis"]
 swagger = ["cdk/swagger", "dep:utoipa"]

+ 45 - 0
crates/cdk-axum/src/cache/backend/memory.rs

@@ -0,0 +1,45 @@
+use std::time::Duration;
+
+use moka::future::Cache;
+
+use crate::cache::{HttpCacheKey, HttpCacheStorage, DEFAULT_TTI_SECS, DEFAULT_TTL_SECS};
+
+/// In memory cache storage for the HTTP cache.
+///
+/// This is the default cache storage backend, which is used if no other storage
+/// backend is provided, or if the provided storage backend is `None`.
+///
+/// The cache is limited to 10,000 entries and it is not shared between
+/// instances nor persisted.
+pub struct InMemoryHttpCache(pub Cache<HttpCacheKey, Vec<u8>>);
+
+impl Default for InMemoryHttpCache {
+    fn default() -> Self {
+        InMemoryHttpCache(
+            Cache::builder()
+                .max_capacity(10_000)
+                .time_to_live(Duration::from_secs(DEFAULT_TTL_SECS))
+                .time_to_idle(Duration::from_secs(DEFAULT_TTI_SECS))
+                .build(),
+        )
+    }
+}
+
+#[async_trait::async_trait]
+impl HttpCacheStorage for InMemoryHttpCache {
+    fn set_expiration_times(&mut self, cache_ttl: Duration, cache_tti: Duration) {
+        self.0 = Cache::builder()
+            .max_capacity(10_000)
+            .time_to_live(cache_ttl)
+            .time_to_idle(cache_tti)
+            .build();
+    }
+
+    async fn get(&self, key: &HttpCacheKey) -> Option<Vec<u8>> {
+        self.0.get(key)
+    }
+
+    async fn set(&self, key: HttpCacheKey, value: Vec<u8>) {
+        self.0.insert(key, value).await;
+    }
+}

+ 7 - 0
crates/cdk-axum/src/cache/backend/mod.rs

@@ -0,0 +1,7 @@
+mod memory;
+#[cfg(feature = "redis")]
+mod redis;
+
+pub use self::memory::InMemoryHttpCache;
+#[cfg(feature = "redis")]
+pub use self::redis::{Config as RedisConfig, HttpCacheRedis};

+ 96 - 0
crates/cdk-axum/src/cache/backend/redis.rs

@@ -0,0 +1,96 @@
+use std::time::Duration;
+
+use redis::AsyncCommands;
+use serde::{Deserialize, Serialize};
+
+use crate::cache::{HttpCacheKey, HttpCacheStorage};
+
+/// Redis cache storage for the HTTP cache.
+///
+/// This cache storage backend uses Redis to store the cache.
+pub struct HttpCacheRedis {
+    cache_ttl: Duration,
+    prefix: Option<Vec<u8>>,
+    client: redis::Client,
+}
+
+/// Configuration for the Redis cache storage.
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct Config {
+    /// Commong key prefix
+    pub key_prefix: Option<String>,
+
+    /// Connection string to the Redis server.
+    pub connection_string: String,
+}
+
+impl HttpCacheRedis {
+    /// Create a new Redis cache.
+    pub fn new(client: redis::Client) -> Self {
+        Self {
+            client,
+            prefix: None,
+            cache_ttl: Duration::from_secs(60),
+        }
+    }
+
+    /// Set a prefix for the cache keys.
+    ///
+    /// This is useful to have all the HTTP cache keys under a common prefix,
+    /// some sort of namespace, to make management of the database easier.
+    pub fn set_prefix(mut self, prefix: Vec<u8>) -> Self {
+        self.prefix = Some(prefix);
+        self
+    }
+}
+
+#[async_trait::async_trait]
+impl HttpCacheStorage for HttpCacheRedis {
+    fn set_expiration_times(&mut self, cache_ttl: Duration, _cache_tti: Duration) {
+        self.cache_ttl = cache_ttl;
+    }
+
+    async fn get(&self, key: &HttpCacheKey) -> Option<Vec<u8>> {
+        let mut conn = self
+            .client
+            .get_multiplexed_tokio_connection()
+            .await
+            .map_err(|err| {
+                tracing::error!("Failed to get redis connection: {:?}", err);
+                err
+            })
+            .ok()?;
+
+        let mut db_key = self.prefix.clone().unwrap_or_default();
+        db_key.extend(&**key);
+
+        conn.get(db_key)
+            .await
+            .map_err(|err| {
+                tracing::error!("Failed to get value from redis: {:?}", err);
+                err
+            })
+            .ok()?
+    }
+
+    async fn set(&self, key: HttpCacheKey, value: Vec<u8>) {
+        let mut db_key = self.prefix.clone().unwrap_or_default();
+        db_key.extend(&*key);
+
+        let mut conn = match self.client.get_multiplexed_tokio_connection().await {
+            Ok(conn) => conn,
+            Err(err) => {
+                tracing::error!("Failed to get redis connection: {:?}", err);
+                return;
+            }
+        };
+
+        let _: Result<(), _> = conn
+            .set_ex(db_key, value, self.cache_ttl.as_secs() as usize)
+            .await
+            .map_err(|err| {
+                tracing::error!("Failed to set value in redis: {:?}", err);
+                err
+            });
+    }
+}

+ 102 - 0
crates/cdk-axum/src/cache/config.rs

@@ -0,0 +1,102 @@
+use serde::{Deserialize, Serialize};
+
+pub const ENV_CDK_MINTD_CACHE_BACKEND: &str = "CDK_MINTD_CACHE_BACKEND";
+
+#[cfg(feature = "redis")]
+pub const ENV_CDK_MINTD_CACHE_REDIS_URL: &str = "CDK_MINTD_CACHE_REDIS_URL";
+#[cfg(feature = "redis")]
+pub const ENV_CDK_MINTD_CACHE_REDIS_KEY_PREFIX: &str = "CDK_MINTD_CACHE_REDIS_KEY_PREFIX";
+
+pub const ENV_CDK_MINTD_CACHE_TTI: &str = "CDK_MINTD_CACHE_TTI";
+pub const ENV_CDK_MINTD_CACHE_TTL: &str = "CDK_MINTD_CACHE_TTL";
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+#[serde(tag = "backend")]
+#[serde(rename_all = "lowercase")]
+pub enum Backend {
+    #[default]
+    Memory,
+    #[cfg(feature = "redis")]
+    Redis(super::backend::RedisConfig),
+}
+
+impl Backend {
+    pub fn from_env_str(backend_str: &str) -> Option<Self> {
+        match backend_str.to_lowercase().as_str() {
+            "memory" => Some(Self::Memory),
+            #[cfg(feature = "redis")]
+            "redis" => {
+                // Get Redis configuration from environment
+                let connection_string = std::env::var(ENV_CDK_MINTD_CACHE_REDIS_URL)
+                    .unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
+
+                let key_prefix = std::env::var(ENV_CDK_MINTD_CACHE_REDIS_KEY_PREFIX).ok();
+
+                Some(Self::Redis(super::backend::RedisConfig {
+                    connection_string,
+                    key_prefix,
+                }))
+            }
+            _ => None,
+        }
+    }
+}
+
+/// Cache configuration.
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct Config {
+    /// Cache backend.
+    #[serde(default)]
+    #[serde(flatten)]
+    pub backend: Backend,
+
+    /// Time to live for the cache entries.
+    pub ttl: Option<u64>,
+
+    /// Time for the cache entries to be idle.
+    pub tti: Option<u64>,
+}
+
+impl Config {
+    /// Config from env
+    pub fn from_env(mut self) -> Self {
+        use std::env;
+
+        // Parse backend
+        if let Ok(backend_str) = env::var(ENV_CDK_MINTD_CACHE_BACKEND) {
+            if let Some(backend) = Backend::from_env_str(&backend_str) {
+                self.backend = backend;
+
+                // If Redis backend is selected, parse Redis configuration
+                #[cfg(feature = "redis")]
+                if matches!(self.backend, Backend::Redis(_)) {
+                    let connection_string = env::var(ENV_CDK_MINTD_CACHE_REDIS_URL)
+                        .unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
+
+                    let key_prefix = env::var(ENV_CDK_MINTD_CACHE_REDIS_KEY_PREFIX).ok();
+
+                    self.backend = Backend::Redis(super::backend::RedisConfig {
+                        connection_string,
+                        key_prefix,
+                    });
+                }
+            }
+        }
+
+        // Parse TTL
+        if let Ok(ttl_str) = env::var(ENV_CDK_MINTD_CACHE_TTL) {
+            if let Ok(ttl) = ttl_str.parse() {
+                self.ttl = Some(ttl);
+            }
+        }
+
+        // Parse TTI
+        if let Ok(tti_str) = env::var(ENV_CDK_MINTD_CACHE_TTI) {
+            if let Ok(tti) = tti_str.parse() {
+                self.tti = Some(tti);
+            }
+        }
+
+        self
+    }
+}

+ 188 - 0
crates/cdk-axum/src/cache/mod.rs

@@ -0,0 +1,188 @@
+//! HTTP cache.
+//!
+//! This is mod defines a common trait to define custom backends for the HTTP cache.
+//!
+//! The HTTP cache is a layer to cache responses from HTTP requests, to avoid hitting
+//! the same endpoint multiple times, which can be expensive and slow, or to provide
+//! idempotent operations.
+//!
+//! This mod also provides common backend implementations as well, such as In
+//! Memory (default) and Redis.
+use std::ops::Deref;
+use std::sync::Arc;
+use std::time::Duration;
+
+use serde::de::DeserializeOwned;
+use serde::Serialize;
+use sha2::{Digest, Sha256};
+
+mod backend;
+mod config;
+
+pub use self::backend::*;
+pub use self::config::Config;
+
+#[async_trait::async_trait]
+/// Cache storage for the HTTP cache.
+pub trait HttpCacheStorage {
+    /// Sets the expiration times for the cache.
+    fn set_expiration_times(&mut self, cache_ttl: Duration, cache_tti: Duration);
+
+    /// Get a value from the cache.
+    async fn get(&self, key: &HttpCacheKey) -> Option<Vec<u8>>;
+
+    /// Set a value in the cache.
+    async fn set(&self, key: HttpCacheKey, value: Vec<u8>);
+}
+
+/// Http cache with a pluggable storage backend.
+pub struct HttpCache {
+    /// Time to live for the cache.
+    pub ttl: Duration,
+    /// Time to idle for the cache.
+    pub tti: Duration,
+    /// Storage backend for the cache.
+    storage: Arc<Box<dyn HttpCacheStorage + Send + Sync>>,
+}
+
+impl Default for HttpCache {
+    fn default() -> Self {
+        Self::new(
+            Duration::from_secs(DEFAULT_TTL_SECS),
+            Duration::from_secs(DEFAULT_TTI_SECS),
+            None,
+        )
+    }
+}
+
+/// Max payload size for the cache key.
+///
+/// This is a trade-off between security and performance. A large payload can be used to
+/// perform a CPU attack.
+const MAX_PAYLOAD_SIZE: usize = 10 * 1024 * 1024;
+
+/// Default TTL for the cache.
+const DEFAULT_TTL_SECS: u64 = 60;
+
+/// Default TTI for the cache.
+const DEFAULT_TTI_SECS: u64 = 60;
+
+/// Http cache key.
+///
+/// This type ensures no Vec<u8> is used as a key, which is error-prone.
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub struct HttpCacheKey([u8; 32]);
+
+impl Deref for HttpCacheKey {
+    type Target = [u8; 32];
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl From<config::Config> for HttpCache {
+    fn from(config: config::Config) -> Self {
+        match config.backend {
+            config::Backend::Memory => Self::new(
+                Duration::from_secs(config.ttl.unwrap_or(DEFAULT_TTL_SECS)),
+                Duration::from_secs(config.tti.unwrap_or(DEFAULT_TTI_SECS)),
+                None,
+            ),
+            #[cfg(feature = "redis")]
+            config::Backend::Redis(redis_config) => {
+                let client = redis::Client::open(redis_config.connection_string)
+                    .expect("Failed to create Redis client");
+                let storage = HttpCacheRedis::new(client).set_prefix(
+                    redis_config
+                        .key_prefix
+                        .unwrap_or_default()
+                        .as_bytes()
+                        .to_vec(),
+                );
+                Self::new(
+                    Duration::from_secs(config.ttl.unwrap_or(DEFAULT_TTL_SECS)),
+                    Duration::from_secs(config.tti.unwrap_or(DEFAULT_TTI_SECS)),
+                    Some(Box::new(storage)),
+                )
+            }
+        }
+    }
+}
+
+impl HttpCache {
+    /// Create a new HTTP cache.
+    pub fn new(
+        ttl: Duration,
+        tti: Duration,
+        storage: Option<Box<dyn HttpCacheStorage + Send + Sync + 'static>>,
+    ) -> Self {
+        let mut storage = storage.unwrap_or_else(|| Box::new(InMemoryHttpCache::default()));
+        storage.set_expiration_times(ttl, tti);
+
+        Self {
+            ttl,
+            tti,
+            storage: Arc::new(storage),
+        }
+    }
+
+    /// Calculate a cache key from a serializable value.
+    ///
+    /// Usually the input is the request body or query parameters.
+    ///
+    /// The result is an optional cache key. If the key cannot be calculated, it
+    /// will be None, meaning the value cannot be cached, therefore the entire
+    /// caching mechanism should be skipped.
+    ///
+    /// Instead of using the entire serialized input as the key, the key is a
+    /// double hash to have a predictable key size, although it may open the
+    /// window for CPU attacks with large payloads, but it is a trade-off.
+    /// Perhaps upper layer have a protection against large payloads.
+    pub fn calculate_key<K>(&self, key: &K) -> Option<HttpCacheKey>
+    where
+        K: Serialize,
+    {
+        let json_value = match serde_json::to_vec(key) {
+            Ok(value) => value,
+            Err(err) => {
+                tracing::warn!("Failed to serialize key: {:?}", err);
+                return None;
+            }
+        };
+
+        if json_value.len() > MAX_PAYLOAD_SIZE {
+            tracing::warn!("Key size is too large: {}", json_value.len());
+            return None;
+        }
+
+        let first_hash = Sha256::digest(json_value);
+        let second_hash = Sha256::digest(first_hash);
+        Some(HttpCacheKey(second_hash.into()))
+    }
+
+    /// Get a value from the cache.
+    pub async fn get<V>(self: &Arc<Self>, key: &HttpCacheKey) -> Option<V>
+    where
+        V: DeserializeOwned,
+    {
+        self.storage.get(key).await.and_then(|value| {
+            serde_json::from_slice(&value)
+                .map_err(|e| {
+                    tracing::warn!("Failed to deserialize value: {:?}", e);
+                    e
+                })
+                .ok()
+        })
+    }
+
+    /// Set a value in the cache.
+    pub async fn set<V: Serialize>(self: &Arc<Self>, key: HttpCacheKey, value: &V) {
+        if let Ok(bytes) = serde_json::to_vec(value).map_err(|e| {
+            tracing::warn!("Failed to serialize value: {:?}", e);
+            e
+        }) {
+            self.storage.set(key, bytes).await;
+        }
+    }
+}

+ 15 - 10
crates/cdk-axum/src/lib.rs

@@ -4,15 +4,15 @@
 #![warn(rustdoc::bare_urls)]
 
 use std::sync::Arc;
-use std::time::Duration;
 
 use anyhow::Result;
 use axum::routing::{get, post};
 use axum::Router;
+use cache::HttpCache;
 use cdk::mint::Mint;
-use moka::future::Cache;
 use router_handlers::*;
 
+pub mod cache;
 mod router_handlers;
 mod ws;
 
@@ -52,7 +52,7 @@ use uuid::Uuid;
 #[derive(Clone)]
 pub struct MintState {
     mint: Arc<Mint>,
-    cache: Cache<String, String>,
+    cache: Arc<cache::HttpCache>,
 }
 
 #[cfg(feature = "swagger")]
@@ -131,15 +131,20 @@ pub struct MintState {
 /// OpenAPI spec for the mint's v1 APIs
 pub struct ApiDocV1;
 
-/// Create mint [`Router`] with required endpoints for cashu mint
-pub async fn create_mint_router(mint: Arc<Mint>, cache_ttl: u64, cache_tti: u64) -> Result<Router> {
+/// Create mint [`Router`] with required endpoints for cashu mint with the default cache
+pub async fn create_mint_router(mint: Arc<Mint>) -> Result<Router> {
+    create_mint_router_with_custom_cache(mint, Default::default()).await
+}
+
+/// Create mint [`Router`] with required endpoints for cashu mint with a custom
+/// backend for cache
+pub async fn create_mint_router_with_custom_cache(
+    mint: Arc<Mint>,
+    cache: HttpCache,
+) -> Result<Router> {
     let state = MintState {
         mint,
-        cache: Cache::builder()
-            .max_capacity(10_000)
-            .time_to_live(Duration::from_secs(cache_ttl))
-            .time_to_idle(Duration::from_secs(cache_tti))
-            .build(),
+        cache: Arc::new(cache),
     };
 
     let v1_router = Router::new()

+ 11 - 11
crates/cdk-axum/src/router_handlers.rs

@@ -11,7 +11,6 @@ use cdk::nuts::{
     SwapRequest, SwapResponse,
 };
 use cdk::util::unix_time;
-use cdk::Error;
 use paste::paste;
 use uuid::Uuid;
 
@@ -31,19 +30,20 @@ macro_rules! post_cache_wrapper {
 
                 let json_extracted_payload = payload.deref();
                 let State(mint_state) = state.clone();
-                let cache_key = serde_json::to_string(&json_extracted_payload).map_err(|err| {
-                    into_response(Error::from(err))
-                })?;
-
-                if let Some(cached_response) = mint_state.cache.get(&cache_key) {
-                    return Ok(Json(serde_json::from_str(&cached_response)
-                        .expect("Shouldn't panic: response is json-deserializable.")));
+                let cache_key = match mint_state.cache.calculate_key(&json_extracted_payload) {
+                    Some(key) => key,
+                    None => {
+                        // Could not calculate key, just return the handler result
+                        return $handler(state, payload).await;
+                    }
+                };
+
+                if let Some(cached_response) = mint_state.cache.get::<$response_type>(&cache_key).await {
+                    return Ok(Json(cached_response));
                 }
 
                 let response = $handler(state, payload).await?;
-                mint_state.cache.insert(cache_key, serde_json::to_string(response.deref())
-                    .expect("Shouldn't panic: response is json-serializable.")
-                ).await;
+                mint_state.cache.set(cache_key, &response.deref()).await;
                 Ok(response)
             }
         }

+ 1 - 3
crates/cdk-integration-tests/src/init_fake_wallet.rs

@@ -50,11 +50,9 @@ where
     );
 
     let mint = create_mint(database, ln_backends.clone()).await?;
-    let cache_ttl = 3600;
-    let cache_tti = 3600;
     let mint_arc = Arc::new(mint);
 
-    let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc), cache_ttl, cache_tti)
+    let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc))
         .await
         .unwrap();
 

+ 3 - 9
crates/cdk-integration-tests/src/init_regtest.rs

@@ -221,17 +221,11 @@ where
     );
 
     let mint = create_mint(database, ln_backends.clone()).await?;
-    let cache_time_to_live = 3600;
-    let cache_time_to_idle = 3600;
     let mint_arc = Arc::new(mint);
 
-    let v1_service = cdk_axum::create_mint_router(
-        Arc::clone(&mint_arc),
-        cache_time_to_live,
-        cache_time_to_idle,
-    )
-    .await
-    .unwrap();
+    let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc))
+        .await
+        .unwrap();
 
     let mint_service = Router::new()
         .merge(v1_service)

+ 1 - 8
crates/cdk-integration-tests/src/lib.rs

@@ -86,17 +86,10 @@ pub async fn start_mint(
         HashMap::new(),
     )
     .await?;
-    let cache_time_to_live = 3600;
-    let cache_time_to_idle = 3600;
 
     let mint_arc = Arc::new(mint);
 
-    let v1_service = cdk_axum::create_mint_router(
-        Arc::clone(&mint_arc),
-        cache_time_to_live,
-        cache_time_to_idle,
-    )
-    .await?;
+    let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc)).await?;
 
     let mint_service = Router::new()
         .merge(v1_service)

+ 15 - 5
crates/cdk-mintd/Cargo.toml

@@ -6,15 +6,21 @@ authors = ["CDK Developers"]
 license = "MIT"
 homepage = "https://github.com/cashubtc/cdk"
 repository = "https://github.com/cashubtc/cdk.git"
-rust-version = "1.63.0" # MSRV
+rust-version = "1.63.0"                            # MSRV
 description = "CDK mint binary"
 
 [dependencies]
 anyhow = "1"
 axum = "0.6.20"
-cdk = { path = "../cdk", version = "0.5.0", default-features = false, features = ["mint"] }
-cdk-redb = { path = "../cdk-redb", version = "0.5.0", default-features = false, features = ["mint"] }
-cdk-sqlite = { path = "../cdk-sqlite", version = "0.5.0", default-features = false, features = ["mint"] }
+cdk = { path = "../cdk", version = "0.5.0", default-features = false, features = [
+    "mint",
+] }
+cdk-redb = { path = "../cdk-redb", version = "0.5.0", default-features = false, features = [
+    "mint",
+] }
+cdk-sqlite = { path = "../cdk-sqlite", version = "0.5.0", default-features = false, features = [
+    "mint",
+] }
 cdk-cln = { path = "../cdk-cln", version = "0.5.0", default-features = false }
 cdk-lnbits = { path = "../cdk-lnbits", version = "0.5.0", default-features = false }
 cdk-phoenixd = { path = "../cdk-phoenixd", version = "0.5.0", default-features = false }
@@ -25,7 +31,10 @@ cdk-axum = { path = "../cdk-axum", version = "0.5.0", default-features = false }
 config = { version = "0.13.3", features = ["toml"] }
 clap = { version = "4.4.8", features = ["derive", "env", "default"] }
 tokio = { version = "1", default-features = false }
-tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
+tracing = { version = "0.1", default-features = false, features = [
+    "attributes",
+    "log",
+] }
 tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
 futures = { version = "0.3.28", default-features = false }
 serde = { version = "1", default-features = false, features = ["derive"] }
@@ -40,3 +49,4 @@ rand = "0.8.5"
 
 [features]
 swagger = ["cdk-axum/swagger", "dep:utoipa", "dep:utoipa-swagger-ui"]
+redis = ["cdk-axum/redis"]

+ 14 - 3
crates/cdk-mintd/example.config.toml

@@ -6,6 +6,14 @@ mnemonic = ""
 # input_fee_ppk = 0
 # enable_swagger_ui = false
 
+[info.http_cache]
+# memory or redis
+backend = "memory"
+ttl = 60
+tti = 60
+# `key_prefix` and `connection_string` required for redis
+# key_prefix = "mintd"
+# connection_string = "redis://localhost"
 
 
 [mint_info]
@@ -32,9 +40,12 @@ ln_backend = "cln"
 # fee_percent=0.04
 # reserve_fee_min=4
 
-# [cln]
-# Required if using cln backend path to rpc
-# cln_path = ""
+[cln]
+#Required if using cln backend path to rpc
+cln_path = ""
+rpc_path = ""
+fee_percent = 0.02
+reserve_fee_min = 1
 
 # [strike]
 # For the Webhook subscription, the url under [info] must be a valid, absolute, non-local, https url 

+ 15 - 8
crates/cdk-mintd/src/config.rs

@@ -2,6 +2,7 @@ use std::path::PathBuf;
 
 use cdk::nuts::{CurrencyUnit, PublicKey};
 use cdk::Amount;
+use cdk_axum::cache;
 use config::{Config, ConfigError, File};
 use serde::{Deserialize, Serialize};
 
@@ -11,11 +12,10 @@ pub struct Info {
     pub listen_host: String,
     pub listen_port: u16,
     pub mnemonic: String,
-    pub seconds_quote_is_valid_for: Option<u64>,
-    pub seconds_to_cache_requests_for: Option<u64>,
-    pub seconds_to_extend_cache_by: Option<u64>,
     pub input_fee_ppk: Option<u64>,
 
+    pub http_cache: cache::Config,
+
     /// When this is set to true, the mint exposes a Swagger UI for it's API at
     /// `[listen_host]:[listen_port]/swagger-ui`
     ///
@@ -93,6 +93,7 @@ pub struct LNbits {
 #[derive(Debug, Clone, Serialize, Deserialize, Default)]
 pub struct Cln {
     pub rpc_path: PathBuf,
+    #[serde(default)]
     pub bolt12: bool,
     pub fee_percent: f32,
     pub reserve_fee_min: Amount,
@@ -210,7 +211,10 @@ pub struct MintInfo {
 
 impl Settings {
     #[must_use]
-    pub fn new(config_file_name: &Option<PathBuf>) -> Self {
+    pub fn new<P>(config_file_name: Option<P>) -> Self
+    where
+        P: Into<PathBuf>,
+    {
         let default_settings = Self::default();
         // attempt to construct settings with file
         let from_file = Self::new_from_default(&default_settings, config_file_name);
@@ -223,17 +227,20 @@ impl Settings {
         }
     }
 
-    fn new_from_default(
+    fn new_from_default<P>(
         default: &Settings,
-        config_file_name: &Option<PathBuf>,
-    ) -> Result<Self, ConfigError> {
+        config_file_name: Option<P>,
+    ) -> Result<Self, ConfigError>
+    where
+        P: Into<PathBuf>,
+    {
         let mut default_config_file_name = home::home_dir()
             .ok_or(ConfigError::NotFound("Config Path".to_string()))?
             .join("cashu-rs-mint");
 
         default_config_file_name.push("config.toml");
         let config: String = match config_file_name {
-            Some(value) => value.clone().to_string_lossy().to_string(),
+            Some(value) => value.into().to_string_lossy().to_string(),
             None => default_config_file_name.to_string_lossy().to_string(),
         };
         let builder = Config::builder();

+ 4 - 9
crates/cdk-mintd/src/env_vars.rs

@@ -127,22 +127,15 @@ impl Info {
             self.mnemonic = mnemonic;
         }
 
-        // Optional fields
-        if let Ok(seconds_str) = env::var(ENV_SECONDS_QUOTE_VALID) {
-            if let Ok(seconds) = seconds_str.parse() {
-                self.seconds_quote_is_valid_for = Some(seconds);
-            }
-        }
-
         if let Ok(cache_seconds_str) = env::var(ENV_CACHE_SECONDS) {
             if let Ok(seconds) = cache_seconds_str.parse() {
-                self.seconds_to_cache_requests_for = Some(seconds);
+                self.http_cache.ttl = Some(seconds);
             }
         }
 
         if let Ok(extend_cache_str) = env::var(ENV_EXTEND_CACHE_SECONDS) {
             if let Ok(seconds) = extend_cache_str.parse() {
-                self.seconds_to_extend_cache_by = Some(seconds);
+                self.http_cache.tti = Some(seconds);
             }
         }
 
@@ -158,6 +151,8 @@ impl Info {
             }
         }
 
+        self.http_cache = self.http_cache.from_env();
+
         self
     }
 }

+ 19 - 0
crates/cdk-mintd/src/lib.rs

@@ -21,3 +21,22 @@ fn expand_path(path: &str) -> Option<PathBuf> {
         Some(PathBuf::from(path))
     }
 }
+
+#[cfg(test)]
+mod test {
+    use std::env::current_dir;
+
+    use super::*;
+
+    #[test]
+    fn example_is_parsed() {
+        let config = config::Settings::new(Some(format!(
+            "{}/example.config.toml",
+            current_dir().expect("cwd").to_string_lossy()
+        )));
+        let cache = config.info.http_cache;
+
+        assert_eq!(cache.ttl, Some(60));
+        assert_eq!(cache.tti, Some(60));
+    }
+}

+ 8 - 20
crates/cdk-mintd/src/main.rs

@@ -22,6 +22,7 @@ use cdk::nuts::nut17::SupportedMethods;
 use cdk::nuts::nut19::{CachedEndpoint, Method as NUT19Method, Path as NUT19Path};
 use cdk::nuts::{ContactInfo, CurrencyUnit, MintVersion, PaymentMethod};
 use cdk::types::LnKey;
+use cdk_axum::cache::HttpCache;
 use cdk_mintd::cli::CLIArgs;
 use cdk_mintd::config::{self, DatabaseEngine, LnBackend};
 use cdk_mintd::setup::LnBackendSetup;
@@ -36,9 +37,6 @@ use tracing_subscriber::EnvFilter;
 use utoipa::OpenApi;
 
 const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
-const DEFAULT_QUOTE_TTL_SECS: u64 = 1800;
-const DEFAULT_CACHE_TTL_SECS: u64 = 1800;
-const DEFAULT_CACHE_TTI_SECS: u64 = 1800;
 
 #[tokio::main]
 async fn main() -> anyhow::Result<()> {
@@ -72,7 +70,7 @@ async fn main() -> anyhow::Result<()> {
     let mut mint_builder = MintBuilder::new();
 
     let mut settings = if config_file_arg.exists() {
-        config::Settings::new(&Some(config_file_arg))
+        config::Settings::new(Some(config_file_arg))
     } else {
         tracing::info!("Config file does not exist. Attempting to read env vars");
         config::Settings::default()
@@ -302,18 +300,15 @@ async fn main() -> anyhow::Result<()> {
         .with_quote_ttl(10000, 10000)
         .with_seed(mnemonic.to_seed_normalized("").to_vec());
 
-    let cache_ttl = settings
-        .info
-        .seconds_to_cache_requests_for
-        .unwrap_or(DEFAULT_CACHE_TTL_SECS);
-
     let cached_endpoints = vec![
         CachedEndpoint::new(NUT19Method::Post, NUT19Path::MintBolt11),
         CachedEndpoint::new(NUT19Method::Post, NUT19Path::MeltBolt11),
         CachedEndpoint::new(NUT19Method::Post, NUT19Path::Swap),
     ];
 
-    mint_builder = mint_builder.add_cache(Some(cache_ttl), cached_endpoints);
+    let cache: HttpCache = settings.info.http_cache.into();
+
+    mint_builder = mint_builder.add_cache(Some(cache.ttl.as_secs()), cached_endpoints);
 
     let mint = mint_builder.build().await?;
 
@@ -332,16 +327,9 @@ async fn main() -> anyhow::Result<()> {
 
     let listen_addr = settings.info.listen_host;
     let listen_port = settings.info.listen_port;
-    let _quote_ttl = settings
-        .info
-        .seconds_quote_is_valid_for
-        .unwrap_or(DEFAULT_QUOTE_TTL_SECS);
-    let cache_tti = settings
-        .info
-        .seconds_to_extend_cache_by
-        .unwrap_or(DEFAULT_CACHE_TTI_SECS);
-
-    let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint), cache_ttl, cache_tti).await?;
+
+    let v1_service =
+        cdk_axum::create_mint_router_with_custom_cache(Arc::clone(&mint), cache).await?;
 
     let mut mint_service = Router::new()
         .merge(v1_service)

+ 17 - 0
docker-compose.yaml

@@ -13,4 +13,21 @@ services:
       - CDK_MINTD_LISTEN_PORT=8085
       - CDK_MINTD_MNEMONIC=
       - CDK_MINTD_DATABASE=redb
+      - CDK_MINTD_CACHE_BACKEND=memory
+      # - CDK_MINTD_CACHE_REDIS_URL=redis://redis:6379 
+      # - CDK_MINTD_CACHE_REDIS_KEY_PREFIX=cdk-mintd
     command: ["cdk-mintd"]
+    # depends_on:
+    #   - redis
+
+#   redis:
+#     image: redis:7-alpine
+#     container_name: mint_redis
+#     ports:
+#       - "6379:6379"
+#     volumes:
+#       - redis_data:/data
+#     command: redis-server --save 60 1 --loglevel warning
+
+# volumes:
+#    redis_data:

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott