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

fix: config overwrite on start up (#1081)

* fix: config overwrite on start up
thesimplekid 1 hónapja
szülő
commit
aeafab9a10

+ 1 - 14
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<Err = Error> {
 }
 
 /// Base database writer
-#[async_trait]
 pub trait Transaction<'a, Error>:
     DbTransactionFinalizer<Err = Error>
     + 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<Error>:
     async fn begin_transaction<'a>(
         &'a self,
     ) -> Result<Box<dyn Transaction<'a, Error> + Send + Sync + 'a>, Error>;
-
-    /// Get [`MintInfo`]
-    async fn get_mint_info(&self) -> Result<MintInfo, Error>;
-
-    /// Get [`QuoteTTL`]
-    async fn get_quote_ttl(&self) -> Result<QuoteTTL, Error>;
 }
 
 /// Type alias for Mint Database

+ 1 - 0
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,

+ 3 - 6
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<Mint> {
         .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)

+ 5 - 0
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,

+ 1 - 5
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();
 

+ 6 - 0
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)

+ 8 - 0
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<bool>,
+
+    /// 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<QuoteTTL>,
 }
 
 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,
         }
     }
 }

+ 3 - 0
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";

+ 23 - 0
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<u64> = None;
+        let mut melt_ttl_env: Option<u64> = None;
+        if let Ok(mint_ttl_str) = env::var(ENV_QUOTE_TTL_MINT) {
+            if let Ok(v) = mint_ttl_str.parse::<u64>() {
+                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::<u64>() {
+                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
     }
 }

+ 53 - 17
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
 }

+ 2 - 0
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"#)),
 ];

+ 1 - 0
crates/cdk-sql-common/src/mint/migrations/postgres/20250916221000_drop_config_table.sql

@@ -0,0 +1 @@
+DROP TABLE IF EXISTS config;

+ 1 - 0
crates/cdk-sql-common/src/mint/migrations/sqlite/20250916221000_drop_config_table.sql

@@ -0,0 +1 @@
+DROP TABLE IF EXISTS config;

+ 3 - 99
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::<Result<HashMap<_, _>, _>>()
 }
 
-#[inline(always)]
-async fn set_to_config<C, V>(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<RM> SQLMintDatabase<RM>
 where
     RM: DatabasePool + 'static,
@@ -145,21 +124,6 @@ where
         tx.commit().await?;
         Ok(())
     }
-
-    #[inline(always)]
-    async fn fetch_from_config<R>(&self, id: &str) -> Result<R, Error>
-    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<RM> database::MintTransaction<'_, Error> for SQLTransaction<RM>
-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", &quote_ttl).await?)
-    }
-}
+impl<RM> database::MintTransaction<'_, Error> for SQLTransaction<RM> where RM: DatabasePool + 'static
+{}
 
 #[async_trait]
 impl<RM> MintDbWriterFinalizer for SQLTransaction<RM>
@@ -2048,56 +2002,6 @@ where
 
         Ok(Box::new(tx))
     }
-
-    async fn get_mint_info(&self) -> Result<MintInfo, Error> {
-        #[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<QuoteTTL, Error> {
-        #[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<Column>) -> Result<MintKeySetInfo, Error> {

+ 12 - 1
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<MintSqliteDatabase, database::Error> {
     #[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)

+ 33 - 0
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::<MintInfo>(&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);

+ 2 - 2
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;
 

+ 2 - 2
crates/cdk/src/mint/melt.rs

@@ -43,7 +43,7 @@ impl Mint {
         request: String,
         options: Option<MeltOptions>,
     ) -> 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 {

+ 119 - 26
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<MintInfo, Error> {
-        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<QuoteTTL, Error> {
-        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(&quote_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,
+            &quote_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<bool, Error> {
+        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<PaymentProcessorKey, DynMintPayment>,
@@ -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,