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

---------

Co-authored-by: thesimplekid <tsk@thesimplekid.com>
This commit is contained in:
C
2024-12-17 09:39:03 -03:00
committed by GitHub
parent e508639b83
commit dcca57dbd1
22 changed files with 904 additions and 643 deletions

View File

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

View File

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

887
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@ COPY Cargo.toml ./Cargo.toml
COPY crates ./crates COPY crates ./crates
# Start the Nix daemon and develop the environment # 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 # Create a runtime stage
FROM debian:bookworm-slim FROM debian:bookworm-slim

View File

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

View File

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

View File

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

View File

@@ -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
crates/cdk-axum/src/cache/config.rs vendored Normal file
View File

@@ -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
crates/cdk-axum/src/cache/mod.rs vendored Normal file
View File

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

View File

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

View File

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

View File

@@ -50,11 +50,9 @@ where
); );
let mint = create_mint(database, ln_backends.clone()).await?; let mint = create_mint(database, ln_backends.clone()).await?;
let cache_ttl = 3600;
let cache_tti = 3600;
let mint_arc = Arc::new(mint); 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 .await
.unwrap(); .unwrap();

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,14 @@ mnemonic = ""
# input_fee_ppk = 0 # input_fee_ppk = 0
# enable_swagger_ui = false # 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] [mint_info]
@@ -32,9 +40,12 @@ ln_backend = "cln"
# fee_percent=0.04 # fee_percent=0.04
# reserve_fee_min=4 # reserve_fee_min=4
# [cln] [cln]
# Required if using cln backend path to rpc #Required if using cln backend path to rpc
# cln_path = "" cln_path = ""
rpc_path = ""
fee_percent = 0.02
reserve_fee_min = 1
# [strike] # [strike]
# For the Webhook subscription, the url under [info] must be a valid, absolute, non-local, https url # For the Webhook subscription, the url under [info] must be a valid, absolute, non-local, https url

View File

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

View File

@@ -127,22 +127,15 @@ impl Info {
self.mnemonic = mnemonic; 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(cache_seconds_str) = env::var(ENV_CACHE_SECONDS) {
if let Ok(seconds) = cache_seconds_str.parse() { 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(extend_cache_str) = env::var(ENV_EXTEND_CACHE_SECONDS) {
if let Ok(seconds) = extend_cache_str.parse() { 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 self
} }
} }

View File

@@ -21,3 +21,22 @@ fn expand_path(path: &str) -> Option<PathBuf> {
Some(PathBuf::from(path)) 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));
}
}

View File

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

View File

@@ -13,4 +13,21 @@ services:
- CDK_MINTD_LISTEN_PORT=8085 - CDK_MINTD_LISTEN_PORT=8085
- CDK_MINTD_MNEMONIC= - CDK_MINTD_MNEMONIC=
- CDK_MINTD_DATABASE=redb - 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"] 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: