diff --git a/crates/cdk-common/src/database/mint/mod.rs b/crates/cdk-common/src/database/mint/mod.rs index f54a9b8c..01af134e 100644 --- a/crates/cdk-common/src/database/mint/mod.rs +++ b/crates/cdk-common/src/database/mint/mod.rs @@ -4,10 +4,9 @@ use std::collections::HashMap; use async_trait::async_trait; use cashu::quote_id::QuoteId; -use cashu::{Amount, MintInfo}; +use cashu::Amount; use super::Error; -use crate::common::QuoteTTL; use crate::mint::{self, MintKeySetInfo, MintQuote as MintMintQuote}; use crate::nuts::{ BlindSignature, BlindedMessage, CurrencyUnit, Id, MeltQuoteState, Proof, Proofs, PublicKey, @@ -396,7 +395,6 @@ pub trait KVStoreTransaction<'a, Error>: DbTransactionFinalizer { } /// Base database writer -#[async_trait] pub trait Transaction<'a, Error>: DbTransactionFinalizer + QuotesTransaction<'a, Err = Error> @@ -404,11 +402,6 @@ pub trait Transaction<'a, Error>: + ProofsTransaction<'a, Err = Error> + KVStoreTransaction<'a, Error> { - /// Set [`QuoteTTL`] - async fn set_quote_ttl(&mut self, quote_ttl: QuoteTTL) -> Result<(), Error>; - - /// Set [`MintInfo`] - async fn set_mint_info(&mut self, mint_info: MintInfo) -> Result<(), Error>; } /// Key-Value Store Database trait @@ -457,12 +450,6 @@ pub trait Database: async fn begin_transaction<'a>( &'a self, ) -> Result + Send + Sync + 'a>, Error>; - - /// Get [`MintInfo`] - async fn get_mint_info(&self) -> Result; - - /// Get [`QuoteTTL`] - async fn get_quote_ttl(&self) -> Result; } /// Type alias for Mint Database diff --git a/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs b/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs index 67dc1f9a..7777ef74 100644 --- a/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs +++ b/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs @@ -266,6 +266,7 @@ fn create_ldk_settings( ) -> cdk_mintd::config::Settings { cdk_mintd::config::Settings { info: cdk_mintd::config::Info { + quote_ttl: None, url: format!("http://127.0.0.1:{port}"), listen_host: "127.0.0.1".to_string(), listen_port: port, diff --git a/crates/cdk-integration-tests/src/init_pure_tests.rs b/crates/cdk-integration-tests/src/init_pure_tests.rs index c9d5db3a..90c88fb3 100644 --- a/crates/cdk-integration-tests/src/init_pure_tests.rs +++ b/crates/cdk-integration-tests/src/init_pure_tests.rs @@ -11,7 +11,7 @@ use bip39::Mnemonic; use cashu::quote_id::QuoteId; use cashu::{MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response}; use cdk::amount::SplitTarget; -use cdk::cdk_database::{self, MintDatabase, WalletDatabase}; +use cdk::cdk_database::{self, WalletDatabase}; use cdk::mint::{MintBuilder, MintMeltLimits}; use cdk::nuts::nut00::ProofsMethods; use cdk::nuts::{ @@ -271,17 +271,14 @@ pub async fn create_and_start_test_mint() -> Result { .with_description("pure test mint".to_string()) .with_urls(vec!["https://aaa".to_string()]); - let tx_localstore = localstore.clone(); - let mut tx = tx_localstore.begin_transaction().await?; - let quote_ttl = QuoteTTL::new(10000, 10000); - tx.set_quote_ttl(quote_ttl).await?; - tx.commit().await?; let mint = mint_builder .build_with_seed(localstore.clone(), &mnemonic.to_seed_normalized("")) .await?; + mint.set_quote_ttl(quote_ttl).await?; + mint.start().await?; Ok(mint) diff --git a/crates/cdk-integration-tests/src/shared.rs b/crates/cdk-integration-tests/src/shared.rs index c052ca9c..4ad50bf1 100644 --- a/crates/cdk-integration-tests/src/shared.rs +++ b/crates/cdk-integration-tests/src/shared.rs @@ -181,6 +181,8 @@ pub fn create_fake_wallet_settings( cdk_mintd::config::Settings { info: cdk_mintd::config::Info { url: format!("http://127.0.0.1:{port}"), + quote_ttl: None, + listen_host: "127.0.0.1".to_string(), listen_port: port, seed: None, @@ -233,6 +235,8 @@ pub fn create_cln_settings( cdk_mintd::config::Settings { info: cdk_mintd::config::Info { url: format!("http://127.0.0.1:{port}"), + quote_ttl: None, + listen_host: "127.0.0.1".to_string(), listen_port: port, seed: None, @@ -278,6 +282,7 @@ pub fn create_lnd_settings( ) -> cdk_mintd::config::Settings { cdk_mintd::config::Settings { info: cdk_mintd::config::Info { + quote_ttl: None, url: format!("http://127.0.0.1:{port}"), listen_host: "127.0.0.1".to_string(), listen_port: port, diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index b1c3eca8..9aa76ad5 100644 --- a/crates/cdk-integration-tests/tests/mint.rs +++ b/crates/cdk-integration-tests/tests/mint.rs @@ -15,7 +15,6 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; use bip39::Mnemonic; -use cdk::cdk_database::MintDatabase; use cdk::mint::{MintBuilder, MintMeltLimits}; use cdk::nuts::{CurrencyUnit, PaymentMethod}; use cdk::types::{FeeReserve, QuoteTTL}; @@ -64,12 +63,9 @@ async fn test_correct_keyset() { .build_with_seed(localstore.clone(), &mnemonic.to_seed_normalized("")) .await .unwrap(); - let mut tx = localstore.begin_transaction().await.unwrap(); let quote_ttl = QuoteTTL::new(10000, 10000); - tx.set_quote_ttl(quote_ttl).await.unwrap(); - - tx.commit().await.unwrap(); + mint.set_quote_ttl(quote_ttl).await.unwrap(); let active = mint.get_active_keysets(); diff --git a/crates/cdk-mintd/example.config.toml b/crates/cdk-mintd/example.config.toml index 6aa7510e..52898fb4 100644 --- a/crates/cdk-mintd/example.config.toml +++ b/crates/cdk-mintd/example.config.toml @@ -7,6 +7,12 @@ mnemonic = "" # input_fee_ppk = 0 # enable_swagger_ui = false +[info.quote_ttl] +# Prefer explicit fields over inline tables for readability and ease of overrides +mint_ttl = 600 +melt_ttl = 120 + + [info.logging] # Where to output logs: "stdout", "file", or "both" (default: "both") # Note: "stdout" actually outputs to stderr (standard error stream) diff --git a/crates/cdk-mintd/src/config.rs b/crates/cdk-mintd/src/config.rs index a04ab7dc..bb35edad 100644 --- a/crates/cdk-mintd/src/config.rs +++ b/crates/cdk-mintd/src/config.rs @@ -4,6 +4,7 @@ use bitcoin::hashes::{sha256, Hash}; use cdk::nuts::{CurrencyUnit, PublicKey}; use cdk::Amount; use cdk_axum::cache; +use cdk_common::common::QuoteTTL; use config::{Config, ConfigError, File}; use serde::{Deserialize, Serialize}; @@ -68,6 +69,12 @@ pub struct Info { /// /// This requires `mintd` was built with the `swagger` feature flag. pub enable_swagger_ui: Option, + + /// Optional persisted quote TTL values (seconds) to initialize the database with + /// when RPC is disabled or on first-run when RPC is enabled. + /// If not provided, defaults are used. + #[serde(skip_serializing_if = "Option::is_none")] + pub quote_ttl: Option, } impl Default for Info { @@ -84,6 +91,7 @@ impl Default for Info { http_cache: cache::Config::default(), enable_swagger_ui: None, logging: LoggingConfig::default(), + quote_ttl: None, } } } diff --git a/crates/cdk-mintd/src/env_vars/common.rs b/crates/cdk-mintd/src/env_vars/common.rs index f8e5b839..f00b9047 100644 --- a/crates/cdk-mintd/src/env_vars/common.rs +++ b/crates/cdk-mintd/src/env_vars/common.rs @@ -14,6 +14,9 @@ pub const ENV_SECONDS_QUOTE_VALID: &str = "CDK_MINTD_SECONDS_QUOTE_VALID"; pub const ENV_CACHE_SECONDS: &str = "CDK_MINTD_CACHE_SECONDS"; pub const ENV_EXTEND_CACHE_SECONDS: &str = "CDK_MINTD_EXTEND_CACHE_SECONDS"; pub const ENV_INPUT_FEE_PPK: &str = "CDK_MINTD_INPUT_FEE_PPK"; +pub const ENV_QUOTE_TTL_MINT: &str = "CDK_MINTD_QUOTE_TTL_MINT"; +pub const ENV_QUOTE_TTL_MELT: &str = "CDK_MINTD_QUOTE_TTL_MELT"; + pub const ENV_ENABLE_SWAGGER: &str = "CDK_MINTD_ENABLE_SWAGGER"; pub const ENV_LOGGING_OUTPUT: &str = "CDK_MINTD_LOGGING_OUTPUT"; pub const ENV_LOGGING_CONSOLE_LEVEL: &str = "CDK_MINTD_LOGGING_CONSOLE_LEVEL"; diff --git a/crates/cdk-mintd/src/env_vars/info.rs b/crates/cdk-mintd/src/env_vars/info.rs index 1ff11ac6..48308762 100644 --- a/crates/cdk-mintd/src/env_vars/info.rs +++ b/crates/cdk-mintd/src/env_vars/info.rs @@ -3,6 +3,8 @@ use std::env; use std::str::FromStr; +use cdk_common::common::QuoteTTL; + use super::common::*; use crate::config::{Info, LoggingOutput}; @@ -85,6 +87,27 @@ impl Info { self.http_cache = self.http_cache.from_env(); + // Quote TTL from env + let mut mint_ttl_env: Option = None; + let mut melt_ttl_env: Option = None; + if let Ok(mint_ttl_str) = env::var(ENV_QUOTE_TTL_MINT) { + if let Ok(v) = mint_ttl_str.parse::() { + mint_ttl_env = Some(v); + } + } + if let Ok(melt_ttl_str) = env::var(ENV_QUOTE_TTL_MELT) { + if let Ok(v) = melt_ttl_str.parse::() { + melt_ttl_env = Some(v); + } + } + if mint_ttl_env.is_some() || melt_ttl_env.is_some() { + let current = self.quote_ttl.unwrap_or_default(); + self.quote_ttl = Some(QuoteTTL { + mint_ttl: mint_ttl_env.unwrap_or(current.mint_ttl), + melt_ttl: melt_ttl_env.unwrap_or(current.melt_ttl), + }); + } + self } } diff --git a/crates/cdk-mintd/src/lib.rs b/crates/cdk-mintd/src/lib.rs index 02203056..3bde4706 100644 --- a/crates/cdk-mintd/src/lib.rs +++ b/crates/cdk-mintd/src/lib.rs @@ -36,8 +36,8 @@ use cdk::nuts::CurrencyUnit; #[cfg(feature = "auth")] use cdk::nuts::{AuthRequired, Method, ProtectedEndpoint, RoutePath}; use cdk::nuts::{ContactInfo, MintVersion, PaymentMethod}; -use cdk::types::QuoteTTL; use cdk_axum::cache::HttpCache; +use cdk_common::common::QuoteTTL; use cdk_common::database::DynMintDatabase; // internal crate modules #[cfg(feature = "prometheus")] @@ -890,16 +890,21 @@ async fn start_services_with_shutdown( } } + // Determine the desired QuoteTTL from config/env or fall back to defaults + let desired_quote_ttl: QuoteTTL = settings.info.quote_ttl.unwrap_or_default(); + if rpc_enabled { if mint.mint_info().await.is_err() { tracing::info!("Mint info not set on mint, setting."); + // First boot with RPC enabled: seed from config mint.set_mint_info(mint_builder_info).await?; - mint.set_quote_ttl(QuoteTTL::new(10_000, 10_000)).await?; + mint.set_quote_ttl(desired_quote_ttl).await?; } else { - if mint.localstore().get_quote_ttl().await.is_err() { - mint.set_quote_ttl(QuoteTTL::new(10_000, 10_000)).await?; + // If QuoteTTL has never been persisted, seed it now from config + if !mint.quote_ttl_is_persisted().await? { + mint.set_quote_ttl(desired_quote_ttl).await?; } - // Add version information + // Add/refresh version information without altering stored mint_info fields let mint_version = MintVersion::new( "cdk-mintd".to_string(), CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(), @@ -911,9 +916,18 @@ async fn start_services_with_shutdown( tracing::info!("Mint info already set, not using config file settings."); } } else { - tracing::info!("RPC not enabled, using mint info from config."); + // RPC disabled: config is source of truth on every boot + tracing::info!("RPC not enabled, using mint info and quote TTL from config."); + let mut mint_builder_info = mint_builder_info; + + if let Ok(mint_info) = mint.mint_info().await { + if mint_builder_info.pubkey.is_none() { + mint_builder_info.pubkey = mint_info.pubkey; + } + } + mint.set_mint_info(mint_builder_info).await?; - mint.set_quote_ttl(QuoteTTL::new(10_000, 10_000)).await?; + mint.set_quote_ttl(desired_quote_ttl).await?; } let mint_info = mint.mint_info().await?; @@ -1120,11 +1134,39 @@ pub async fn run_mintd_with_shutdown( let mint_builder = MintBuilder::new(localstore); + // If RPC is enabled and DB contains mint_info already, initialize the builder from DB. + // This ensures subsequent builder modifications (like version injection) can respect stored values. + let maybe_mint_builder = { + #[cfg(feature = "management-rpc")] + { + if let Some(rpc_settings) = settings.mint_management_rpc.clone() { + if rpc_settings.enabled { + // Best-effort: pull DB state into builder if present + let mut tmp = mint_builder; + if let Err(e) = tmp.init_from_db_if_present().await { + tracing::warn!("Failed to init builder from DB: {}", e); + } + tmp + } else { + mint_builder + } + } else { + mint_builder + } + } + #[cfg(not(feature = "management-rpc"))] + { + mint_builder + } + }; + let mint_builder = - configure_mint_builder(settings, mint_builder, runtime, work_dir, Some(kv)).await?; + configure_mint_builder(settings, maybe_mint_builder, runtime, work_dir, Some(kv)).await?; #[cfg(feature = "auth")] let mint_builder = setup_authentication(settings, work_dir, mint_builder, db_password).await?; + let config_mint_info = mint_builder.current_mint_info(); + let mint = build_mint(settings, keystore, mint_builder).await?; tracing::debug!("Mint built from builder."); @@ -1136,19 +1178,13 @@ pub async fn run_mintd_with_shutdown( // Pending melt quotes where the payment has **failed** inputs are reset to unspent mint.check_pending_melt_quotes().await?; - let result = start_services_with_shutdown( + start_services_with_shutdown( mint.clone(), settings, work_dir, - mint.mint_info().await?, + config_mint_info, shutdown_signal, routers, ) - .await; - - // Ensure any remaining tracing data is flushed - // This is particularly important for file-based logging - tracing::debug!("Flushing remaining trace data"); - - result + .await } diff --git a/crates/cdk-sql-common/src/mint/migrations.rs b/crates/cdk-sql-common/src/mint/migrations.rs index 9609719e..6bfb9fe1 100644 --- a/crates/cdk-sql-common/src/mint/migrations.rs +++ b/crates/cdk-sql-common/src/mint/migrations.rs @@ -6,6 +6,7 @@ pub static MIGRATIONS: &[(&str, &str, &str)] = &[ ("postgres", "20250901090000_add_kv_store.sql", include_str!(r#"./migrations/postgres/20250901090000_add_kv_store.sql"#)), ("postgres", "20250902140000_add_melt_request_and_blinded_messages.sql", include_str!(r#"./migrations/postgres/20250902140000_add_melt_request_and_blinded_messages.sql"#)), ("postgres", "20250903200000_add_signatory_amounts.sql", include_str!(r#"./migrations/postgres/20250903200000_add_signatory_amounts.sql"#)), + ("postgres", "20250916221000_drop_config_table.sql", include_str!(r#"./migrations/postgres/20250916221000_drop_config_table.sql"#)), ("sqlite", "1_fix_sqlx_migration.sql", include_str!(r#"./migrations/sqlite/1_fix_sqlx_migration.sql"#)), ("sqlite", "20240612124932_init.sql", include_str!(r#"./migrations/sqlite/20240612124932_init.sql"#)), ("sqlite", "20240618195700_quote_state.sql", include_str!(r#"./migrations/sqlite/20240618195700_quote_state.sql"#)), @@ -33,4 +34,5 @@ pub static MIGRATIONS: &[(&str, &str, &str)] = &[ ("sqlite", "20250901090000_add_kv_store.sql", include_str!(r#"./migrations/sqlite/20250901090000_add_kv_store.sql"#)), ("sqlite", "20250902140000_add_melt_request_and_blinded_messages.sql", include_str!(r#"./migrations/sqlite/20250902140000_add_melt_request_and_blinded_messages.sql"#)), ("sqlite", "20250903200000_add_signatory_amounts.sql", include_str!(r#"./migrations/sqlite/20250903200000_add_signatory_amounts.sql"#)), + ("sqlite", "20250916221000_drop_config_table.sql", include_str!(r#"./migrations/sqlite/20250916221000_drop_config_table.sql"#)), ]; diff --git a/crates/cdk-sql-common/src/mint/migrations/postgres/20250916221000_drop_config_table.sql b/crates/cdk-sql-common/src/mint/migrations/postgres/20250916221000_drop_config_table.sql new file mode 100644 index 00000000..2efbbee0 --- /dev/null +++ b/crates/cdk-sql-common/src/mint/migrations/postgres/20250916221000_drop_config_table.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS config; diff --git a/crates/cdk-sql-common/src/mint/migrations/sqlite/20250916221000_drop_config_table.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20250916221000_drop_config_table.sql new file mode 100644 index 00000000..2efbbee0 --- /dev/null +++ b/crates/cdk-sql-common/src/mint/migrations/sqlite/20250916221000_drop_config_table.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS config; diff --git a/crates/cdk-sql-common/src/mint/mod.rs b/crates/cdk-sql-common/src/mint/mod.rs index 19b3042c..d8239c40 100644 --- a/crates/cdk-sql-common/src/mint/mod.rs +++ b/crates/cdk-sql-common/src/mint/mod.rs @@ -15,7 +15,6 @@ use std::sync::Arc; use async_trait::async_trait; use bitcoin::bip32::DerivationPath; -use cdk_common::common::QuoteTTL; use cdk_common::database::mint::validate_kvstore_params; use cdk_common::database::{ self, ConversionError, Error, MintDatabase, MintDbWriterFinalizer, MintKeyDatabaseTransaction, @@ -33,7 +32,7 @@ use cdk_common::state::check_state_transition; use cdk_common::util::unix_time; use cdk_common::{ Amount, BlindSignature, BlindSignatureDleq, BlindedMessage, CurrencyUnit, Id, MeltQuoteState, - MintInfo, PaymentMethod, Proof, Proofs, PublicKey, SecretKey, State, + PaymentMethod, Proof, Proofs, PublicKey, SecretKey, State, }; use lightning_invoice::Bolt11Invoice; use migrations::MIGRATIONS; @@ -102,26 +101,6 @@ where .collect::, _>>() } -#[inline(always)] -async fn set_to_config(conn: &C, id: &str, value: &V) -> Result<(), Error> -where - C: DatabaseExecutor + Send + Sync, - V: ?Sized + serde::Serialize, -{ - query( - r#" - INSERT INTO config (id, value) VALUES (:id, :value) - ON CONFLICT(id) DO UPDATE SET value = excluded.value - "#, - )? - .bind("id", id.to_owned()) - .bind("value", serde_json::to_string(&value)?) - .execute(conn) - .await?; - - Ok(()) -} - impl SQLMintDatabase where RM: DatabasePool + 'static, @@ -145,21 +124,6 @@ where tx.commit().await?; Ok(()) } - - #[inline(always)] - async fn fetch_from_config(&self, id: &str) -> Result - where - R: serde::de::DeserializeOwned, - { - let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; - let value = column_as_string!(query(r#"SELECT value FROM config WHERE id = :id LIMIT 1"#)? - .bind("id", id.to_owned()) - .pluck(&*conn) - .await? - .ok_or(Error::UnknownQuoteTTL)?); - - Ok(serde_json::from_str(&value)?) - } } #[async_trait] @@ -307,18 +271,8 @@ where } #[async_trait] -impl database::MintTransaction<'_, Error> for SQLTransaction -where - RM: DatabasePool + 'static, -{ - async fn set_mint_info(&mut self, mint_info: MintInfo) -> Result<(), Error> { - Ok(set_to_config(&self.inner, "mint_info", &mint_info).await?) - } - - async fn set_quote_ttl(&mut self, quote_ttl: QuoteTTL) -> Result<(), Error> { - Ok(set_to_config(&self.inner, "quote_ttl", "e_ttl).await?) - } -} +impl database::MintTransaction<'_, Error> for SQLTransaction where RM: DatabasePool + 'static +{} #[async_trait] impl MintDbWriterFinalizer for SQLTransaction @@ -2048,56 +2002,6 @@ where Ok(Box::new(tx)) } - - async fn get_mint_info(&self) -> Result { - #[cfg(feature = "prometheus")] - METRICS.inc_in_flight_requests("get_mint_info"); - - #[cfg(feature = "prometheus")] - let start_time = std::time::Instant::now(); - - let result = self.fetch_from_config("mint_info").await; - - #[cfg(feature = "prometheus")] - { - let success = result.is_ok(); - - METRICS.record_mint_operation("get_mint_info", success); - METRICS.record_mint_operation_histogram( - "get_mint_info", - success, - start_time.elapsed().as_secs_f64(), - ); - METRICS.dec_in_flight_requests("get_mint_info"); - } - - Ok(result?) - } - - async fn get_quote_ttl(&self) -> Result { - #[cfg(feature = "prometheus")] - METRICS.inc_in_flight_requests("get_quote_ttl"); - - #[cfg(feature = "prometheus")] - let start_time = std::time::Instant::now(); - - let result = self.fetch_from_config("quote_ttl").await; - - #[cfg(feature = "prometheus")] - { - let success = result.is_ok(); - - METRICS.record_mint_operation("get_quote_ttl", success); - METRICS.record_mint_operation_histogram( - "get_quote_ttl", - success, - start_time.elapsed().as_secs_f64(), - ); - METRICS.dec_in_flight_requests("get_quote_ttl"); - } - - Ok(result?) - } } fn sql_row_to_keyset_info(row: Vec) -> Result { diff --git a/crates/cdk-sqlite/src/mint/memory.rs b/crates/cdk-sqlite/src/mint/memory.rs index 7bbb718b..e99f6d1b 100644 --- a/crates/cdk-sqlite/src/mint/memory.rs +++ b/crates/cdk-sqlite/src/mint/memory.rs @@ -8,6 +8,10 @@ use cdk_common::MintInfo; use super::MintSqliteDatabase; +const CDK_MINT_PRIMARY_NAMESPACE: &str = "cdk_mint"; +const CDK_MINT_CONFIG_SECONDARY_NAMESPACE: &str = "config"; +const CDK_MINT_CONFIG_KV_KEY: &str = "mint_info"; + /// Creates a new in-memory [`MintSqliteDatabase`] instance pub async fn empty() -> Result { #[cfg(not(feature = "sqlcipher"))] @@ -54,7 +58,14 @@ pub async fn new_with_state( tx.add_proofs(pending_proofs, None).await?; tx.add_proofs(spent_proofs, None).await?; - tx.set_mint_info(mint_info).await?; + let mint_info_bytes = serde_json::to_vec(&mint_info)?; + tx.kv_write( + CDK_MINT_PRIMARY_NAMESPACE, + CDK_MINT_CONFIG_SECONDARY_NAMESPACE, + CDK_MINT_CONFIG_KV_KEY, + &mint_info_bytes, + ) + .await?; tx.commit().await?; Ok(db) diff --git a/crates/cdk/src/mint/builder.rs b/crates/cdk/src/mint/builder.rs index be9ed4c5..c1c0ee5f 100644 --- a/crates/cdk/src/mint/builder.rs +++ b/crates/cdk/src/mint/builder.rs @@ -82,6 +82,31 @@ impl MintBuilder { self } + /// Initialize builder's MintInfo from the database if present. + /// If not present or parsing fails, keeps the current MintInfo. + pub async fn init_from_db_if_present(&mut self) -> Result<(), cdk_database::Error> { + // Attempt to read existing mint_info from the KV store + let bytes_opt = self + .localstore + .kv_read( + super::CDK_MINT_PRIMARY_NAMESPACE, + super::CDK_MINT_CONFIG_SECONDARY_NAMESPACE, + super::CDK_MINT_CONFIG_KV_KEY, + ) + .await?; + + if let Some(bytes) = bytes_opt { + if let Ok(info) = serde_json::from_slice::(&bytes) { + self.mint_info = info; + } else { + // If parsing fails, leave the current builder state untouched + tracing::warn!("Failed to parse existing mint_info from DB; using builder state"); + } + } + + Ok(()) + } + /// Set blind auth settings #[cfg(feature = "auth")] pub fn with_blind_auth( @@ -128,6 +153,14 @@ impl MintBuilder { self } + /// Get a clone of the current MintInfo configured on the builder + /// This allows using config-derived settings to initialize persistent state + /// before any attempt to read from the database, which avoids first-run + /// failures when the DB is empty. + pub fn current_mint_info(&self) -> MintInfo { + self.mint_info.clone() + } + /// Set terms of service URL pub fn with_tos_url(mut self, tos_url: String) -> Self { self.mint_info.tos_url = Some(tos_url); diff --git a/crates/cdk/src/mint/issue/mod.rs b/crates/cdk/src/mint/issue/mod.rs index 14e8ce9f..45c0268e 100644 --- a/crates/cdk/src/mint/issue/mod.rs +++ b/crates/cdk/src/mint/issue/mod.rs @@ -168,7 +168,7 @@ impl Mint { &self, mint_quote_request: &MintQuoteRequest, ) -> Result<(), Error> { - let mint_info = self.localstore.get_mint_info().await?; + let mint_info = self.mint_info().await?; let unit = mint_quote_request.unit(); let amount = mint_quote_request.amount(); @@ -246,7 +246,7 @@ impl Mint { let payment_options = match mint_quote_request { MintQuoteRequest::Bolt11(bolt11_request) => { - let mint_ttl = self.localstore.get_quote_ttl().await?.mint_ttl; + let mint_ttl = self.quote_ttl().await?.mint_ttl; let quote_expiry = unix_time() + mint_ttl; diff --git a/crates/cdk/src/mint/melt.rs b/crates/cdk/src/mint/melt.rs index 014871a5..580fe62f 100644 --- a/crates/cdk/src/mint/melt.rs +++ b/crates/cdk/src/mint/melt.rs @@ -43,7 +43,7 @@ impl Mint { request: String, options: Option, ) -> Result<(), Error> { - let mint_info = self.localstore.get_mint_info().await?; + let mint_info = self.mint_info().await?; let nut05 = mint_info.nuts.nut05; ensure_cdk!(!nut05.disabled, Error::MeltingDisabled); @@ -196,7 +196,7 @@ impl Mint { Error::UnsupportedUnit })?; - let melt_ttl = self.localstore.get_quote_ttl().await?.melt_ttl; + let melt_ttl = self.quote_ttl().await?.melt_ttl; let quote = MeltQuote::new( MeltPaymentRequest::Bolt11 { diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index 4b429ca1..45c11c39 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -50,6 +50,11 @@ pub use builder::{MintBuilder, MintMeltLimits}; pub use cdk_common::mint::{MeltQuote, MintKeySetInfo, MintQuote}; pub use verification::Verification; +const CDK_MINT_PRIMARY_NAMESPACE: &str = "cdk_mint"; +const CDK_MINT_CONFIG_SECONDARY_NAMESPACE: &str = "config"; +const CDK_MINT_CONFIG_KV_KEY: &str = "mint_info"; +const CDK_MINT_QUOTE_TTL_KV_KEY: &str = "quote_ttl"; + /// Cashu Mint #[derive(Clone)] pub struct Mint { @@ -150,26 +155,60 @@ impl Mint { .count() ); - let mint_info = if mint_info.pubkey.is_none() { - let mut info = mint_info; - info.pubkey = Some(keysets.pubkey); - info - } else { - mint_info - }; + // Persist missing pubkey early to avoid losing it on next boot and ensure stable identity across restarts + let mut computed_info = mint_info; + if computed_info.pubkey.is_none() { + computed_info.pubkey = Some(keysets.pubkey); + } - let mint_store = localstore.clone(); - let mut tx = mint_store.begin_transaction().await?; - tx.set_mint_info(mint_info.clone()).await?; - tx.set_quote_ttl(QuoteTTL::default()).await?; - tx.commit().await?; + match localstore + .kv_read( + CDK_MINT_PRIMARY_NAMESPACE, + CDK_MINT_CONFIG_SECONDARY_NAMESPACE, + CDK_MINT_CONFIG_KV_KEY, + ) + .await? + { + Some(bytes) => { + let mut stored: MintInfo = serde_json::from_slice(&bytes)?; + let mut mutated = false; + if stored.pubkey.is_none() && computed_info.pubkey.is_some() { + stored.pubkey = computed_info.pubkey; + mutated = true; + } + if mutated { + let updated = serde_json::to_vec(&stored)?; + let mut tx = localstore.begin_transaction().await?; + tx.kv_write( + CDK_MINT_PRIMARY_NAMESPACE, + CDK_MINT_CONFIG_SECONDARY_NAMESPACE, + CDK_MINT_CONFIG_KV_KEY, + &updated, + ) + .await?; + tx.commit().await?; + } + } + None => { + let bytes = serde_json::to_vec(&computed_info)?; + let mut tx = localstore.begin_transaction().await?; + tx.kv_write( + CDK_MINT_PRIMARY_NAMESPACE, + CDK_MINT_CONFIG_SECONDARY_NAMESPACE, + CDK_MINT_CONFIG_KV_KEY, + &bytes, + ) + .await?; + tx.commit().await?; + } + } Ok(Self { signatory, pubsub_manager: Arc::new(localstore.clone().into()), localstore, #[cfg(feature = "auth")] - oidc_client: mint_info.nuts.nut21.as_ref().map(|nut21| { + oidc_client: computed_info.nuts.nut21.as_ref().map(|nut21| { OidcClient::new( nut21.openid_discovery.clone(), Some(nut21.client_id.clone()), @@ -373,7 +412,17 @@ impl Mint { /// Get mint info #[instrument(skip_all)] pub async fn mint_info(&self) -> Result { - let mint_info = self.localstore.get_mint_info().await?; + let mint_info = self + .localstore + .kv_read( + CDK_MINT_PRIMARY_NAMESPACE, + CDK_MINT_CONFIG_SECONDARY_NAMESPACE, + CDK_MINT_CONFIG_KV_KEY, + ) + .await? + .ok_or(Error::CouldNotGetMintInfo)?; + + let mint_info: MintInfo = serde_json::from_slice(&mint_info)?; #[cfg(feature = "auth")] let mint_info = if let Some(auth_db) = self.auth_localstore.as_ref() { @@ -415,27 +464,78 @@ impl Mint { /// Set mint info #[instrument(skip_all)] pub async fn set_mint_info(&self, mint_info: MintInfo) -> Result<(), Error> { + tracing::info!("Updating mint info"); + let mint_info_bytes = serde_json::to_vec(&mint_info)?; let mut tx = self.localstore.begin_transaction().await?; - tx.set_mint_info(mint_info).await?; - Ok(tx.commit().await?) + tx.kv_write( + CDK_MINT_PRIMARY_NAMESPACE, + CDK_MINT_CONFIG_SECONDARY_NAMESPACE, + CDK_MINT_CONFIG_KV_KEY, + &mint_info_bytes, + ) + .await?; + tx.commit().await?; + Ok(()) } /// Get quote ttl #[instrument(skip_all)] pub async fn quote_ttl(&self) -> Result { - Ok(self.localstore.get_quote_ttl().await?) + let quote_ttl_bytes = self + .localstore + .kv_read( + CDK_MINT_PRIMARY_NAMESPACE, + CDK_MINT_CONFIG_SECONDARY_NAMESPACE, + CDK_MINT_QUOTE_TTL_KV_KEY, + ) + .await?; + + match quote_ttl_bytes { + Some(bytes) => { + let quote_ttl: QuoteTTL = serde_json::from_slice(&bytes)?; + Ok(quote_ttl) + } + None => { + // Return default if not found + Ok(QuoteTTL::default()) + } + } } /// Set quote ttl #[instrument(skip_all)] pub async fn set_quote_ttl(&self, quote_ttl: QuoteTTL) -> Result<(), Error> { + let quote_ttl_bytes = serde_json::to_vec("e_ttl)?; let mut tx = self.localstore.begin_transaction().await?; - tx.set_quote_ttl(quote_ttl).await?; - Ok(tx.commit().await?) + tx.kv_write( + CDK_MINT_PRIMARY_NAMESPACE, + CDK_MINT_CONFIG_SECONDARY_NAMESPACE, + CDK_MINT_QUOTE_TTL_KV_KEY, + "e_ttl_bytes, + ) + .await?; + tx.commit().await?; + Ok(()) } /// For each backend starts a task that waits for any invoice to be paid /// Once invoice is paid mint quote status is updated + /// Returns true if a QuoteTTL is persisted in the database. This is used to avoid overwriting + /// explicit configuration with defaults when the TTL has already been set by an operator. + #[instrument(skip_all)] + pub async fn quote_ttl_is_persisted(&self) -> Result { + let quote_ttl_bytes = self + .localstore + .kv_read( + CDK_MINT_PRIMARY_NAMESPACE, + CDK_MINT_CONFIG_SECONDARY_NAMESPACE, + CDK_MINT_QUOTE_TTL_KV_KEY, + ) + .await?; + + Ok(quote_ttl_bytes.is_some()) + } + #[instrument(skip_all)] async fn wait_for_paid_invoices( payment_processors: &HashMap, @@ -627,13 +727,6 @@ impl Mint { return Err(Error::AmountUndefined); } - if mint_quote.payment_method == PaymentMethod::Bolt11 - && mint_quote.amount != Some(payment_amount_quote_unit) - { - tracing::error!("Bolt11 incoming payment should equal mint quote."); - return Err(Error::IncorrectQuoteAmount); - } - tracing::debug!( "Payment received amount in quote unit {} {}", mint_quote.unit,