mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-22 07:04:56 +01:00
Introduce pluggable backend cache for the HTTP layer. (#495)
--------- Co-authored-by: thesimplekid <tsk@thesimplekid.com>
This commit is contained in:
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -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,2 +1,2 @@
|
||||
[language-server.rust-analyzer.config]
|
||||
cargo = { features = ["wallet", "mint", "swagger"] }
|
||||
cargo = { features = ["wallet", "mint", "swagger", "redis"] }
|
||||
|
||||
887
Cargo.lock
generated
887
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
crates/cdk-axum/src/cache/backend/memory.rs
vendored
Normal file
45
crates/cdk-axum/src/cache/backend/memory.rs
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
7
crates/cdk-axum/src/cache/backend/mod.rs
vendored
Normal file
7
crates/cdk-axum/src/cache/backend/mod.rs
vendored
Normal 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};
|
||||
96
crates/cdk-axum/src/cache/backend/redis.rs
vendored
Normal file
96
crates/cdk-axum/src/cache/backend/redis.rs
vendored
Normal 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
102
crates/cdk-axum/src/cache/config.rs
vendored
Normal 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
188
crates/cdk-axum/src/cache/mod.rs
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,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))
|
||||
})?;
|
||||
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(&cache_key) {
|
||||
return Ok(Json(serde_json::from_str(&cached_response)
|
||||
.expect("Shouldn't panic: response is json-deserializable.")));
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user