Skip to main content

cdk_mintd/
lib.rs

1#![allow(missing_docs)]
2//! Cdk mintd lib
3
4// std
5use std::collections::{HashMap, HashSet};
6use std::env::{self};
7use std::net::SocketAddr;
8use std::path::{Path, PathBuf};
9use std::str::FromStr;
10use std::sync::Arc;
11
12// external crates
13use anyhow::{anyhow, bail, Context, Result};
14use axum::extract::DefaultBodyLimit;
15use axum::Router;
16use bip39::Mnemonic;
17use cdk::cdk_database::{self, KVStore, KVStoreCompareAndSwap, MintDatabase, MintKeysDatabase};
18use cdk::mint::{Mint, MintBuilder, MintMeltLimits};
19use cdk::nuts::nut00::KnownMethod;
20#[cfg(any(
21    feature = "cln",
22    feature = "lnd",
23    feature = "ldk-node",
24    feature = "fakewallet",
25    feature = "bdk",
26    feature = "grpc-processor"
27))]
28use cdk::nuts::nut17::SupportedMethods;
29use cdk::nuts::nut19::{CachedEndpoint, Method as NUT19Method, Path as NUT19Path};
30use cdk::nuts::{
31    AuthRequired, ContactInfo, Method, MintVersion, PaymentMethod, ProtectedEndpoint, RoutePath,
32};
33use cdk_axum::cache::HttpCache;
34use cdk_common::common::QuoteTTL;
35use cdk_common::database::DynMintDatabase;
36// internal crate modules
37#[cfg(feature = "prometheus")]
38use cdk_common::payment::MetricsMintPayment;
39use cdk_common::payment::MintPayment;
40#[cfg(feature = "postgres")]
41use cdk_postgres::{MintPgAuthDatabase, MintPgDatabase, PgConfig};
42#[cfg(feature = "sqlite")]
43use cdk_sqlite::mint::MintSqliteAuthDatabase;
44#[cfg(feature = "sqlite")]
45use cdk_sqlite::MintSqliteDatabase;
46use cli::CLIArgs;
47use config::{AuthType, DatabaseEngine, PaymentBackendType};
48use env_vars::ENV_WORK_DIR;
49use setup::PaymentBackendSetup;
50use tower::ServiceBuilder;
51use tower_http::compression::CompressionLayer;
52use tower_http::decompression::RequestDecompressionLayer;
53use tower_http::trace::TraceLayer;
54use tracing_appender::{non_blocking, rolling};
55use tracing_subscriber::fmt::writer::MakeWriterExt;
56use tracing_subscriber::EnvFilter;
57
58pub mod cli;
59pub mod config;
60mod config_migration;
61mod config_service;
62mod config_store;
63pub mod env_vars;
64mod secret;
65pub mod setup;
66
67pub use config_migration::{migrate_legacy_configuration, MigrationOutcome};
68pub use config_service::{ApplyOutcome, RollbackOutcome};
69
70#[cfg(test)]
71pub(crate) mod test_utils {
72    use std::path::PathBuf;
73    use std::sync::atomic::{AtomicUsize, Ordering};
74    use std::sync::{Mutex, MutexGuard, OnceLock};
75
76    pub(crate) fn env_lock() -> MutexGuard<'static, ()> {
77        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
78        LOCK.get_or_init(|| Mutex::new(()))
79            .lock()
80            .unwrap_or_else(|e| e.into_inner())
81    }
82
83    pub(crate) fn unique_temp_path(name: &str) -> PathBuf {
84        static COUNTER: AtomicUsize = AtomicUsize::new(0);
85        std::env::temp_dir().join(format!(
86            "{name}_{}_{}",
87            std::process::id(),
88            COUNTER.fetch_add(1, Ordering::Relaxed)
89        ))
90    }
91}
92
93const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
94const DEFAULT_BATCH_MINT_SIZE: u64 = 100;
95const REQUEST_BODY_LIMIT_BYTES: usize = 1_048_576;
96
97type DynSignatory = Arc<dyn cdk_signatory::signatory::Signatory + Send + Sync>;
98
99#[derive(Clone)]
100struct ValidatedSigningSource {
101    expected_pubkey: cdk::nuts::PublicKey,
102    remote_signatory: Option<DynSignatory>,
103}
104
105impl std::fmt::Debug for ValidatedSigningSource {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        f.debug_struct("ValidatedSigningSource")
108            .field("expected_pubkey", &self.expected_pubkey)
109            .field("remote_signatory", &self.remote_signatory.is_some())
110            .finish()
111    }
112}
113
114/// Drives the startup-selected configuration document through its
115/// activation lifecycle and centralizes the policy derived from it.
116///
117/// The activation phase is derived from the persisted
118/// [`config_store::DocumentState`]: a `Pending` document is *activating*
119/// during this startup — its canonical values are forced into the database
120/// and, once every service is up, the record is committed `Applied`. An
121/// `Applied` document keeps RPC-managed canonical values.
122#[derive(Debug, Clone)]
123struct ConfigurationActivation {
124    service: config_service::ConfigurationService,
125    phase: ActivationPhase,
126}
127
128/// Phase of the document being activated by this startup.
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130enum ActivationPhase {
131    /// The document was never served by a daemon: force its canonical
132    /// values and commit `revision` as applied once all services are up.
133    Pending {
134        /// Revision expected to be committed by this startup.
135        revision: u64,
136    },
137    /// The document was served by a previous daemon run.
138    Applied,
139}
140
141impl ConfigurationActivation {
142    fn new(
143        service: config_service::ConfigurationService,
144        state: config_store::DocumentState,
145        revision: u64,
146    ) -> Self {
147        let phase = match state {
148            config_store::DocumentState::Pending => ActivationPhase::Pending { revision },
149            config_store::DocumentState::Applied => ActivationPhase::Applied,
150        };
151        Self { service, phase }
152    }
153
154    /// Whether startup forces the document's canonical mint info and quote
155    /// TTL into the database instead of preserving RPC-managed values.
156    fn forces_configuration(&self) -> bool {
157        matches!(self.phase, ActivationPhase::Pending { .. })
158    }
159
160    /// Whether startup preserves RPC-managed canonical database values.
161    fn preserves_database_values(&self, rpc_enabled: bool) -> bool {
162        rpc_enabled && !self.forces_configuration()
163    }
164
165    /// Commits the document as applied now that every service is up.
166    ///
167    /// A document that was already applied is left untouched. A document
168    /// replaced by a concurrent `config apply` during startup stays pending
169    /// for the next restart.
170    async fn mark_applied(&self) -> Result<()> {
171        if let ActivationPhase::Pending { revision } = self.phase {
172            if !self.service.mark_applied(revision).await? {
173                tracing::info!(
174                    "A newer configuration was stored during startup and remains unapplied for the next restart."
175                );
176            }
177        }
178        Ok(())
179    }
180}
181
182#[cfg(feature = "management-rpc")]
183#[derive(Debug, Clone)]
184struct ConfigurationMutationGuard {
185    service: config_service::ConfigurationService,
186}
187
188#[cfg(feature = "management-rpc")]
189#[async_trait::async_trait]
190impl cdk_mint_rpc::MintMutationGuard for ConfigurationMutationGuard {
191    async fn check(&self) -> Result<(), cdk_mint_rpc::MintMutationGuardError> {
192        match self.service.has_pending_configuration().await {
193            Ok(true) => Err(cdk_mint_rpc::MintMutationGuardError::FailedPrecondition(
194                "A configuration apply is pending; restart cdk-mintd before making management RPC changes"
195                    .to_owned(),
196            )),
197            Ok(false) => Ok(()),
198            Err(error) => Err(cdk_mint_rpc::MintMutationGuardError::Internal(format!(
199                "Could not inspect the stored configuration state: {error}"
200            ))),
201        }
202    }
203}
204
205#[cfg(all(feature = "management-rpc", feature = "bdk"))]
206type ConfiguredWalletInfoProvider = Option<cdk_mint_rpc::DynWalletInfoProvider>;
207#[cfg(not(all(feature = "management-rpc", feature = "bdk")))]
208type ConfiguredWalletInfoProvider = ();
209
210#[cfg(all(feature = "management-rpc", feature = "bdk"))]
211#[derive(Clone)]
212struct BdkWalletInfoProvider {
213    bdk: Arc<cdk_bdk::CdkBdk>,
214}
215
216#[cfg(all(feature = "management-rpc", feature = "bdk"))]
217#[async_trait::async_trait]
218impl cdk_mint_rpc::WalletInfoProvider for BdkWalletInfoProvider {
219    async fn create_deposit_address(
220        &self,
221    ) -> std::result::Result<String, cdk_mint_rpc::WalletInfoError> {
222        self.bdk
223            .create_operator_deposit_address()
224            .await
225            .map_err(|err| cdk_mint_rpc::WalletInfoError::new(err.to_string()))
226    }
227
228    async fn get_balance(
229        &self,
230    ) -> std::result::Result<cdk_mint_rpc::wallet::GetBalanceResponse, cdk_mint_rpc::WalletInfoError>
231    {
232        let balance = self.bdk.wallet_balance().await;
233
234        Ok(cdk_mint_rpc::wallet::GetBalanceResponse {
235            confirmed_sat: balance.confirmed_sat,
236            trusted_pending_sat: balance.trusted_pending_sat,
237            untrusted_pending_sat: balance.untrusted_pending_sat,
238            immature_sat: balance.immature_sat,
239            trusted_spendable_sat: balance.trusted_spendable_sat,
240            total_sat: balance.total_sat,
241            network: balance.network,
242            synced_height: balance.synced_height,
243        })
244    }
245
246    async fn list_transactions(
247        &self,
248        offset: usize,
249        limit: usize,
250    ) -> std::result::Result<cdk_mint_rpc::WalletTransactionPage, cdk_mint_rpc::WalletInfoError>
251    {
252        let page = self
253            .bdk
254            .wallet_transactions(offset, limit)
255            .await
256            .map_err(|err| cdk_mint_rpc::WalletInfoError::new(err.to_string()))?;
257
258        Ok(cdk_mint_rpc::WalletTransactionPage {
259            transactions: page
260                .items
261                .into_iter()
262                .map(|transaction| cdk_mint_rpc::wallet::WalletTransaction {
263                    txid: transaction.txid,
264                    inputs: transaction
265                        .inputs
266                        .into_iter()
267                        .map(|input| cdk_mint_rpc::wallet::WalletTransactionInput {
268                            txid: input.txid,
269                            vout: input.vout,
270                            amount_sat: input.amount_sat,
271                            address: input.address,
272                        })
273                        .collect(),
274                    outputs: transaction
275                        .outputs
276                        .into_iter()
277                        .map(|output| cdk_mint_rpc::wallet::WalletTransactionOutput {
278                            vout: output.vout,
279                            address: output.address,
280                            amount_sat: output.amount_sat,
281                            quote_id: output.quote_id,
282                        })
283                        .collect(),
284                    received_sat: transaction.received_sat,
285                    sent_sat: transaction.sent_sat,
286                    fee_sat: transaction.fee_sat,
287                    balance_delta_sat: transaction.balance_delta_sat,
288                    confirmation_height: transaction.confirmation_height,
289                    confirmation_time: transaction.confirmation_time,
290                    first_seen: transaction.first_seen,
291                })
292                .collect(),
293            total: page.total,
294        })
295    }
296
297    async fn list_addresses(
298        &self,
299        offset: usize,
300        limit: usize,
301    ) -> std::result::Result<cdk_mint_rpc::WalletAddressPage, cdk_mint_rpc::WalletInfoError> {
302        let page = self
303            .bdk
304            .wallet_addresses(offset, limit)
305            .await
306            .map_err(|err| cdk_mint_rpc::WalletInfoError::new(err.to_string()))?;
307
308        Ok(cdk_mint_rpc::WalletAddressPage {
309            addresses: page
310                .items
311                .into_iter()
312                .map(|address| cdk_mint_rpc::wallet::WalletAddress {
313                    address: address.address,
314                    keychain: match address.keychain {
315                        cdk_bdk::WalletKeychain::External => {
316                            cdk_mint_rpc::wallet::KeychainKind::External.into()
317                        }
318                        cdk_bdk::WalletKeychain::Internal => {
319                            cdk_mint_rpc::wallet::KeychainKind::Internal.into()
320                        }
321                    },
322                    derivation_index: address.derivation_index,
323                    used: address.used,
324                    balance_sat: address.balance_sat,
325                    confirmed_balance_sat: address.confirmed_balance_sat,
326                })
327                .collect(),
328            total: page.total,
329        })
330    }
331}
332
333#[cfg(all(feature = "management-rpc", feature = "bdk"))]
334fn no_wallet_info_provider() -> ConfiguredWalletInfoProvider {
335    None
336}
337
338#[cfg(not(all(feature = "management-rpc", feature = "bdk")))]
339fn no_wallet_info_provider() -> ConfiguredWalletInfoProvider {}
340
341fn extract_supported_payment_methods(mint_info: &cdk::nuts::MintInfo) -> Vec<String> {
342    let mut seen = HashSet::new();
343    mint_info
344        .nuts
345        .nut04
346        .methods
347        .iter()
348        .map(|method| method.method.to_string())
349        .filter(|method| seen.insert(method.clone()))
350        .collect()
351}
352
353#[cfg(feature = "cln")]
354fn expand_path(path: &str) -> Option<PathBuf> {
355    if path == "~" {
356        return home::home_dir();
357    }
358
359    if let Some(remainder) = path.strip_prefix("~/") {
360        return home::home_dir().map(|home_dir| home_dir.join(remainder));
361    }
362
363    Some(PathBuf::from(path))
364}
365
366/// Performs the initial setup for the application, including configuring tracing,
367/// parsing CLI arguments, setting up the working directory, loading settings,
368/// and initializing the database connection.
369async fn initial_setup(
370    work_dir: &Path,
371    settings: &config::Settings,
372    db_password: Option<String>,
373) -> Result<(
374    DynMintDatabase,
375    Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
376    Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync>,
377    Arc<dyn KVStoreCompareAndSwap<Err = cdk_database::Error> + Send + Sync>,
378)> {
379    tracing::info!("Initializing database...");
380    let (localstore, keystore, kv, configuration_store) =
381        setup_database(settings, work_dir, db_password).await?;
382    tracing::info!("Database initialized successfully");
383    Ok((localstore, keystore, kv, configuration_store))
384}
385
386/// Sets up and initializes a tracing subscriber with custom log filtering.
387/// Logs can be configured to output to stdout only, file only, or both.
388/// Returns a guard that must be kept alive and properly dropped on shutdown.
389pub fn setup_tracing(
390    work_dir: &Path,
391    logging_config: &config::LoggingConfig,
392) -> Result<Option<tracing_appender::non_blocking::WorkerGuard>> {
393    let default_filter = "debug";
394    let hyper_filter = "hyper=warn,rustls=warn,reqwest=warn";
395    let h2_filter = "h2=warn";
396    let tower_filter = "tower=warn";
397    let tower_http = "tower_http=warn";
398    let rustls = "rustls=warn";
399    let tungstenite = "tungstenite=warn";
400    let tokio_postgres = "tokio_postgres=warn";
401
402    let env_filter = EnvFilter::new(format!(
403        "{default_filter},{hyper_filter},{h2_filter},{tower_filter},{tower_http},{rustls},{tungstenite},{tokio_postgres}"
404    ));
405
406    use config::LoggingOutput;
407    match logging_config.output {
408        LoggingOutput::Stderr => {
409            // Console output only (stderr)
410            let console_level = logging_config
411                .console_level
412                .as_deref()
413                .unwrap_or("info")
414                .parse::<tracing::Level>()
415                .unwrap_or(tracing::Level::INFO);
416
417            let stderr = std::io::stderr.with_max_level(console_level);
418
419            tracing_subscriber::fmt()
420                .with_env_filter(env_filter)
421                .with_ansi(false)
422                .with_writer(stderr)
423                .init();
424
425            tracing::info!("Logging initialized: console only ({}+)", console_level);
426            Ok(None)
427        }
428        LoggingOutput::File => {
429            // File output only
430            let file_level = logging_config
431                .file_level
432                .as_deref()
433                .unwrap_or("debug")
434                .parse::<tracing::Level>()
435                .unwrap_or(tracing::Level::DEBUG);
436
437            // Create logs directory in work_dir if it doesn't exist
438            let logs_dir = work_dir.join("logs");
439            std::fs::create_dir_all(&logs_dir)?;
440
441            // Set up file appender with daily rotation
442            let file_appender = rolling::daily(&logs_dir, "cdk-mintd.log");
443            let (non_blocking_appender, guard) = non_blocking(file_appender);
444
445            let file_writer = non_blocking_appender.with_max_level(file_level);
446
447            tracing_subscriber::fmt()
448                .with_env_filter(env_filter)
449                .with_ansi(false)
450                .with_writer(file_writer)
451                .init();
452
453            tracing::info!(
454                "Logging initialized: file only at {}/cdk-mintd.log ({}+)",
455                logs_dir.display(),
456                file_level
457            );
458            Ok(Some(guard))
459        }
460        LoggingOutput::Both => {
461            // Both console and file output (stderr + file)
462            let console_level = logging_config
463                .console_level
464                .as_deref()
465                .unwrap_or("info")
466                .parse::<tracing::Level>()
467                .unwrap_or(tracing::Level::INFO);
468            let file_level = logging_config
469                .file_level
470                .as_deref()
471                .unwrap_or("debug")
472                .parse::<tracing::Level>()
473                .unwrap_or(tracing::Level::DEBUG);
474
475            // Create logs directory in work_dir if it doesn't exist
476            let logs_dir = work_dir.join("logs");
477            std::fs::create_dir_all(&logs_dir)?;
478
479            // Set up file appender with daily rotation
480            let file_appender = rolling::daily(&logs_dir, "cdk-mintd.log");
481            let (non_blocking_appender, guard) = non_blocking(file_appender);
482
483            // Combine console output (stderr) and file output
484            let stderr = std::io::stderr.with_max_level(console_level);
485            let file_writer = non_blocking_appender.with_max_level(file_level);
486
487            tracing_subscriber::fmt()
488                .with_env_filter(env_filter)
489                .with_ansi(false)
490                .with_writer(stderr.and(file_writer))
491                .init();
492
493            tracing::info!(
494                "Logging initialized: console ({}+) and file at {}/cdk-mintd.log ({}+)",
495                console_level,
496                logs_dir.display(),
497                file_level
498            );
499            Ok(Some(guard))
500        }
501    }
502}
503
504/// Retrieves the work directory based on command-line arguments, environment variables, or system defaults.
505pub async fn get_work_directory(args: &CLIArgs) -> Result<PathBuf> {
506    let work_dir = if let Some(work_dir) = &args.work_dir {
507        tracing::info!("Using work dir from cmd arg");
508        work_dir.clone()
509    } else if let Ok(env_work_dir) = env::var(ENV_WORK_DIR) {
510        tracing::info!("Using work dir from env var");
511        env_work_dir.into()
512    } else {
513        work_dir()?
514    };
515    tracing::info!("Using work dir: {}", work_dir.display());
516    Ok(work_dir)
517}
518
519/// Loads the application settings based on a configuration file and environment variables.
520pub fn load_settings(work_dir: &Path, config_path: Option<PathBuf>) -> Result<config::Settings> {
521    let settings = load_settings_from_sources(work_dir, config_path)?;
522    validate_settings(&settings)?;
523
524    Ok(settings)
525}
526
527fn load_settings_from_sources(
528    work_dir: &Path,
529    config_path: Option<PathBuf>,
530) -> Result<config::Settings> {
531    // get config file name from args
532    let config_file_arg = match config_path {
533        Some(c) => c,
534        None => work_dir.join("config.toml"),
535    };
536
537    let mut settings = if config_file_arg.exists() {
538        config::Settings::try_new(Some(config_file_arg.clone()))
539            .with_context(|| format!("Failed to read config file {}", config_file_arg.display()))?
540    } else {
541        tracing::info!("Config file does not exist. Attempting to read env vars");
542        config::Settings::default()
543    };
544    // This check for any settings defined in ENV VARs
545    // ENV VARS will take **priority** over those in the config
546    settings.from_env()
547}
548
549pub(crate) fn validate_settings(settings: &config::Settings) -> Result<()> {
550    validate_payment_backends(settings)?;
551    settings
552        .validate_backend_pairing()
553        .map_err(anyhow::Error::msg)?;
554    validate_listen_config(settings)?;
555    validate_signing_config(settings)?;
556    validate_payment_backend_config(settings)?;
557    validate_onchain_config(settings)?;
558    validate_database_config(settings)?;
559    validate_auth_config(settings)?;
560    validate_management_rpc_config(settings)?;
561    validate_prometheus_config(settings)?;
562
563    Ok(())
564}
565
566fn validate_payment_backends(settings: &config::Settings) -> Result<()> {
567    let has_payment_backend = settings
568        .payment_backend
569        .iter()
570        .any(|backend| backend.backend != PaymentBackendType::None);
571    let has_onchain_backend = settings
572        .onchain
573        .as_ref()
574        .is_some_and(|onchain| onchain.onchain_backend != config::OnchainBackend::None);
575
576    if !has_payment_backend && !has_onchain_backend {
577        bail!("At least one payment backend must be configured");
578    }
579
580    Ok(())
581}
582
583fn validate_database_config(settings: &config::Settings) -> Result<()> {
584    if settings.database.engine == DatabaseEngine::Postgres {
585        let pg_config = settings.database.postgres.as_ref().ok_or_else(|| {
586            anyhow!("PostgreSQL configuration is required when using PostgreSQL engine")
587        })?;
588
589        if pg_config.url.is_empty() {
590            bail!("PostgreSQL URL is required. Set it in config file [database.postgres] section or via CDK_MINTD_POSTGRES_URL/CDK_MINTD_DATABASE_URL environment variable");
591        }
592    }
593
594    Ok(())
595}
596
597fn validate_listen_config(settings: &config::Settings) -> Result<()> {
598    format!(
599        "{}:{}",
600        settings.info.listen_host, settings.info.listen_port
601    )
602    .parse::<SocketAddr>()
603    .map_err(|err| {
604        anyhow!(
605            "Invalid mint listen address [info].listen_host/[info].listen_port ({}:{}): {err}",
606            settings.info.listen_host,
607            settings.info.listen_port
608        )
609    })?;
610
611    Ok(())
612}
613
614fn validate_signing_config(settings: &config::Settings) -> Result<()> {
615    const MIN_SEED_BYTES: usize = 32;
616
617    if let Some(signatory) = settings.enabled_signatory() {
618        let has_local_seed = settings
619            .info
620            .seed
621            .as_ref()
622            .is_some_and(|seed| !seed.is_empty());
623        let has_local_mnemonic = settings
624            .info
625            .mnemonic
626            .as_ref()
627            .is_some_and(|mnemonic| !mnemonic.is_empty());
628        if has_local_seed || has_local_mnemonic {
629            bail!(
630                "Remote signatory configuration cannot include [info].seed or [info].mnemonic; \
631                 keep private signing material on the signatory host"
632            );
633        }
634
635        if signatory.tls_dir.is_none() && !signatory.allow_insecure {
636            bail!(
637                "gRPC signatory TLS is not configured. Set [signatory].tls_dir or \
638                 [signatory].allow_insecure = true to connect without TLS"
639            );
640        }
641
642        return Ok(());
643    }
644
645    let seed = settings.info.seed.as_ref();
646    let mnemonic = settings
647        .info
648        .mnemonic
649        .as_ref()
650        .filter(|value| !value.is_empty());
651
652    if let Some(seed) = seed {
653        if seed.is_empty() {
654            bail!("Seed in [info].seed must not be empty");
655        }
656        if seed.len() < MIN_SEED_BYTES {
657            bail!(
658                "Seed in [info].seed is too short ({} bytes); require at least {MIN_SEED_BYTES} bytes",
659                seed.len()
660            );
661        }
662        return Ok(());
663    }
664
665    if let Some(mnemonic) = mnemonic {
666        Mnemonic::from_str(mnemonic)
667            .map_err(|err| anyhow!("Invalid mnemonic in [info].mnemonic: {err}"))?;
668        return Ok(());
669    }
670
671    bail!("No signing source configured. Set [info].mnemonic or [info].seed to an env:/file: secret reference, or enable [signatory]");
672}
673
674fn validate_payment_backend_config(settings: &config::Settings) -> Result<()> {
675    // `validate_payment_backends` already permits valid on-chain-only configs,
676    // so an empty `payment_backend` simply skips the backend-specific checks below.
677    for payment_backend in &settings.payment_backend {
678        if payment_backend.min_mint > payment_backend.max_mint {
679            bail!("Payment backend min_mint cannot be greater than max_mint");
680        }
681        if payment_backend.min_melt > payment_backend.max_melt {
682            bail!("Payment backend min_melt cannot be greater than max_melt");
683        }
684
685        match payment_backend.backend {
686            PaymentBackendType::None => {}
687            #[cfg(feature = "cln")]
688            PaymentBackendType::Cln => {
689                let cln = settings.cln.as_ref().ok_or_else(|| {
690                    anyhow!("CLN backend selected but [cln] config section is missing")
691                })?;
692                if cln.rpc_path.as_os_str().is_empty() {
693                    bail!("CLN rpc_path must be set in [cln].rpc_path");
694                }
695            }
696            #[cfg(feature = "lnd")]
697            PaymentBackendType::Lnd => {
698                let lnd = settings.lnd.as_ref().ok_or_else(|| {
699                    anyhow!("LND backend selected but [lnd] config section is missing")
700                })?;
701                if lnd.address.is_empty() {
702                    bail!("LND address must be set in [lnd].address");
703                }
704                if lnd.cert_file.as_os_str().is_empty() {
705                    bail!("LND cert_file must be set in [lnd].cert_file");
706                }
707                if lnd.macaroon_file.as_os_str().is_empty() {
708                    bail!("LND macaroon_file must be set in [lnd].macaroon_file");
709                }
710            }
711            #[cfg(feature = "fakewallet")]
712            PaymentBackendType::FakeWallet => {
713                let fake_wallet = settings.fake_wallet.as_ref().ok_or_else(|| {
714                    anyhow!(
715                        "Fake wallet backend selected but [fake_wallet] config section is missing"
716                    )
717                })?;
718                if fake_wallet.supported_units.is_empty() {
719                    bail!("Fake wallet supported_units must contain at least one unit in [fake_wallet].supported_units");
720                }
721                if fake_wallet.min_delay_time > fake_wallet.max_delay_time {
722                    bail!("Fake wallet min_delay_time cannot be greater than max_delay_time");
723                }
724            }
725            #[cfg(feature = "grpc-processor")]
726            PaymentBackendType::GrpcProcessor => {
727                let grpc_processor = settings.grpc_processor.as_ref().ok_or_else(|| {
728                    anyhow!(
729                        "gRPC payment processor backend selected but [grpc_processor] config section is missing"
730                    )
731                })?;
732                if grpc_processor.supported_units.is_empty() {
733                    bail!("gRPC payment processor supported_units must contain at least one unit in [grpc_processor].supported_units");
734                }
735                if grpc_processor.address.is_empty() {
736                    bail!("gRPC payment processor address must be set in [grpc_processor].address");
737                }
738            }
739            #[cfg(feature = "ldk-node")]
740            PaymentBackendType::LdkNode => {
741                if settings.ldk_node.is_none() {
742                    bail!("LDK Node backend selected but [ldk_node] config section is missing");
743                }
744            }
745        }
746    }
747
748    Ok(())
749}
750
751fn validate_onchain_config(settings: &config::Settings) -> Result<()> {
752    let Some(onchain) = settings.onchain.as_ref() else {
753        return Ok(());
754    };
755
756    if onchain.min_mint > onchain.max_mint {
757        bail!("On-chain min_mint cannot be greater than max_mint");
758    }
759    if onchain.min_melt > onchain.max_melt {
760        bail!("On-chain min_melt cannot be greater than max_melt");
761    }
762
763    match onchain.onchain_backend {
764        config::OnchainBackend::None => {}
765        #[cfg(feature = "bdk")]
766        config::OnchainBackend::Bdk => {
767            let bdk = settings.bdk.as_ref().ok_or_else(|| {
768                anyhow!("BDK onchain backend selected but [bdk] config section is missing")
769            })?;
770            bdk.validate().map_err(anyhow::Error::msg)?;
771        }
772        #[cfg(feature = "fakewallet")]
773        config::OnchainBackend::FakeWallet => {
774            if settings.fake_wallet.is_none() {
775                bail!(
776                    "Fake wallet onchain backend selected but [fake_wallet] config section is missing"
777                );
778            }
779        }
780    }
781
782    Ok(())
783}
784
785fn validate_auth_config(settings: &config::Settings) -> Result<()> {
786    let Some(auth) = settings.auth.as_ref() else {
787        return Ok(());
788    };
789
790    if auth.openid_discovery.is_empty() {
791        bail!("Auth openid_discovery must be set in [auth].openid_discovery");
792    }
793    if auth.openid_client_id.is_empty() {
794        bail!("Auth openid_client_id must be set in [auth].openid_client_id");
795    }
796
797    if settings.database.engine == DatabaseEngine::Postgres {
798        let auth_db_config = settings.auth_database.as_ref().ok_or_else(|| {
799            anyhow!("Auth database configuration is required when using PostgreSQL with authentication. Set [auth_database]")
800        })?;
801        let auth_pg_config = auth_db_config.postgres.as_ref().ok_or_else(|| {
802            anyhow!("PostgreSQL auth database configuration is required when using PostgreSQL with authentication. Set [auth_database.postgres]")
803        })?;
804        if auth_pg_config.url.is_empty() {
805            bail!("Auth database PostgreSQL URL is required. Set [auth_database.postgres].url to an env: or file: secret reference");
806        }
807    }
808
809    Ok(())
810}
811
812fn validate_management_rpc_config(settings: &config::Settings) -> Result<()> {
813    #[cfg(not(feature = "management-rpc"))]
814    let _ = settings;
815
816    #[cfg(feature = "management-rpc")]
817    if let Some(rpc_settings) = settings.mint_management_rpc.as_ref() {
818        if rpc_settings.enabled {
819            let address = rpc_settings.address.as_deref().unwrap_or("127.0.0.1");
820            let port = rpc_settings.port.unwrap_or(8086);
821            format!("{address}:{port}")
822                .parse::<SocketAddr>()
823                .map_err(|err| {
824                    anyhow!(
825                        "Invalid mint management RPC address [mint_management_rpc].address/[mint_management_rpc].port ({address}:{port}): {err}"
826                    )
827                })?;
828        }
829    }
830
831    Ok(())
832}
833
834fn validate_prometheus_config(settings: &config::Settings) -> Result<()> {
835    #[cfg(not(feature = "prometheus"))]
836    let _ = settings;
837
838    #[cfg(feature = "prometheus")]
839    if let Some(prometheus_settings) = settings.prometheus.as_ref() {
840        if prometheus_settings.enabled {
841            let address = prometheus_settings
842                .address
843                .as_deref()
844                .unwrap_or("127.0.0.1");
845            let port = prometheus_settings.port.unwrap_or(9000);
846            format!("{address}:{port}")
847                .parse::<SocketAddr>()
848                .map_err(|err| {
849                    anyhow!(
850                        "Invalid Prometheus address [prometheus].address/[prometheus].port ({address}:{port}): {err}"
851                    )
852                })?;
853        }
854    }
855
856    Ok(())
857}
858
859/// Loads settings from command line arguments, environment variables, and optional seed file.
860pub fn load_settings_from_args(work_dir: &Path, args: &CLIArgs) -> Result<config::Settings> {
861    let mut settings = load_settings_from_sources(work_dir, args.config.clone())?;
862
863    if let Some(seed_file) = args.seed_file.as_deref() {
864        apply_seed_file(&mut settings, seed_file)?;
865    }
866
867    validate_settings(&settings)?;
868
869    Ok(settings)
870}
871
872/// Overrides the configured mint and active payment backend mnemonic with a seed file.
873pub fn apply_seed_file(settings: &mut config::Settings, seed_file: &Path) -> Result<()> {
874    let mnemonic = std::fs::read_to_string(seed_file)
875        .with_context(|| format!("Failed to read seed file {}", seed_file.display()))?;
876    let mnemonic = mnemonic.trim();
877
878    if mnemonic.is_empty() {
879        bail!("Seed file {} is empty", seed_file.display());
880    }
881
882    Mnemonic::parse(mnemonic)
883        .with_context(|| format!("Invalid seed phrase in seed file {}", seed_file.display()))?;
884
885    settings.info.seed = None;
886    settings.info.mnemonic = Some(mnemonic.to_owned());
887
888    #[cfg(feature = "bdk")]
889    if settings
890        .onchain
891        .as_ref()
892        .is_some_and(|onchain| onchain.onchain_backend == config::OnchainBackend::Bdk)
893    {
894        let mut bdk = settings.bdk.clone().unwrap_or_default();
895        bdk.mnemonic = Some(mnemonic.to_owned());
896        settings.bdk = Some(bdk);
897    }
898
899    #[cfg(feature = "ldk-node")]
900    if settings
901        .payment_backend
902        .iter()
903        .any(|backend| backend.backend == PaymentBackendType::LdkNode)
904    {
905        let mut ldk_node = settings.ldk_node.clone().unwrap_or_default();
906        ldk_node.ldk_node_mnemonic = Some(mnemonic.to_owned());
907        settings.ldk_node = Some(ldk_node);
908    }
909
910    Ok(())
911}
912
913async fn setup_database(
914    settings: &config::Settings,
915    _work_dir: &Path,
916    _db_password: Option<String>,
917) -> Result<(
918    DynMintDatabase,
919    Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
920    Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync>,
921    Arc<dyn KVStoreCompareAndSwap<Err = cdk_database::Error> + Send + Sync>,
922)> {
923    tracing::info!("Using database engine: {:?}", settings.database.engine);
924    match settings.database.engine {
925        #[cfg(feature = "sqlite")]
926        DatabaseEngine::Sqlite => {
927            let db = setup_sqlite_database(_work_dir, _db_password).await?;
928            let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> = db.clone();
929            let kv: Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync> = db.clone();
930            let configuration_store: Arc<
931                dyn KVStoreCompareAndSwap<Err = cdk_database::Error> + Send + Sync,
932            > = db.clone();
933            let keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync> = db;
934            Ok((localstore, keystore, kv, configuration_store))
935        }
936        #[cfg(feature = "postgres")]
937        DatabaseEngine::Postgres => {
938            // Get the PostgreSQL configuration, ensuring it exists
939            let pg_config = settings.database.postgres.as_ref().ok_or_else(|| {
940                anyhow!("PostgreSQL configuration is required when using PostgreSQL engine")
941            })?;
942
943            if pg_config.url.is_empty() {
944                bail!("PostgreSQL URL is required. Set it in config file [database.postgres] section or via CDK_MINTD_POSTGRES_URL/CDK_MINTD_DATABASE_URL environment variable");
945            }
946
947            #[cfg(feature = "postgres")]
948            let db_config = PgConfig::new(
949                pg_config.url.as_str(),
950                pg_config.tls_mode.as_deref(),
951                pg_config.max_connections,
952                pg_config.connection_timeout_seconds,
953            );
954            #[cfg(feature = "postgres")]
955            let pg_db = Arc::new(MintPgDatabase::new(db_config).await?);
956            tracing::info!("PostgreSQL database connection established");
957            #[cfg(feature = "postgres")]
958            let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> =
959                pg_db.clone();
960            #[cfg(feature = "postgres")]
961            let kv: Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync> = pg_db.clone();
962            #[cfg(feature = "postgres")]
963            let configuration_store: Arc<
964                dyn KVStoreCompareAndSwap<Err = cdk_database::Error> + Send + Sync,
965            > = pg_db.clone();
966            #[cfg(feature = "postgres")]
967            let keystore: Arc<
968                dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync,
969            > = pg_db;
970            #[cfg(feature = "postgres")]
971            return Ok((localstore, keystore, kv, configuration_store));
972
973            #[cfg(not(feature = "postgres"))]
974            bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
975        }
976        #[cfg(not(feature = "sqlite"))]
977        DatabaseEngine::Sqlite => {
978            bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
979        }
980        #[cfg(not(feature = "postgres"))]
981        DatabaseEngine::Postgres => {
982            bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
983        }
984    }
985}
986
987#[cfg(feature = "sqlite")]
988async fn setup_sqlite_database(
989    work_dir: &Path,
990    _password: Option<String>,
991) -> Result<Arc<MintSqliteDatabase>> {
992    let sql_db_path = work_dir.join("cdk-mintd.sqlite");
993    tracing::info!("SQLite database path: {}", sql_db_path.display());
994
995    #[cfg(not(feature = "sqlcipher"))]
996    let db = MintSqliteDatabase::new(&sql_db_path).await?;
997    #[cfg(feature = "sqlcipher")]
998    let db = {
999        // Get password from command line arguments for sqlcipher
1000        let password = _password
1001            .ok_or_else(|| anyhow!("Password required when sqlcipher feature is enabled"))?;
1002        tracing::info!("Using SQLCipher encryption for SQLite database");
1003        MintSqliteDatabase::new((sql_db_path, password)).await?
1004    };
1005
1006    tracing::info!("SQLite database initialized successfully");
1007    Ok(Arc::new(db))
1008}
1009
1010/**
1011 * Configures a `MintBuilder` instance with provided settings and initializes
1012 * routers for the configured payment backends.
1013 */
1014async fn configure_mint_builder_with_wallet_info(
1015    settings: &config::Settings,
1016    mint_builder: MintBuilder,
1017    runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1018    work_dir: &Path,
1019    kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
1020) -> Result<(MintBuilder, ConfiguredWalletInfoProvider)> {
1021    settings
1022        .validate_backend_pairing()
1023        .map_err(anyhow::Error::msg)?;
1024
1025    // Configure basic mint information
1026    let mint_builder = configure_basic_info(settings, mint_builder);
1027
1028    // Check that fake wallet is not used on mainnet
1029    #[cfg(feature = "fakewallet")]
1030    if settings
1031        .payment_backend
1032        .iter()
1033        .any(|backend| backend.backend == PaymentBackendType::FakeWallet)
1034    {
1035        if let Some(_onchain) = &settings.onchain {
1036            #[cfg(feature = "bdk")]
1037            if _onchain.onchain_backend == config::OnchainBackend::Bdk {
1038                if let Some(bdk) = &settings.bdk {
1039                    if let Some(network) = &bdk.network {
1040                        let network = network.to_lowercase();
1041                        if network == "mainnet" || network == "bitcoin" {
1042                            bail!("Fake wallet cannot be used as a payment backend when On-chain is configured for Mainnet");
1043                        }
1044                    }
1045                }
1046            }
1047        }
1048    }
1049
1050    // Configure payment backends
1051    let mint_builder = configure_payment_backends(
1052        settings,
1053        mint_builder,
1054        runtime.clone(),
1055        work_dir,
1056        kv_store.clone(),
1057    )
1058    .await?;
1059
1060    // Configure onchain backend
1061    let (mint_builder, wallet_info_provider) = configure_onchain_backend_with_wallet_info(
1062        settings,
1063        mint_builder,
1064        runtime,
1065        work_dir,
1066        kv_store,
1067    )
1068    .await?;
1069
1070    // Extract configured payment methods from mint_builder
1071    let mint_info = mint_builder.current_mint_info();
1072    let payment_methods = extract_supported_payment_methods(&mint_info);
1073
1074    // Enable batch minting by default for all supported methods
1075    let mint_builder = mint_builder
1076        .with_batch_minting(Some(DEFAULT_BATCH_MINT_SIZE), Some(payment_methods.clone()));
1077
1078    // Configure caching with payment methods
1079    let mint_builder = configure_cache(settings, mint_builder, &payment_methods).await?;
1080
1081    // Configure transaction limits
1082    let mint_builder =
1083        mint_builder.with_limits(settings.limits.max_inputs, settings.limits.max_outputs);
1084
1085    // Verify at least one payment processor is configured
1086    if mint_builder
1087        .current_mint_info()
1088        .nuts
1089        .nut04
1090        .methods
1091        .is_empty()
1092    {
1093        bail!("At least one payment backend must be configured");
1094    }
1095
1096    Ok((mint_builder, wallet_info_provider))
1097}
1098
1099#[cfg(test)]
1100async fn configure_mint_builder(
1101    settings: &config::Settings,
1102    mint_builder: MintBuilder,
1103    runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1104    work_dir: &Path,
1105    kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
1106) -> Result<MintBuilder> {
1107    Ok(
1108        configure_mint_builder_with_wallet_info(
1109            settings,
1110            mint_builder,
1111            runtime,
1112            work_dir,
1113            kv_store,
1114        )
1115        .await?
1116        .0,
1117    )
1118}
1119
1120/// Configures basic mint information (name, contact info, descriptions, etc.)
1121fn configure_basic_info(settings: &config::Settings, mint_builder: MintBuilder) -> MintBuilder {
1122    // Add contact information
1123    let mut contacts = Vec::new();
1124    if let Some(nostr_key) = &settings.mint_info.contact_nostr_public_key {
1125        if !nostr_key.is_empty() {
1126            contacts.push(ContactInfo::new("nostr".to_string(), nostr_key.to_string()));
1127        }
1128    }
1129    if let Some(email) = &settings.mint_info.contact_email {
1130        if !email.is_empty() {
1131            contacts.push(ContactInfo::new("email".to_string(), email.to_string()));
1132        }
1133    }
1134
1135    // Add version information
1136    let mint_version = MintVersion::new(
1137        "cdk-mintd".to_string(),
1138        CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
1139    );
1140
1141    // Configure mint builder with basic info
1142    let mut builder = mint_builder.with_version(mint_version);
1143
1144    // Only set name if it's not empty
1145    if !settings.mint_info.name.is_empty() {
1146        builder = builder.with_name(settings.mint_info.name.clone());
1147    }
1148
1149    // Only set description if it's not empty
1150    if !settings.mint_info.description.is_empty() {
1151        builder = builder.with_description(settings.mint_info.description.clone());
1152    }
1153
1154    // Add optional information
1155    if let Some(long_description) = &settings.mint_info.description_long {
1156        if !long_description.is_empty() {
1157            builder = builder.with_long_description(long_description.to_string());
1158        }
1159    }
1160
1161    for contact in contacts {
1162        builder = builder.with_contact_info(contact);
1163    }
1164
1165    if let Some(pubkey) = settings.mint_info.pubkey {
1166        builder = builder.with_pubkey(pubkey);
1167    }
1168
1169    if let Some(icon_url) = &settings.mint_info.icon_url {
1170        if !icon_url.is_empty() {
1171            builder = builder.with_icon_url(icon_url.to_string());
1172        }
1173    }
1174
1175    if let Some(motd) = &settings.mint_info.motd {
1176        if !motd.is_empty() {
1177            builder = builder.with_motd(motd.to_string());
1178        }
1179    }
1180
1181    if let Some(tos_url) = &settings.mint_info.tos_url {
1182        if !tos_url.is_empty() {
1183            builder = builder.with_tos_url(tos_url.to_string());
1184        }
1185    }
1186
1187    builder = builder.with_keyset_v2(settings.info.use_keyset_v2);
1188
1189    builder
1190}
1191/// Configures payment backends based on the specified backend types
1192async fn configure_payment_backends(
1193    settings: &config::Settings,
1194    mut mint_builder: MintBuilder,
1195    _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1196    work_dir: &Path,
1197    _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
1198) -> Result<MintBuilder> {
1199    if settings.payment_backend.is_empty() {
1200        tracing::info!("No payment backend configured");
1201        return Ok(mint_builder);
1202    }
1203
1204    #[cfg(feature = "fakewallet")]
1205    let mut configure_fake_wallet_keyset_rotations = false;
1206
1207    for backend_entry in &settings.payment_backend {
1208        let mint_melt_limits = MintMeltLimits {
1209            mint_min: backend_entry.min_mint,
1210            mint_max: backend_entry.max_mint,
1211            melt_min: backend_entry.min_melt,
1212            melt_max: backend_entry.max_melt,
1213        };
1214
1215        tracing::debug!(
1216            "Payment backend: {:?} (unit: {:?})",
1217            backend_entry.backend,
1218            backend_entry.unit
1219        );
1220
1221        match backend_entry.backend {
1222            #[cfg(feature = "cln")]
1223            PaymentBackendType::Cln => {
1224                let cln_settings = settings.cln.clone().ok_or_else(|| {
1225                    anyhow!("CLN backend selected but [cln] config section is missing")
1226                })?;
1227                let cln = cln_settings
1228                    .setup(
1229                        settings,
1230                        cdk::nuts::CurrencyUnit::Msat,
1231                        None,
1232                        work_dir,
1233                        _kv_store.clone(),
1234                    )
1235                    .await?;
1236                #[cfg(feature = "prometheus")]
1237                let cln = MetricsMintPayment::new(cln);
1238
1239                mint_builder = configure_backend_for_unit(
1240                    settings,
1241                    mint_builder,
1242                    backend_entry.unit.clone(),
1243                    mint_melt_limits,
1244                    Arc::new(cln),
1245                )
1246                .await?;
1247            }
1248            #[cfg(feature = "lnd")]
1249            PaymentBackendType::Lnd => {
1250                let lnd_settings = settings.lnd.clone().ok_or_else(|| {
1251                    anyhow!("LND backend selected but [lnd] config section is missing")
1252                })?;
1253                let lnd = lnd_settings
1254                    .setup(
1255                        settings,
1256                        cdk::nuts::CurrencyUnit::Msat,
1257                        None,
1258                        work_dir,
1259                        _kv_store.clone(),
1260                    )
1261                    .await?;
1262                #[cfg(feature = "prometheus")]
1263                let lnd = MetricsMintPayment::new(lnd);
1264
1265                mint_builder = configure_backend_for_unit(
1266                    settings,
1267                    mint_builder,
1268                    backend_entry.unit.clone(),
1269                    mint_melt_limits,
1270                    Arc::new(lnd),
1271                )
1272                .await?;
1273            }
1274            #[cfg(feature = "fakewallet")]
1275            PaymentBackendType::FakeWallet => {
1276                let fake_wallet = settings.fake_wallet.clone().ok_or_else(|| {
1277                    anyhow!(
1278                        "Fake wallet backend selected but [fake_wallet] config section is missing"
1279                    )
1280                })?;
1281                tracing::info!("Using fake wallet: {:?}", fake_wallet);
1282
1283                let fake = fake_wallet
1284                    .setup(
1285                        settings,
1286                        backend_entry.unit.clone(),
1287                        None,
1288                        work_dir,
1289                        _kv_store.clone(),
1290                    )
1291                    .await?;
1292                #[cfg(feature = "prometheus")]
1293                let fake = MetricsMintPayment::new(fake);
1294
1295                mint_builder = configure_backend_for_unit(
1296                    settings,
1297                    mint_builder,
1298                    backend_entry.unit.clone(),
1299                    mint_melt_limits,
1300                    Arc::new(fake),
1301                )
1302                .await?;
1303
1304                configure_fake_wallet_keyset_rotations = true;
1305            }
1306            #[cfg(feature = "grpc-processor")]
1307            PaymentBackendType::GrpcProcessor => {
1308                let grpc_processor = settings.grpc_processor.clone().ok_or_else(|| {
1309                    anyhow!(
1310                        "gRPC payment processor backend selected but [grpc_processor] config section is missing"
1311                    )
1312                })?;
1313
1314                tracing::info!(
1315                    "Attempting to start with gRPC payment processor at {}:{}.",
1316                    grpc_processor.address,
1317                    grpc_processor.port
1318                );
1319
1320                let processor = grpc_processor
1321                    .setup(settings, backend_entry.unit.clone(), None, work_dir, None)
1322                    .await?;
1323                #[cfg(feature = "prometheus")]
1324                let processor = MetricsMintPayment::new(processor);
1325
1326                mint_builder = configure_backend_for_unit(
1327                    settings,
1328                    mint_builder,
1329                    backend_entry.unit.clone(),
1330                    mint_melt_limits,
1331                    Arc::new(processor),
1332                )
1333                .await?;
1334            }
1335            #[cfg(feature = "ldk-node")]
1336            PaymentBackendType::LdkNode => {
1337                let ldk_node_settings = settings.ldk_node.clone().ok_or_else(|| {
1338                    anyhow!("LDK Node backend selected but [ldk_node] config section is missing")
1339                })?;
1340                tracing::info!("Using LDK Node backend: {:?}", ldk_node_settings);
1341
1342                let ldk_node = ldk_node_settings
1343                    .setup(
1344                        settings,
1345                        backend_entry.unit.clone(),
1346                        _runtime.clone(),
1347                        work_dir,
1348                        _kv_store.clone(),
1349                    )
1350                    .await?;
1351
1352                mint_builder = configure_backend_for_unit(
1353                    settings,
1354                    mint_builder,
1355                    backend_entry.unit.clone(),
1356                    mint_melt_limits,
1357                    Arc::new(ldk_node),
1358                )
1359                .await?;
1360            }
1361            PaymentBackendType::None => {
1362                tracing::info!(
1363                    "No payment backend configured for unit {:?}",
1364                    backend_entry.unit
1365                );
1366            }
1367        };
1368    }
1369
1370    #[cfg(feature = "fakewallet")]
1371    if configure_fake_wallet_keyset_rotations {
1372        let fake_wallet = settings.fake_wallet.as_ref().ok_or_else(|| {
1373            anyhow!("Fake wallet backend selected but [fake_wallet] config section is missing")
1374        })?;
1375        mint_builder = configure_fake_wallet_keyset_rotations_once(mint_builder, fake_wallet);
1376    }
1377
1378    Ok(mint_builder)
1379}
1380
1381#[cfg(feature = "fakewallet")]
1382fn configure_fake_wallet_keyset_rotations_once(
1383    mut mint_builder: MintBuilder,
1384    fake_wallet: &config::FakeWallet,
1385) -> MintBuilder {
1386    for rotation_cfg in &fake_wallet.keyset_rotations {
1387        use cdk::mint::KeysetRotation;
1388
1389        let amounts = cdk::mint::UnitConfig::default().amounts;
1390        let final_expiry = if rotation_cfg.expired {
1391            Some(cdk::util::unix_time().saturating_sub(3600))
1392        } else {
1393            None
1394        };
1395
1396        mint_builder = mint_builder.with_keyset_rotation(KeysetRotation {
1397            unit: rotation_cfg.unit.clone(),
1398            amounts,
1399            input_fee_ppk: rotation_cfg.input_fee_ppk,
1400            use_keyset_v2: rotation_cfg.version == "v2",
1401            final_expiry,
1402        });
1403    }
1404
1405    mint_builder
1406}
1407
1408/// Configures Onchain backend based on the specified backend type
1409async fn configure_onchain_backend_with_wallet_info(
1410    settings: &config::Settings,
1411    #[cfg_attr(not(feature = "bdk"), allow(unused_mut))] mut mint_builder: MintBuilder,
1412    _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1413    _work_dir: &Path,
1414    _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
1415) -> Result<(MintBuilder, ConfiguredWalletInfoProvider)> {
1416    use config::OnchainBackend;
1417    #[cfg(feature = "bdk")]
1418    use setup::OnchainBackendSetup;
1419
1420    #[cfg(all(feature = "management-rpc", feature = "bdk"))]
1421    let mut wallet_info_provider = no_wallet_info_provider();
1422
1423    if let Some(onchain_settings) = &settings.onchain {
1424        match onchain_settings.onchain_backend {
1425            #[cfg(feature = "bdk")]
1426            OnchainBackend::Bdk => {
1427                let mint_melt_limits = MintMeltLimits {
1428                    mint_min: onchain_settings.min_mint,
1429                    mint_max: onchain_settings.max_mint,
1430                    melt_min: onchain_settings.min_melt,
1431                    melt_max: onchain_settings.max_melt,
1432                };
1433
1434                let bdk_settings = settings.bdk.clone().ok_or_else(|| {
1435                    anyhow!("BDK onchain backend selected but [bdk] config section is missing")
1436                })?;
1437                let bdk = bdk_settings
1438                    .setup(
1439                        settings,
1440                        cdk::nuts::CurrencyUnit::Sat,
1441                        None,
1442                        _work_dir,
1443                        _kv_store,
1444                    )
1445                    .await?;
1446                let bdk = Arc::new(bdk);
1447
1448                #[cfg(feature = "management-rpc")]
1449                {
1450                    wallet_info_provider = Some(Arc::new(BdkWalletInfoProvider {
1451                        bdk: Arc::clone(&bdk),
1452                    }));
1453                }
1454
1455                mint_builder = configure_backend_for_unit(
1456                    settings,
1457                    mint_builder,
1458                    cdk::nuts::CurrencyUnit::Sat,
1459                    mint_melt_limits,
1460                    bdk,
1461                )
1462                .await?;
1463            }
1464            OnchainBackend::None => {}
1465            #[cfg(feature = "fakewallet")]
1466            OnchainBackend::FakeWallet => {
1467                let has_payment_backend = settings
1468                    .payment_backend
1469                    .iter()
1470                    .any(|backend| backend.backend != PaymentBackendType::None);
1471                let has_real_payment_backend = settings.payment_backend.iter().any(|backend| {
1472                    !matches!(
1473                        backend.backend,
1474                        PaymentBackendType::None | PaymentBackendType::FakeWallet
1475                    )
1476                });
1477
1478                if !has_payment_backend {
1479                    let mint_melt_limits = MintMeltLimits {
1480                        mint_min: onchain_settings.min_mint,
1481                        mint_max: onchain_settings.max_mint,
1482                        melt_min: onchain_settings.min_melt,
1483                        melt_max: onchain_settings.max_melt,
1484                    };
1485                    let fake_wallet = settings
1486                        .fake_wallet
1487                        .clone()
1488                        .ok_or_else(|| anyhow!("Fake wallet config section is missing"))?;
1489
1490                    for unit in fake_wallet.clone().supported_units {
1491                        let fake = fake_wallet
1492                            .setup(settings, unit.clone(), None, _work_dir, _kv_store.clone())
1493                            .await?;
1494                        #[cfg(feature = "prometheus")]
1495                        let fake = MetricsMintPayment::new(fake);
1496
1497                        mint_builder = configure_backend_for_methods(
1498                            settings,
1499                            mint_builder,
1500                            unit,
1501                            mint_melt_limits,
1502                            Arc::new(fake),
1503                            vec![PaymentMethod::Known(KnownMethod::Onchain)],
1504                        )
1505                        .await?;
1506                    }
1507                } else if has_real_payment_backend {
1508                    bail!(
1509                        "onchain_backend = \"fakewallet\" cannot be combined with a real payment backend"
1510                    );
1511                }
1512            }
1513        }
1514    }
1515
1516    #[cfg(all(feature = "management-rpc", feature = "bdk"))]
1517    {
1518        Ok((mint_builder, wallet_info_provider))
1519    }
1520    #[cfg(not(all(feature = "management-rpc", feature = "bdk")))]
1521    {
1522        Ok((mint_builder, no_wallet_info_provider()))
1523    }
1524}
1525
1526#[cfg(test)]
1527async fn configure_onchain_backend(
1528    settings: &config::Settings,
1529    mint_builder: MintBuilder,
1530    runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1531    work_dir: &Path,
1532    kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
1533) -> Result<MintBuilder> {
1534    Ok(configure_onchain_backend_with_wallet_info(
1535        settings,
1536        mint_builder,
1537        runtime,
1538        work_dir,
1539        kv_store,
1540    )
1541    .await?
1542    .0)
1543}
1544
1545/// Helper function to configure a mint builder with a payment backend for a specific currency unit
1546async fn configure_backend_for_unit(
1547    settings: &config::Settings,
1548    mint_builder: MintBuilder,
1549    unit: cdk::nuts::CurrencyUnit,
1550    mint_melt_limits: MintMeltLimits,
1551    backend: Arc<dyn MintPayment<Err = cdk_common::payment::Error> + Send + Sync>,
1552) -> Result<MintBuilder> {
1553    let payment_settings = backend.get_settings().await?;
1554    validate_backend_unit(&unit, &payment_settings.unit)?;
1555
1556    let mut methods = Vec::new();
1557
1558    // Add bolt11 if supported by payment processor
1559    if payment_settings.bolt11.is_some() {
1560        methods.push(PaymentMethod::Known(KnownMethod::Bolt11));
1561    }
1562
1563    // Add bolt12 if supported by payment processor
1564    if payment_settings.bolt12.is_some() {
1565        methods.push(PaymentMethod::Known(KnownMethod::Bolt12));
1566    }
1567
1568    // Add onchain if supported by payment processor
1569    if payment_settings.onchain.is_some() {
1570        methods.push(PaymentMethod::Known(KnownMethod::Onchain));
1571    }
1572
1573    // Add custom methods from payment settings
1574    for method_name in payment_settings.custom.keys() {
1575        methods.push(PaymentMethod::from(method_name.as_str()));
1576    }
1577
1578    configure_backend_for_methods(
1579        settings,
1580        mint_builder,
1581        unit,
1582        mint_melt_limits,
1583        backend,
1584        methods,
1585    )
1586    .await
1587}
1588
1589async fn configure_backend_for_methods(
1590    settings: &config::Settings,
1591    mut mint_builder: MintBuilder,
1592    unit: cdk::nuts::CurrencyUnit,
1593    mint_melt_limits: MintMeltLimits,
1594    backend: Arc<dyn MintPayment<Err = cdk_common::payment::Error> + Send + Sync>,
1595    methods: Vec<PaymentMethod>,
1596) -> Result<MintBuilder> {
1597    // Add all supported payment methods to the mint builder
1598    for method in &methods {
1599        mint_builder
1600            .add_payment_processor(
1601                unit.clone(),
1602                method.clone(),
1603                mint_melt_limits,
1604                backend.clone(),
1605            )
1606            .await?;
1607    }
1608
1609    // Configure NUT17 (WebSocket support) for all payment methods
1610    for method in &methods {
1611        let method_str = method.to_string();
1612        let nut17_supported = match method_str.as_str() {
1613            "bolt11" => SupportedMethods::default_bolt11(unit.clone()),
1614            "bolt12" => SupportedMethods::default_bolt12(unit.clone()),
1615            _ => SupportedMethods::default_custom(method.clone(), unit.clone()),
1616        };
1617        mint_builder = mint_builder.with_supported_websockets(nut17_supported);
1618    }
1619
1620    if let Some(input_fee) = settings.info.input_fee_ppk {
1621        mint_builder.set_unit_fee(&unit, input_fee)?;
1622    }
1623
1624    Ok(mint_builder)
1625}
1626
1627fn validate_backend_unit(
1628    configured_unit: &cdk::nuts::CurrencyUnit,
1629    backend_unit: &str,
1630) -> Result<()> {
1631    let backend_unit = cdk::nuts::CurrencyUnit::from_str(backend_unit)
1632        .with_context(|| format!("Payment backend returned invalid unit `{backend_unit}`"))?;
1633
1634    if units_are_compatible(&backend_unit, configured_unit) {
1635        return Ok(());
1636    }
1637
1638    bail!(
1639        "Payment backend reports unit {} but config registers unit {}; only matching units or sat/msat conversions are supported",
1640        backend_unit,
1641        configured_unit
1642    )
1643}
1644
1645fn units_are_compatible(
1646    backend_unit: &cdk::nuts::CurrencyUnit,
1647    configured_unit: &cdk::nuts::CurrencyUnit,
1648) -> bool {
1649    backend_unit == configured_unit
1650        || matches!(
1651            (backend_unit, configured_unit),
1652            (cdk::nuts::CurrencyUnit::Sat, cdk::nuts::CurrencyUnit::Msat)
1653                | (cdk::nuts::CurrencyUnit::Msat, cdk::nuts::CurrencyUnit::Sat)
1654        )
1655}
1656
1657/// Configures cache settings with support for custom payment methods
1658async fn configure_cache(
1659    settings: &config::Settings,
1660    mint_builder: MintBuilder,
1661    payment_methods: &[String],
1662) -> Result<MintBuilder> {
1663    let mut cached_endpoints = vec![
1664        // Always include swap endpoint
1665        CachedEndpoint::new(NUT19Method::Post, NUT19Path::Swap),
1666    ];
1667
1668    // Add cache endpoints for each configured payment method
1669    for method in payment_methods {
1670        // All payment methods (including bolt11, bolt12) use custom paths now
1671        cached_endpoints.push(CachedEndpoint::new(
1672            NUT19Method::Post,
1673            NUT19Path::custom_mint(method),
1674        ));
1675        cached_endpoints.push(CachedEndpoint::new(
1676            NUT19Method::Post,
1677            NUT19Path::custom_melt(method),
1678        ));
1679    }
1680
1681    let cache: HttpCache = HttpCache::from_config(settings.info.http_cache.clone()).await?;
1682    Ok(mint_builder.with_cache(Some(cache.ttl.as_secs()), cached_endpoints))
1683}
1684
1685async fn setup_authentication(
1686    settings: &config::Settings,
1687    _work_dir: &Path,
1688    mut mint_builder: MintBuilder,
1689    _password: Option<String>,
1690) -> Result<(
1691    MintBuilder,
1692    Option<cdk_common::database::DynMintAuthDatabase>,
1693)> {
1694    if let Some(auth_settings) = settings.auth.clone() {
1695        use cdk_common::database::DynMintAuthDatabase;
1696
1697        tracing::info!("Auth settings are defined. {:?}", auth_settings);
1698        let auth_localstore: DynMintAuthDatabase = match settings.database.engine {
1699            #[cfg(feature = "sqlite")]
1700            DatabaseEngine::Sqlite => {
1701                #[cfg(feature = "sqlite")]
1702                {
1703                    let sql_db_path = _work_dir.join("cdk-mintd-auth.sqlite");
1704                    #[cfg(not(feature = "sqlcipher"))]
1705                    let sqlite_db = MintSqliteAuthDatabase::new(&sql_db_path).await?;
1706                    #[cfg(feature = "sqlcipher")]
1707                    let sqlite_db = {
1708                        // Get password from command line arguments for sqlcipher
1709                        let password = _password.clone().ok_or_else(|| {
1710                            anyhow!("Password required when sqlcipher feature is enabled")
1711                        })?;
1712                        MintSqliteAuthDatabase::new((sql_db_path, password)).await?
1713                    };
1714
1715                    Arc::new(sqlite_db)
1716                }
1717                #[cfg(not(feature = "sqlite"))]
1718                {
1719                    bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
1720                }
1721            }
1722            #[cfg(feature = "postgres")]
1723            DatabaseEngine::Postgres => {
1724                #[cfg(feature = "postgres")]
1725                {
1726                    // Require dedicated auth database configuration - no fallback to main database
1727                    let auth_db_config = settings.auth_database.as_ref().ok_or_else(|| {
1728                        anyhow!("Auth database configuration is required when using PostgreSQL with authentication. Set [auth_database]")
1729                    })?;
1730
1731                    let auth_pg_config = auth_db_config.postgres.as_ref().ok_or_else(|| {
1732                        anyhow!("PostgreSQL auth database configuration is required when using PostgreSQL with authentication. Set [auth_database.postgres]")
1733                    })?;
1734
1735                    if auth_pg_config.url.is_empty() {
1736                        bail!("Auth database PostgreSQL URL is required and cannot be empty. Set [auth_database.postgres].url to an env: or file: secret reference");
1737                    }
1738
1739                    let auth_db_config = PgConfig::new(
1740                        auth_pg_config.url.as_str(),
1741                        auth_pg_config.tls_mode.as_deref(),
1742                        auth_pg_config.max_connections,
1743                        auth_pg_config.connection_timeout_seconds,
1744                    );
1745                    Arc::new(MintPgAuthDatabase::new(auth_db_config).await?)
1746                }
1747                #[cfg(not(feature = "postgres"))]
1748                {
1749                    bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
1750                }
1751            }
1752            #[cfg(not(feature = "sqlite"))]
1753            DatabaseEngine::Sqlite => {
1754                bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
1755            }
1756            #[cfg(not(feature = "postgres"))]
1757            DatabaseEngine::Postgres => {
1758                bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
1759            }
1760        };
1761
1762        let mut protected_endpoints = HashMap::new();
1763        let mut blind_auth_endpoints = vec![];
1764        let mut clear_auth_endpoints = vec![];
1765        let mut unprotected_endpoints = vec![];
1766
1767        let mint_blind_auth_endpoint =
1768            ProtectedEndpoint::new(Method::Post, RoutePath::MintBlindAuth);
1769
1770        protected_endpoints.insert(mint_blind_auth_endpoint.clone(), AuthRequired::Clear);
1771
1772        clear_auth_endpoints.push(mint_blind_auth_endpoint);
1773
1774        // Helper function to add endpoint based on auth type
1775        let mut add_endpoint = |endpoint: ProtectedEndpoint, auth_type: &AuthType| {
1776            match auth_type {
1777                AuthType::Blind => {
1778                    protected_endpoints.insert(endpoint.clone(), AuthRequired::Blind);
1779                    blind_auth_endpoints.push(endpoint);
1780                }
1781                AuthType::Clear => {
1782                    protected_endpoints.insert(endpoint.clone(), AuthRequired::Clear);
1783                    clear_auth_endpoints.push(endpoint);
1784                }
1785                AuthType::None => {
1786                    unprotected_endpoints.push(endpoint);
1787                }
1788            };
1789        };
1790
1791        // Payment method endpoints (bolt11, bolt12, custom) will be added dynamically
1792        // after the mint is built and we can query the payment processors for their
1793        // supported methods. See the start_services_with_shutdown function where we
1794        // add auth endpoints for all configured payment methods.
1795
1796        // Swap endpoint
1797        {
1798            let swap_protected_endpoint = ProtectedEndpoint::new(Method::Post, RoutePath::Swap);
1799            add_endpoint(swap_protected_endpoint, &auth_settings.swap);
1800        }
1801
1802        // Restore endpoint
1803        {
1804            let restore_protected_endpoint =
1805                ProtectedEndpoint::new(Method::Post, RoutePath::Restore);
1806            add_endpoint(restore_protected_endpoint, &auth_settings.restore);
1807        }
1808
1809        // Check proof state endpoint
1810        {
1811            let state_protected_endpoint =
1812                ProtectedEndpoint::new(Method::Post, RoutePath::Checkstate);
1813            add_endpoint(state_protected_endpoint, &auth_settings.check_proof_state);
1814        }
1815
1816        // Ws endpoint
1817        {
1818            let ws_protected_endpoint = ProtectedEndpoint::new(Method::Get, RoutePath::Ws);
1819            add_endpoint(ws_protected_endpoint, &auth_settings.websocket_auth);
1820        }
1821
1822        // Custom protected_endpoints will be added dynamically after the mint is built
1823        // and we can query the payment processors for their supported methods.
1824        // For now, we don't add any custom endpoints here - they'll be added in the
1825        // start_services_with_shutdown function after we have access to the mint instance.
1826
1827        mint_builder = mint_builder.with_auth(
1828            auth_localstore.clone(),
1829            auth_settings.openid_discovery,
1830            auth_settings.openid_client_id,
1831            clear_auth_endpoints,
1832        );
1833        mint_builder =
1834            mint_builder.with_blind_auth(auth_settings.mint_max_bat, blind_auth_endpoints);
1835
1836        let mut tx = auth_localstore.begin_transaction().await?;
1837
1838        if !unprotected_endpoints.is_empty() {
1839            tx.remove_protected_endpoints(unprotected_endpoints).await?;
1840        }
1841        if !protected_endpoints.is_empty() {
1842            tx.add_protected_endpoints(protected_endpoints).await?;
1843        }
1844        tx.commit().await?;
1845
1846        Ok((mint_builder, Some(auth_localstore)))
1847    } else {
1848        Ok((mint_builder, None))
1849    }
1850}
1851
1852/// Build mints with the configured the signing method (remote signatory or local seed)
1853async fn build_mint(
1854    settings: &config::Settings,
1855    keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
1856    mint_builder: MintBuilder,
1857    validated_signing_source: Option<&ValidatedSigningSource>,
1858) -> Result<Mint> {
1859    if let Some(signatory) = settings.enabled_signatory() {
1860        let tls_dir = signatory.tls_dir.clone();
1861
1862        if tls_dir.is_none() {
1863            if !signatory.allow_insecure {
1864                bail!(
1865                    "gRPC signatory TLS is not configured. Set [signatory].tls_dir or \
1866                     [signatory].allow_insecure = true to connect without TLS"
1867                );
1868            }
1869
1870            tracing::warn!(
1871                "No gRPC signatory TLS directory configured; connecting without TLS because \
1872                 allow_insecure is true"
1873            );
1874        }
1875
1876        let remote_signatory = match validated_signing_source
1877            .and_then(|validated| validated.remote_signatory.clone())
1878        {
1879            Some(remote_signatory) => {
1880                tracing::info!(
1881                    "Using the remote signatory connection validated during configuration startup"
1882                );
1883                remote_signatory
1884            }
1885            None => {
1886                tracing::info!(
1887                    "Connecting to remote signatory at {}:{} with TLS directory {:?}",
1888                    signatory.address,
1889                    signatory.port,
1890                    tls_dir
1891                );
1892                Arc::new(
1893                    cdk_signatory::SignatoryRpcClient::new(
1894                        &signatory.address,
1895                        signatory.port,
1896                        tls_dir,
1897                    )
1898                    .await?,
1899                )
1900            }
1901        };
1902        if let Some(validated) = validated_signing_source {
1903            ensure_signatory_identity(&remote_signatory, validated.expected_pubkey).await?;
1904        }
1905
1906        Ok(mint_builder.build_with_signatory(remote_signatory).await?)
1907    } else if let Some(seed) = settings.info.seed.clone().filter(|seed| !seed.is_empty()) {
1908        if validated_signing_source.is_some_and(|validated| validated.remote_signatory.is_some()) {
1909            bail!("Validated remote signatory provided for local signing configuration");
1910        }
1911        let seed_bytes: Vec<u8> = seed.into();
1912        Ok(mint_builder.build_with_seed(keystore, &seed_bytes).await?)
1913    } else if let Some(mnemonic) = settings
1914        .info
1915        .mnemonic
1916        .clone()
1917        .map(|s| Mnemonic::from_str(&s))
1918        .transpose()?
1919    {
1920        Ok(mint_builder
1921            .build_with_seed(keystore, &mnemonic.to_seed_normalized(""))
1922            .await?)
1923    } else {
1924        bail!("No seed nor remote signatory set");
1925    }
1926}
1927
1928async fn ensure_signatory_identity(
1929    signatory: &DynSignatory,
1930    expected_pubkey: cdk::nuts::PublicKey,
1931) -> Result<()> {
1932    let actual_pubkey = signatory.keysets().await?.pubkey;
1933    if actual_pubkey != expected_pubkey {
1934        return Err(config_service::ConfigurationServiceError::SigningIdentityChange.into());
1935    }
1936    Ok(())
1937}
1938
1939async fn reconcile_canonical_configuration(
1940    mint: &Mint,
1941    mut configured_mint_info: cdk::nuts::MintInfo,
1942    configured_quote_ttl: QuoteTTL,
1943    preserve_database_values: bool,
1944) -> Result<()> {
1945    if !preserve_database_values {
1946        tracing::info!(
1947            "Applying mint info and quote TTL from the database-backed configuration document."
1948        );
1949        if let Ok(stored_mint_info) = mint.mint_info().await {
1950            if configured_mint_info.pubkey.is_none() {
1951                configured_mint_info.pubkey = stored_mint_info.pubkey;
1952            }
1953        }
1954        mint.set_mint_info_and_quote_ttl(configured_mint_info, configured_quote_ttl)
1955            .await?;
1956        return Ok(());
1957    }
1958
1959    if mint.mint_info().await.is_err() {
1960        tracing::info!("Mint info not set on mint, setting.");
1961        mint.set_mint_info_and_quote_ttl(configured_mint_info, configured_quote_ttl)
1962            .await?;
1963        return Ok(());
1964    }
1965
1966    if !mint.quote_ttl_is_persisted().await? {
1967        mint.set_quote_ttl(configured_quote_ttl).await?;
1968    }
1969    let mint_version = MintVersion::new(
1970        "cdk-mintd".to_string(),
1971        CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
1972    );
1973    let mut stored_mint_info = mint.mint_info().await?;
1974    stored_mint_info.version = Some(mint_version);
1975    mint.set_mint_info(stored_mint_info).await?;
1976    tracing::info!("Preserving RPC-managed mint info from the database.");
1977    Ok(())
1978}
1979
1980/// A mint daemon with every resource built and all database reconciliation
1981/// completed, ready to start its services.
1982struct PreparedMintd {
1983    mint: Arc<cdk::mint::Mint>,
1984    #[cfg(feature = "prometheus")]
1985    prometheus: Option<config::Prometheus>,
1986    mint_service: Router,
1987    listen_addr: String,
1988    listen_port: u16,
1989    activation: Option<ConfigurationActivation>,
1990    shutdown_tx: tokio::sync::broadcast::Sender<()>,
1991    /// Management RPC server and its TLS directory, when enabled.
1992    #[cfg(feature = "management-rpc")]
1993    rpc_to_start: Option<(cdk_mint_rpc::MintRPCServer, Option<PathBuf>)>,
1994}
1995
1996/// A mint daemon with all services started and its configuration committed
1997/// as applied, serving requests until shutdown.
1998struct RunningMintd {
1999    mint: Arc<cdk::mint::Mint>,
2000    mint_service: Router,
2001    listener: tokio::net::TcpListener,
2002    shutdown_tx: tokio::sync::broadcast::Sender<()>,
2003    #[cfg(feature = "prometheus")]
2004    prometheus_handle: Option<tokio::task::JoinHandle<()>>,
2005    #[cfg(feature = "management-rpc")]
2006    rpc_server: Option<cdk_mint_rpc::MintRPCServer>,
2007}
2008
2009impl PreparedMintd {
2010    /// Builds every resource and performs all database reconciliation
2011    /// without starting tasks or binding listeners.
2012    #[allow(clippy::too_many_arguments)]
2013    async fn prepare(
2014        mint: Arc<cdk::mint::Mint>,
2015        settings: &config::Settings,
2016        _work_dir: &Path,
2017        _wallet_info_provider: ConfiguredWalletInfoProvider,
2018        mint_builder_info: cdk::nuts::MintInfo,
2019        routers: Vec<Router>,
2020        auth_localstore: Option<cdk_common::database::DynMintAuthDatabase>,
2021        activation: Option<ConfigurationActivation>,
2022    ) -> Result<Self> {
2023        let listen_addr = settings.info.listen_host.clone();
2024        let listen_port = settings.info.listen_port;
2025        let cache: HttpCache = HttpCache::from_config(settings.info.http_cache.clone()).await?;
2026
2027        #[cfg(feature = "management-rpc")]
2028        let mut rpc_enabled = false;
2029        #[cfg(not(feature = "management-rpc"))]
2030        let rpc_enabled = false;
2031
2032        #[cfg(feature = "management-rpc")]
2033        let mut rpc_to_start = None;
2034
2035        #[cfg(feature = "management-rpc")]
2036        {
2037            if let Some(rpc_settings) = settings.mint_management_rpc.clone() {
2038                if rpc_settings.enabled {
2039                    let addr = rpc_settings.address.unwrap_or("127.0.0.1".to_string());
2040                    let port = rpc_settings.port.unwrap_or(8086);
2041                    let mut mint_rpc = cdk_mint_rpc::MintRPCServer::new(&addr, port, mint.clone())?
2042                        .with_mint_quote_payment_override(
2043                            rpc_settings.allow_mint_quote_payment_override,
2044                        );
2045                    if let Some(activation) = activation.as_ref() {
2046                        mint_rpc =
2047                            mint_rpc.with_mutation_guard(Arc::new(ConfigurationMutationGuard {
2048                                service: activation.service.clone(),
2049                            }));
2050                    }
2051                    #[cfg(feature = "bdk")]
2052                    if let Some(provider) = _wallet_info_provider.clone() {
2053                        mint_rpc = mint_rpc.with_wallet_info_provider(provider);
2054                    }
2055
2056                    let tls_dir = rpc_settings.tls_dir.unwrap_or(_work_dir.join("tls"));
2057
2058                    let tls_dir = if tls_dir.exists() {
2059                        Some(tls_dir)
2060                    } else if rpc_settings.allow_insecure {
2061                        tracing::warn!(
2062                        "TLS directory does not exist: {}. Starting RPC server in INSECURE mode without TLS encryption because allow_insecure is true",
2063                        tls_dir.display()
2064                    );
2065                        None
2066                    } else {
2067                        bail!(
2068                            "Management RPC TLS directory does not exist: {}. Set \
2069                         [mint_management_rpc].tls_dir or \
2070                         [mint_management_rpc].allow_insecure = true to start without \
2071                         TLS",
2072                            tls_dir.display()
2073                        );
2074                    };
2075
2076                    rpc_to_start = Some((mint_rpc, tls_dir));
2077                    rpc_enabled = true;
2078                }
2079            }
2080        }
2081
2082        // Determine the desired QuoteTTL from config/env or fall back to defaults
2083        let desired_quote_ttl: QuoteTTL = settings.info.quote_ttl.unwrap_or_default();
2084
2085        let preserve_database_values = match activation.as_ref() {
2086            Some(activation) => activation.preserves_database_values(rpc_enabled),
2087            // A startup without a database-backed configuration record never
2088            // forces document values; preserve database values when the
2089            // management RPC could have authored them.
2090            None => rpc_enabled,
2091        };
2092
2093        reconcile_canonical_configuration(
2094            mint.as_ref(),
2095            mint_builder_info,
2096            desired_quote_ttl,
2097            preserve_database_values,
2098        )
2099        .await?;
2100
2101        let mint_info = mint.mint_info().await?;
2102        let nut04_methods = mint_info.nuts.nut04.supported_methods();
2103        let nut05_methods = mint_info.nuts.nut05.supported_methods();
2104
2105        // Get custom payment methods from payment processors
2106        let mut custom_methods = mint.get_custom_payment_methods().await?;
2107
2108        // Add bolt11 if it's supported by any payment processor
2109        let bolt11_method = PaymentMethod::Known(KnownMethod::Bolt11);
2110        let bolt11_supported =
2111            nut04_methods.contains(&&bolt11_method) || nut05_methods.contains(&&bolt11_method);
2112        // Add bolt12 if it's supported by any payment processor
2113        let bolt12_method = PaymentMethod::Known(KnownMethod::Bolt12);
2114        let bolt12_supported =
2115            nut04_methods.contains(&&bolt12_method) || nut05_methods.contains(&&bolt12_method);
2116
2117        // Add onchain if it's supported by any payment processor
2118        let onchain_method = PaymentMethod::Known(KnownMethod::Onchain);
2119        let onchain_supported =
2120            nut04_methods.contains(&&onchain_method) || nut05_methods.contains(&&onchain_method);
2121
2122        if bolt11_supported
2123            && !custom_methods.contains(&PaymentMethod::Known(KnownMethod::Bolt11).to_string())
2124        {
2125            custom_methods.push(PaymentMethod::Known(KnownMethod::Bolt11).to_string());
2126        }
2127        if bolt12_supported
2128            && !custom_methods.contains(&PaymentMethod::Known(KnownMethod::Bolt12).to_string())
2129        {
2130            custom_methods.push(PaymentMethod::Known(KnownMethod::Bolt12).to_string());
2131        }
2132        if onchain_supported
2133            && !custom_methods.contains(&PaymentMethod::Known(KnownMethod::Onchain).to_string())
2134        {
2135            custom_methods.push(PaymentMethod::Known(KnownMethod::Onchain).to_string());
2136        }
2137
2138        tracing::info!("Payment methods: {:?}", custom_methods);
2139
2140        // Configure auth for custom payment methods if auth is enabled
2141        if let (Some(ref auth_settings), Some(auth_db)) = (&settings.auth, &auth_localstore) {
2142            if auth_settings.auth_enabled {
2143                use std::collections::HashMap;
2144
2145                use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
2146                use cdk::nuts::AuthRequired;
2147
2148                use crate::config::AuthType;
2149
2150                // First, remove all existing payment-method-related endpoints from the database
2151                // to ensure old payment methods don't persist when configuration changes
2152                let existing_endpoints = auth_db.get_auth_for_endpoints().await?;
2153                let payment_method_endpoints_to_remove: Vec<ProtectedEndpoint> = existing_endpoints
2154                    .keys()
2155                    .filter(|endpoint| {
2156                        matches!(
2157                            endpoint.path,
2158                            RoutePath::MintQuote(_)
2159                                | RoutePath::Mint(_)
2160                                | RoutePath::MeltQuote(_)
2161                                | RoutePath::Melt(_)
2162                        )
2163                    })
2164                    .cloned()
2165                    .collect();
2166
2167                if !payment_method_endpoints_to_remove.is_empty() {
2168                    tracing::debug!(
2169                        "Removing {} old payment method endpoints from database",
2170                        payment_method_endpoints_to_remove.len()
2171                    );
2172                    let mut tx = auth_db.begin_transaction().await?;
2173                    tx.remove_protected_endpoints(payment_method_endpoints_to_remove)
2174                        .await?;
2175                    tx.commit().await?;
2176                }
2177
2178                // Now add endpoints for current payment methods
2179                if !custom_methods.is_empty() {
2180                    let mut protected_endpoints = HashMap::new();
2181
2182                    for method_name in &custom_methods {
2183                        tracing::debug!(
2184                            "Adding auth endpoints for payment method: {}",
2185                            method_name
2186                        );
2187
2188                        // Determine auth type based on settings
2189                        let mint_quote_auth = match auth_settings.get_mint_quote {
2190                            AuthType::Clear => Some(AuthRequired::Clear),
2191                            AuthType::Blind => Some(AuthRequired::Blind),
2192                            AuthType::None => None,
2193                        };
2194
2195                        let check_mint_quote_auth = match auth_settings.check_mint_quote {
2196                            AuthType::Clear => Some(AuthRequired::Clear),
2197                            AuthType::Blind => Some(AuthRequired::Blind),
2198                            AuthType::None => None,
2199                        };
2200
2201                        let mint_auth = match auth_settings.mint {
2202                            AuthType::Clear => Some(AuthRequired::Clear),
2203                            AuthType::Blind => Some(AuthRequired::Blind),
2204                            AuthType::None => None,
2205                        };
2206
2207                        let melt_quote_auth = match auth_settings.get_melt_quote {
2208                            AuthType::Clear => Some(AuthRequired::Clear),
2209                            AuthType::Blind => Some(AuthRequired::Blind),
2210                            AuthType::None => None,
2211                        };
2212
2213                        let check_melt_quote_auth = match auth_settings.check_melt_quote {
2214                            AuthType::Clear => Some(AuthRequired::Clear),
2215                            AuthType::Blind => Some(AuthRequired::Blind),
2216                            AuthType::None => None,
2217                        };
2218
2219                        let melt_auth = match auth_settings.melt {
2220                            AuthType::Clear => Some(AuthRequired::Clear),
2221                            AuthType::Blind => Some(AuthRequired::Blind),
2222                            AuthType::None => None,
2223                        };
2224
2225                        // Create endpoints for each payment method operation
2226                        if let Some(auth) = mint_quote_auth {
2227                            protected_endpoints.insert(
2228                                ProtectedEndpoint::new(
2229                                    Method::Post,
2230                                    RoutePath::MintQuote(method_name.clone()),
2231                                ),
2232                                auth,
2233                            );
2234                        }
2235                        if let Some(auth) = check_mint_quote_auth {
2236                            protected_endpoints.insert(
2237                                ProtectedEndpoint::new(
2238                                    Method::Get,
2239                                    RoutePath::MintQuote(method_name.clone()),
2240                                ),
2241                                auth,
2242                            );
2243                        }
2244                        if let Some(auth) = mint_auth {
2245                            protected_endpoints.insert(
2246                                ProtectedEndpoint::new(
2247                                    Method::Post,
2248                                    RoutePath::Mint(method_name.clone()),
2249                                ),
2250                                auth,
2251                            );
2252                        }
2253                        if let Some(auth) = melt_quote_auth {
2254                            protected_endpoints.insert(
2255                                ProtectedEndpoint::new(
2256                                    Method::Post,
2257                                    RoutePath::MeltQuote(method_name.clone()),
2258                                ),
2259                                auth,
2260                            );
2261                        }
2262                        if let Some(auth) = check_melt_quote_auth {
2263                            protected_endpoints.insert(
2264                                ProtectedEndpoint::new(
2265                                    Method::Get,
2266                                    RoutePath::MeltQuote(method_name.clone()),
2267                                ),
2268                                auth,
2269                            );
2270                        }
2271                        if let Some(auth) = melt_auth {
2272                            protected_endpoints.insert(
2273                                ProtectedEndpoint::new(
2274                                    Method::Post,
2275                                    RoutePath::Melt(method_name.clone()),
2276                                ),
2277                                auth,
2278                            );
2279                        }
2280                    }
2281
2282                    // Add all custom endpoints in one transaction
2283                    if !protected_endpoints.is_empty() {
2284                        let mut tx = auth_db.begin_transaction().await?;
2285                        tx.add_protected_endpoints(protected_endpoints).await?;
2286                        tx.commit().await?;
2287                    }
2288                }
2289            }
2290        }
2291
2292        let v1_service = cdk_axum::create_mint_router_with_custom_cache(
2293            Arc::clone(&mint),
2294            cache,
2295            custom_methods,
2296            settings.info.enable_info_page.unwrap_or(true),
2297        )
2298        .await?;
2299
2300        let mut mint_service = Router::new()
2301            .merge(v1_service)
2302            .layer(DefaultBodyLimit::max(REQUEST_BODY_LIMIT_BYTES))
2303            .layer(
2304                ServiceBuilder::new()
2305                    .layer(RequestDecompressionLayer::new())
2306                    .layer(CompressionLayer::new()),
2307            )
2308            .layer(TraceLayer::new_for_http());
2309
2310        for router in routers {
2311            mint_service = mint_service.merge(router);
2312        }
2313
2314        // Create a broadcast channel to share shutdown signal between services
2315        let (shutdown_tx, _) = tokio::sync::broadcast::channel::<()>(1);
2316
2317        Ok(Self {
2318            mint,
2319            #[cfg(feature = "prometheus")]
2320            prometheus: settings.prometheus.clone(),
2321            mint_service,
2322            listen_addr,
2323            listen_port,
2324            activation,
2325            shutdown_tx,
2326            #[cfg(feature = "management-rpc")]
2327            rpc_to_start,
2328        })
2329    }
2330
2331    /// Starts all services, binds the HTTP listener, and commits the
2332    /// configuration as applied.
2333    async fn activate(self) -> Result<RunningMintd> {
2334        #[cfg(feature = "management-rpc")]
2335        let rpc_server = {
2336            if let Some((mut mint_rpc, tls_dir)) = self.rpc_to_start {
2337                mint_rpc.start(tls_dir).await?;
2338                Some(mint_rpc)
2339            } else {
2340                None
2341            }
2342        };
2343
2344        // Start Prometheus server if enabled
2345        #[cfg(feature = "prometheus")]
2346        let prometheus_handle = {
2347            if let Some(prometheus_settings) = &self.prometheus {
2348                if prometheus_settings.enabled {
2349                    let addr = prometheus_settings
2350                        .address
2351                        .clone()
2352                        .unwrap_or("127.0.0.1".to_string());
2353                    let port = prometheus_settings.port.unwrap_or(9000);
2354
2355                    let address = format!("{addr}:{port}")
2356                        .parse()
2357                        .with_context(|| format!("Invalid Prometheus address {addr}:{port}"))?;
2358
2359                    let server = cdk_prometheus::PrometheusBuilder::new()
2360                        .bind_address(address)
2361                        .build_with_cdk_metrics()?;
2362
2363                    let mut shutdown_rx = self.shutdown_tx.subscribe();
2364                    let prometheus_shutdown = async move {
2365                        let _ = shutdown_rx.recv().await;
2366                    };
2367
2368                    Some(tokio::spawn(async move {
2369                        if let Err(e) = server.start(prometheus_shutdown).await {
2370                            tracing::error!("Failed to start prometheus server: {}", e);
2371                        }
2372                    }))
2373                } else {
2374                    None
2375                }
2376            } else {
2377                None
2378            }
2379        };
2380
2381        self.mint.start().await?;
2382
2383        let socket_addr =
2384            SocketAddr::from_str(&format!("{}:{}", self.listen_addr, self.listen_port))?;
2385
2386        let listener = tokio::net::TcpListener::bind(socket_addr).await?;
2387
2388        tracing::info!("listening on {}", listener.local_addr()?);
2389
2390        // All fallible startup steps have succeeded and the daemon is about to
2391        // serve with this configuration, so it can be recorded as applied.
2392        if let Some(activation) = &self.activation {
2393            activation.mark_applied().await?;
2394        }
2395
2396        Ok(RunningMintd {
2397            mint: self.mint,
2398            mint_service: self.mint_service,
2399            listener,
2400            shutdown_tx: self.shutdown_tx,
2401            #[cfg(feature = "prometheus")]
2402            prometheus_handle,
2403            #[cfg(feature = "management-rpc")]
2404            rpc_server,
2405        })
2406    }
2407}
2408
2409impl RunningMintd {
2410    /// Serves requests until the shutdown signal fires, then stops all
2411    /// services gracefully.
2412    async fn serve(
2413        self,
2414        shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
2415    ) -> Result<()> {
2416        // Create a task to wait for the shutdown signal and broadcast it
2417        let shutdown_broadcast_task = {
2418            let shutdown_tx = self.shutdown_tx.clone();
2419            tokio::spawn(async move {
2420                shutdown_signal.await;
2421                tracing::info!("Shutdown signal received, broadcasting to all services");
2422                let _ = shutdown_tx.send(());
2423            })
2424        };
2425
2426        // Create shutdown future for axum server
2427        let mut axum_shutdown_rx = self.shutdown_tx.subscribe();
2428        let axum_shutdown = async move {
2429            let _ = axum_shutdown_rx.recv().await;
2430        };
2431
2432        // Wait for axum server to complete with custom shutdown signal
2433        let axum_result =
2434            axum::serve(self.listener, self.mint_service).with_graceful_shutdown(axum_shutdown);
2435
2436        match axum_result.await {
2437            Ok(_) => {
2438                tracing::info!("Axum server stopped with okay status");
2439            }
2440            Err(err) => {
2441                tracing::warn!("Axum server stopped with error");
2442                tracing::error!("{}", err);
2443                bail!("Axum exited with error")
2444            }
2445        }
2446
2447        // Wait for the shutdown broadcast task to complete
2448        let _ = shutdown_broadcast_task.await;
2449
2450        // Wait for prometheus server to shutdown if it was started
2451        #[cfg(feature = "prometheus")]
2452        if let Some(handle) = self.prometheus_handle {
2453            if let Err(e) = handle.await {
2454                tracing::warn!("Prometheus server task failed: {}", e);
2455            }
2456        }
2457
2458        self.mint.stop().await?;
2459
2460        #[cfg(feature = "management-rpc")]
2461        {
2462            if let Some(rpc_server) = self.rpc_server {
2463                rpc_server.stop().await?;
2464            }
2465        }
2466
2467        Ok(())
2468    }
2469}
2470
2471/// Starts all mintd services and blocks until the shutdown signal fires.
2472#[allow(clippy::too_many_arguments)]
2473async fn start_services_with_shutdown(
2474    mint: Arc<cdk::mint::Mint>,
2475    settings: &config::Settings,
2476    work_dir: &Path,
2477    wallet_info_provider: ConfiguredWalletInfoProvider,
2478    mint_builder_info: cdk::nuts::MintInfo,
2479    shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
2480    routers: Vec<Router>,
2481    auth_localstore: Option<cdk_common::database::DynMintAuthDatabase>,
2482    activation: Option<ConfigurationActivation>,
2483) -> Result<()> {
2484    let prepared = PreparedMintd::prepare(
2485        mint,
2486        settings,
2487        work_dir,
2488        wallet_info_provider,
2489        mint_builder_info,
2490        routers,
2491        auth_localstore,
2492        activation,
2493    )
2494    .await?;
2495    let running = prepared.activate().await?;
2496    running.serve(shutdown_signal).await
2497}
2498
2499async fn shutdown_signal() {
2500    tokio::signal::ctrl_c()
2501        .await
2502        .expect("failed to install CTRL+C handler");
2503    tracing::info!("Shutdown signal received");
2504}
2505
2506fn work_dir() -> Result<PathBuf> {
2507    let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?;
2508    let dir = home_dir.join(".cdk-mintd");
2509
2510    std::fs::create_dir_all(&dir)?;
2511
2512    Ok(dir)
2513}
2514
2515/// The main entry point for the application when used as a library
2516pub async fn run_mintd(
2517    work_dir: &Path,
2518    settings: &config::Settings,
2519    db_password: Option<String>,
2520    enable_logging: bool,
2521    runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
2522    routers: Vec<Router>,
2523) -> Result<()> {
2524    let _guard = if enable_logging {
2525        setup_tracing(work_dir, &settings.info.logging)?
2526    } else {
2527        None
2528    };
2529
2530    let result = run_mintd_with_shutdown(
2531        work_dir,
2532        settings,
2533        shutdown_signal(),
2534        db_password,
2535        runtime,
2536        routers,
2537    )
2538    .await;
2539
2540    // Explicitly drop the guard to ensure proper cleanup
2541    if let Some(guard) = _guard {
2542        tracing::info!("Shutting down logging worker thread");
2543        drop(guard);
2544        // Give the worker thread a moment to flush any remaining logs
2545        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2546    }
2547
2548    tracing::info!("Mintd shutdown");
2549
2550    result
2551}
2552
2553/// Run mintd with a custom shutdown signal
2554pub async fn run_mintd_with_shutdown(
2555    work_dir: &Path,
2556    settings: &config::Settings,
2557    shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
2558    db_password: Option<String>,
2559    runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
2560    routers: Vec<Router>,
2561) -> Result<()> {
2562    let (localstore, keystore, kv, _configuration_store) =
2563        initial_setup(work_dir, settings, db_password.clone()).await?;
2564
2565    run_mintd_with_database_and_shutdown(
2566        work_dir,
2567        settings,
2568        localstore,
2569        keystore,
2570        kv,
2571        shutdown_signal,
2572        db_password,
2573        runtime,
2574        routers,
2575        None,
2576        None,
2577    )
2578    .await
2579}
2580
2581#[allow(clippy::too_many_arguments)]
2582async fn run_mintd_with_database_and_shutdown(
2583    work_dir: &Path,
2584    settings: &config::Settings,
2585    localstore: DynMintDatabase,
2586    keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
2587    kv: Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync>,
2588    shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
2589    db_password: Option<String>,
2590    runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
2591    routers: Vec<Router>,
2592    activation: Option<ConfigurationActivation>,
2593    validated_signing_source: Option<ValidatedSigningSource>,
2594) -> Result<()> {
2595    let mint_builder = MintBuilder::new(localstore);
2596
2597    // If RPC is enabled and DB contains mint_info already, initialize the builder from DB.
2598    // This ensures subsequent builder modifications (like version injection) can respect stored values.
2599    let maybe_mint_builder = {
2600        #[cfg(feature = "management-rpc")]
2601        {
2602            if activation
2603                .as_ref()
2604                .is_some_and(ConfigurationActivation::forces_configuration)
2605            {
2606                mint_builder
2607            } else if let Some(rpc_settings) = settings.mint_management_rpc.clone() {
2608                if rpc_settings.enabled {
2609                    // Best-effort: pull DB state into builder if present
2610                    let mut tmp = mint_builder;
2611                    if let Err(e) = tmp.init_from_db_if_present().await {
2612                        tracing::warn!("Failed to init builder from DB: {}", e);
2613                    }
2614                    tmp
2615                } else {
2616                    mint_builder
2617                }
2618            } else {
2619                mint_builder
2620            }
2621        }
2622        #[cfg(not(feature = "management-rpc"))]
2623        {
2624            mint_builder
2625        }
2626    };
2627
2628    let (mint_builder, wallet_info_provider) = configure_mint_builder_with_wallet_info(
2629        settings,
2630        maybe_mint_builder,
2631        runtime,
2632        work_dir,
2633        Some(kv),
2634    )
2635    .await?;
2636    let (mint_builder, auth_localstore) =
2637        setup_authentication(settings, work_dir, mint_builder, db_password).await?;
2638
2639    let config_mint_info = mint_builder.current_mint_info();
2640
2641    let mint = build_mint(
2642        settings,
2643        keystore,
2644        mint_builder,
2645        validated_signing_source.as_ref(),
2646    )
2647    .await?;
2648
2649    tracing::debug!("Mint built from builder.");
2650
2651    let mint = Arc::new(mint);
2652
2653    start_services_with_shutdown(
2654        mint.clone(),
2655        settings,
2656        work_dir,
2657        wallet_info_provider,
2658        config_mint_info,
2659        shutdown_signal,
2660        routers,
2661        auth_localstore,
2662        activation,
2663    )
2664    .await
2665}
2666
2667fn load_database_bootstrap_settings() -> Result<config::Settings> {
2668    let mut settings = config::Settings::default();
2669    if let Ok(database) = env::var(env_vars::DATABASE_ENV_VAR) {
2670        settings.database.engine =
2671            DatabaseEngine::from_str(&database).map_err(anyhow::Error::msg)?;
2672    }
2673    if settings.database.engine == DatabaseEngine::Postgres {
2674        settings.database.postgres = Some(config::PostgresConfig::default().from_env());
2675    } else {
2676        settings.database.postgres = None;
2677    }
2678    validate_database_config(&settings)?;
2679    Ok(settings)
2680}
2681
2682fn configuration_service(
2683    store: Arc<dyn KVStoreCompareAndSwap<Err = cdk_database::Error> + Send + Sync>,
2684    settings: &config::Settings,
2685) -> config_service::ConfigurationService {
2686    config_service::ConfigurationService::new(
2687        config_store::ConfigRepository::new(store),
2688        settings.database.clone(),
2689    )
2690}
2691
2692/// Validates a database-backed configuration document without writing it.
2693pub async fn validate_configuration_document(document: &str) -> Result<()> {
2694    config_service::ConfigurationService::validate_import(document).await?;
2695    Ok(())
2696}
2697
2698/// Initializes the authoritative configuration record in the selected database.
2699pub async fn initialize_configuration(
2700    work_dir: &Path,
2701    document: &str,
2702    db_password: Option<String>,
2703) -> Result<()> {
2704    let bootstrap = load_database_bootstrap_settings()?;
2705    let (localstore, _keystore, _kv, configuration_store) =
2706        initial_setup(work_dir, &bootstrap, db_password).await?;
2707    let mut mint_builder = MintBuilder::new(localstore);
2708    mint_builder.init_from_db_if_present().await?;
2709    let database_pubkey = mint_builder.current_mint_info().pubkey;
2710    configuration_service(configuration_store, &bootstrap)
2711        .initialize(document, database_pubkey)
2712        .await?;
2713    Ok(())
2714}
2715
2716/// Validates and atomically replaces the authoritative configuration record.
2717pub async fn apply_configuration(
2718    work_dir: &Path,
2719    document: &str,
2720    validate_only: bool,
2721    db_password: Option<String>,
2722) -> Result<ApplyOutcome> {
2723    let bootstrap = load_database_bootstrap_settings()?;
2724    let (_localstore, _keystore, _kv, configuration_store) =
2725        initial_setup(work_dir, &bootstrap, db_password).await?;
2726    Ok(configuration_service(configuration_store, &bootstrap)
2727        .apply(document, validate_only)
2728        .await?)
2729}
2730
2731/// Stages the last configuration known to have been applied successfully.
2732pub async fn rollback_configuration(
2733    work_dir: &Path,
2734    db_password: Option<String>,
2735) -> Result<RollbackOutcome> {
2736    let bootstrap = load_database_bootstrap_settings()?;
2737    let (_localstore, _keystore, _kv, configuration_store) =
2738        initial_setup(work_dir, &bootstrap, db_password).await?;
2739    Ok(configuration_service(configuration_store, &bootstrap)
2740        .rollback()
2741        .await?)
2742}
2743
2744/// Reads the unresolved authoritative configuration document.
2745pub async fn stored_configuration_document(
2746    work_dir: &Path,
2747    db_password: Option<String>,
2748) -> Result<String> {
2749    let bootstrap = load_database_bootstrap_settings()?;
2750    let (_localstore, _keystore, _kv, configuration_store) =
2751        initial_setup(work_dir, &bootstrap, db_password).await?;
2752    Ok(configuration_service(configuration_store, &bootstrap)
2753        .document()
2754        .await?)
2755}
2756
2757/// Runs mintd using only the configuration stored in its primary database.
2758pub async fn run_mintd_from_database(
2759    work_dir: &Path,
2760    db_password: Option<String>,
2761    enable_logging: bool,
2762    runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
2763    routers: Vec<Router>,
2764) -> Result<()> {
2765    let bootstrap = load_database_bootstrap_settings()?;
2766    let (localstore, keystore, kv, configuration_store) =
2767        initial_setup(work_dir, &bootstrap, db_password.clone()).await?;
2768    let service = configuration_service(configuration_store, &bootstrap);
2769    let startup = service.startup().await?;
2770    let validated_signing_source = Some(ValidatedSigningSource {
2771        expected_pubkey: startup.signing_identity.pubkey,
2772        remote_signatory: startup
2773            .remote_signatory
2774            .map(|signatory| -> DynSignatory { signatory }),
2775    });
2776    let activation = Some(ConfigurationActivation::new(
2777        service,
2778        startup.state,
2779        startup.revision,
2780    ));
2781    let settings = startup.resolved.settings;
2782
2783    let guard = if enable_logging {
2784        setup_tracing(work_dir, &settings.info.logging)?
2785    } else {
2786        None
2787    };
2788
2789    let result = run_mintd_with_database_and_shutdown(
2790        work_dir,
2791        &settings,
2792        localstore,
2793        keystore,
2794        kv,
2795        shutdown_signal(),
2796        db_password,
2797        runtime,
2798        routers,
2799        activation,
2800        validated_signing_source,
2801    )
2802    .await;
2803
2804    if let Some(guard) = guard {
2805        tracing::info!("Shutting down logging worker thread");
2806        drop(guard);
2807        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2808    }
2809
2810    tracing::info!("Mintd shutdown");
2811    result
2812}
2813
2814#[cfg(test)]
2815mod tests {
2816    use std::fs;
2817
2818    use cdk::nuts::{CurrencyUnit, MintMethodSettings, PaymentMethod};
2819
2820    use super::*;
2821
2822    const TEST_MNEMONIC: &str =
2823        "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
2824
2825    fn temp_seed_file(name: &str) -> PathBuf {
2826        std::env::temp_dir().join(format!("cdk_mintd_{name}_{}", std::process::id()))
2827    }
2828
2829    #[cfg(feature = "sqlite")]
2830    #[tokio::test]
2831    async fn validated_remote_signatory_identity_is_checked_at_mint_build_boundary() {
2832        use cdk_signatory::db_signatory::DbSignatory;
2833        use cdk_signatory::signatory::Signatory;
2834        use cdk_sqlite::mint::memory;
2835
2836        let expected_store = Arc::new(memory::empty().await.expect("expected signatory database"));
2837        let expected_signatory =
2838            DbSignatory::new(expected_store, &[7; 32], HashMap::new(), Default::default())
2839                .await
2840                .expect("expected signatory");
2841        let expected_pubkey = expected_signatory
2842            .keysets()
2843            .await
2844            .expect("expected keysets")
2845            .pubkey;
2846
2847        let actual_store = Arc::new(memory::empty().await.expect("actual signatory database"));
2848        let actual_signatory: DynSignatory = Arc::new(
2849            DbSignatory::new(actual_store, &[9; 32], HashMap::new(), Default::default())
2850                .await
2851                .expect("actual signatory"),
2852        );
2853        let actual_pubkey = actual_signatory
2854            .keysets()
2855            .await
2856            .expect("actual keysets")
2857            .pubkey;
2858
2859        ensure_signatory_identity(&actual_signatory, actual_pubkey)
2860            .await
2861            .expect("matching identity");
2862        let error = ensure_signatory_identity(&actual_signatory, expected_pubkey)
2863            .await
2864            .expect_err("changed identity should be rejected");
2865        assert!(
2866            error
2867                .to_string()
2868                .contains("signing identity does not match this mint database"),
2869            "unexpected error: {error}"
2870        );
2871    }
2872
2873    #[cfg(feature = "sqlite")]
2874    #[tokio::test]
2875    async fn unapplied_configuration_refreshes_canonical_values_once() {
2876        use cdk_sqlite::mint::memory;
2877
2878        let database = Arc::new(memory::empty().await.expect("in-memory database"));
2879        let mut builder = MintBuilder::new(database.clone());
2880        builder
2881            .configure_unit(CurrencyUnit::Sat, Default::default())
2882            .expect("configure unit");
2883        let mint = builder
2884            .build_with_seed(database.clone(), &[7; 32])
2885            .await
2886            .expect("build mint");
2887
2888        let stored_info = MintBuilder::new(database.clone())
2889            .with_name("stored".to_owned())
2890            .current_mint_info();
2891        mint.set_mint_info(stored_info)
2892            .await
2893            .expect("set stored mint info");
2894        mint.set_quote_ttl(QuoteTTL::new(10, 20))
2895            .await
2896            .expect("set stored quote ttl");
2897
2898        let imported_info = MintBuilder::new(database.clone())
2899            .with_name("imported".to_owned())
2900            .current_mint_info();
2901        reconcile_canonical_configuration(&mint, imported_info, QuoteTTL::new(30, 40), false)
2902            .await
2903            .expect("apply imported canonical values");
2904        assert_eq!(
2905            mint.mint_info().await.expect("mint info").name.as_deref(),
2906            Some("imported")
2907        );
2908        assert_eq!(
2909            mint.quote_ttl().await.expect("quote ttl"),
2910            QuoteTTL::new(30, 40)
2911        );
2912
2913        let mut rpc_info = mint.mint_info().await.expect("mint info");
2914        rpc_info.name = Some("rpc-managed".to_owned());
2915        mint.set_mint_info(rpc_info)
2916            .await
2917            .expect("set RPC-managed mint info");
2918        mint.set_quote_ttl(QuoteTTL::new(50, 60))
2919            .await
2920            .expect("set RPC-managed quote ttl");
2921
2922        let later_document_info = MintBuilder::new(database)
2923            .with_name("document".to_owned())
2924            .current_mint_info();
2925        reconcile_canonical_configuration(&mint, later_document_info, QuoteTTL::new(70, 80), true)
2926            .await
2927            .expect("preserve RPC-managed canonical values");
2928        assert_eq!(
2929            mint.mint_info().await.expect("mint info").name.as_deref(),
2930            Some("rpc-managed")
2931        );
2932        assert_eq!(
2933            mint.quote_ttl().await.expect("quote ttl"),
2934            QuoteTTL::new(50, 60)
2935        );
2936    }
2937
2938    #[cfg(feature = "sqlite")]
2939    #[tokio::test]
2940    async fn preserve_mode_seeds_empty_mint_and_missing_quote_ttl() {
2941        use cdk_sqlite::mint::memory;
2942
2943        let database = Arc::new(memory::empty().await.expect("in-memory database"));
2944        let mut builder = MintBuilder::new(database.clone());
2945        builder
2946            .configure_unit(CurrencyUnit::Sat, Default::default())
2947            .expect("configure unit");
2948        let mint = builder
2949            .build_with_seed(database.clone(), &[9; 32])
2950            .await
2951            .expect("build mint");
2952
2953        // Fresh mint stores default mint info during build; clear preservation by exercising
2954        // the branch where quote TTL has not been persisted yet.
2955        let seeded_info = MintBuilder::new(database.clone())
2956            .with_name("seeded".to_owned())
2957            .current_mint_info();
2958        // Force mint info missing path using a separate mint without set info if possible.
2959        // If mint always has info after build, still cover missing-ttl path.
2960        if mint.mint_info().await.is_ok() {
2961            // Overwrite mint info without quote ttl persistence by using an empty info DB path:
2962            // re-build mint and never call set_quote_ttl.
2963            let database = Arc::new(memory::empty().await.expect("second database"));
2964            let mut builder = MintBuilder::new(database.clone());
2965            builder
2966                .configure_unit(CurrencyUnit::Sat, Default::default())
2967                .expect("configure unit");
2968            let mint = builder
2969                .build_with_seed(database.clone(), &[11; 32])
2970                .await
2971                .expect("build mint");
2972            assert!(!mint
2973                .quote_ttl_is_persisted()
2974                .await
2975                .expect("quote ttl persistence probe"));
2976            let info = MintBuilder::new(database)
2977                .with_name("preserve-ttl".to_owned())
2978                .current_mint_info();
2979            reconcile_canonical_configuration(&mint, info, QuoteTTL::new(1, 2), true)
2980                .await
2981                .expect("preserve with missing ttl");
2982            assert!(mint
2983                .quote_ttl_is_persisted()
2984                .await
2985                .expect("quote ttl should now be persisted"));
2986            assert_eq!(
2987                mint.quote_ttl().await.expect("quote ttl"),
2988                QuoteTTL::new(1, 2)
2989            );
2990        } else {
2991            reconcile_canonical_configuration(&mint, seeded_info, QuoteTTL::new(3, 4), true)
2992                .await
2993                .expect("seed mint info when missing");
2994            assert_eq!(
2995                mint.mint_info().await.expect("mint info").name.as_deref(),
2996                Some("seeded")
2997            );
2998        }
2999    }
3000
3001    #[cfg(feature = "sqlite")]
3002    #[tokio::test]
3003    async fn unapplied_configuration_preserves_existing_mint_pubkey() {
3004        use cdk::nuts::PublicKey;
3005        use cdk_sqlite::mint::memory;
3006
3007        let database = Arc::new(memory::empty().await.expect("in-memory database"));
3008        let mut builder = MintBuilder::new(database.clone());
3009        builder
3010            .configure_unit(CurrencyUnit::Sat, Default::default())
3011            .expect("configure unit");
3012        let mint = builder
3013            .build_with_seed(database.clone(), &[13; 32])
3014            .await
3015            .expect("build mint");
3016
3017        let pubkey = PublicKey::from_hex(
3018            "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619",
3019        )
3020        .expect("static pubkey");
3021        let mut stored = MintBuilder::new(database.clone())
3022            .with_name("stored".to_owned())
3023            .current_mint_info();
3024        stored.pubkey = Some(pubkey);
3025        mint.set_mint_info(stored)
3026            .await
3027            .expect("set stored mint info");
3028
3029        let mut imported = MintBuilder::new(database)
3030            .with_name("imported".to_owned())
3031            .current_mint_info();
3032        imported.pubkey = None;
3033        reconcile_canonical_configuration(&mint, imported, QuoteTTL::new(7, 8), false)
3034            .await
3035            .expect("apply imported values");
3036        assert_eq!(
3037            mint.mint_info().await.expect("mint info").pubkey,
3038            Some(pubkey)
3039        );
3040        assert_eq!(
3041            mint.mint_info().await.expect("mint info").name.as_deref(),
3042            Some("imported")
3043        );
3044    }
3045
3046    #[cfg(all(feature = "sqlite", feature = "fakewallet"))]
3047    #[tokio::test]
3048    async fn database_configuration_public_api_round_trip() {
3049        let work_dir = crate::test_utils::unique_temp_path("cdk_mintd_public_config_api");
3050        fs::create_dir_all(&work_dir).expect("create work dir");
3051        let secret_path = work_dir.join("mnemonic.secret");
3052        fs::write(&secret_path, TEST_MNEMONIC).expect("write mnemonic secret");
3053
3054        #[cfg(feature = "sqlcipher")]
3055        let password = Some("test-password".to_string());
3056        #[cfg(not(feature = "sqlcipher"))]
3057        let password: Option<String> = None;
3058
3059        let first = format!(
3060            r#"
3061[info]
3062mnemonic = "file:{}"
3063
3064[mint_info]
3065name = "first-public"
3066
3067[payment_backend]
3068backend = "fakewallet"
3069
3070[fake_wallet]
3071
3072[database]
3073engine = "sqlite"
3074"#,
3075            secret_path.display()
3076        );
3077        let second = first.replace("first-public", "second-public");
3078
3079        validate_configuration_document(&first)
3080            .await
3081            .expect("validate first document");
3082        initialize_configuration(&work_dir, &first, password.clone())
3083            .await
3084            .expect("initialize configuration");
3085        assert_eq!(
3086            stored_configuration_document(&work_dir, password.clone())
3087                .await
3088                .expect("read stored document"),
3089            first
3090        );
3091
3092        let validate_only = apply_configuration(&work_dir, &second, true, password.clone())
3093            .await
3094            .expect("validate-only apply");
3095        assert!(!validate_only.restart_required);
3096        assert_eq!(
3097            stored_configuration_document(&work_dir, password.clone())
3098                .await
3099                .expect("document unchanged"),
3100            first
3101        );
3102
3103        let applied = apply_configuration(&work_dir, &second, false, password.clone())
3104            .await
3105            .expect("apply replacement");
3106        assert!(applied.restart_required);
3107        assert_eq!(
3108            stored_configuration_document(&work_dir, password)
3109                .await
3110                .expect("replacement stored"),
3111            second
3112        );
3113
3114        let bootstrap = load_database_bootstrap_settings().expect("bootstrap settings");
3115        assert_eq!(bootstrap.database.engine, DatabaseEngine::Sqlite);
3116        assert!(bootstrap.database.postgres.is_none());
3117
3118        let _ = fs::remove_dir_all(&work_dir);
3119    }
3120
3121    #[test]
3122    fn load_database_bootstrap_settings_defaults_to_sqlite() {
3123        let _env_lock = crate::test_utils::env_lock();
3124        clear_mintd_env();
3125        std::env::remove_var(env_vars::DATABASE_ENV_VAR);
3126
3127        let settings = load_database_bootstrap_settings().expect("default bootstrap");
3128        assert_eq!(settings.database.engine, DatabaseEngine::Sqlite);
3129        assert!(settings.database.postgres.is_none());
3130        clear_mintd_env();
3131    }
3132
3133    #[test]
3134    fn apply_seed_file_sets_mint_mnemonic_from_trimmed_file_contents() {
3135        let seed_file = temp_seed_file("seed_file_sets_seed");
3136        fs::write(&seed_file, format!("  {TEST_MNEMONIC}\n")).expect("seed file should be written");
3137        let mut settings = config::Settings {
3138            info: config::Info {
3139                seed: Some("raw seed from config".to_string()),
3140                mnemonic: Some("mnemonic from config".to_string()),
3141                ..Default::default()
3142            },
3143            signatory: Some(config::Signatory {
3144                enabled: true,
3145                address: "127.0.0.1".to_string(),
3146                port: 15060,
3147                tls_dir: Some("/tmp/certs".into()),
3148                allow_insecure: false,
3149            }),
3150            ..Default::default()
3151        };
3152
3153        apply_seed_file(&mut settings, &seed_file).expect("seed file should be applied");
3154
3155        assert_eq!(settings.info.seed, None);
3156        assert_eq!(settings.info.mnemonic, Some(TEST_MNEMONIC.to_string()));
3157        assert_eq!(
3158            settings
3159                .signatory
3160                .as_ref()
3161                .map(|signatory| signatory.address.clone()),
3162            Some("127.0.0.1".to_string())
3163        );
3164        assert_eq!(
3165            settings.signatory.as_ref().map(|signatory| signatory.port),
3166            Some(15060)
3167        );
3168        assert_eq!(
3169            settings
3170                .signatory
3171                .as_ref()
3172                .and_then(|signatory| signatory.tls_dir.clone()),
3173            Some("/tmp/certs".into())
3174        );
3175
3176        let _ = fs::remove_file(&seed_file);
3177    }
3178
3179    #[cfg(feature = "bdk")]
3180    #[test]
3181    fn apply_seed_file_sets_active_bdk_mnemonic() {
3182        use crate::config::{Bdk, Onchain, OnchainBackend};
3183
3184        let seed_file = temp_seed_file("seed_file_sets_bdk_seed");
3185        fs::write(&seed_file, TEST_MNEMONIC).expect("seed file should be written");
3186        let mut settings = config::Settings {
3187            onchain: Some(Onchain {
3188                onchain_backend: OnchainBackend::Bdk,
3189                ..Default::default()
3190            }),
3191            bdk: Some(Bdk {
3192                mnemonic: Some("old bdk mnemonic".to_string()),
3193                ..Default::default()
3194            }),
3195            ..Default::default()
3196        };
3197
3198        apply_seed_file(&mut settings, &seed_file).expect("seed file should be applied");
3199
3200        assert_eq!(
3201            settings
3202                .bdk
3203                .expect("bdk settings should be present")
3204                .mnemonic,
3205            Some(TEST_MNEMONIC.to_string())
3206        );
3207
3208        let _ = fs::remove_file(&seed_file);
3209    }
3210
3211    #[cfg(feature = "ldk-node")]
3212    #[test]
3213    fn apply_seed_file_sets_active_ldk_node_mnemonic() {
3214        use crate::config::{LdkNode, PaymentBackend, PaymentBackendType};
3215
3216        let seed_file = temp_seed_file("seed_file_sets_ldk_seed");
3217        fs::write(&seed_file, TEST_MNEMONIC).expect("seed file should be written");
3218        let mut settings = config::Settings {
3219            payment_backend: vec![PaymentBackend {
3220                backend: PaymentBackendType::LdkNode,
3221                ..Default::default()
3222            }],
3223            ldk_node: Some(LdkNode {
3224                ldk_node_mnemonic: Some("old ldk mnemonic".to_string()),
3225                ..Default::default()
3226            }),
3227            ..Default::default()
3228        };
3229
3230        apply_seed_file(&mut settings, &seed_file).expect("seed file should be applied");
3231
3232        assert_eq!(
3233            settings
3234                .ldk_node
3235                .expect("ldk node settings should be present")
3236                .ldk_node_mnemonic,
3237            Some(TEST_MNEMONIC.to_string())
3238        );
3239
3240        let _ = fs::remove_file(&seed_file);
3241    }
3242
3243    #[test]
3244    fn apply_seed_file_rejects_empty_seed_file() {
3245        let seed_file = temp_seed_file("empty_seed_file");
3246        fs::write(&seed_file, "\n\t ").expect("seed file should be written");
3247        let mut settings = config::Settings::default();
3248
3249        let err = apply_seed_file(&mut settings, &seed_file)
3250            .expect_err("empty seed file should be rejected");
3251
3252        assert!(err.to_string().contains("is empty"));
3253        assert_eq!(settings.info.seed, None);
3254
3255        let _ = fs::remove_file(&seed_file);
3256    }
3257
3258    #[test]
3259    fn apply_seed_file_rejects_invalid_seed_phrase() {
3260        let seed_file = temp_seed_file("invalid_seed_file");
3261        fs::write(&seed_file, "not a valid seed phrase").expect("seed file should be written");
3262        let mut settings = config::Settings::default();
3263
3264        let err = apply_seed_file(&mut settings, &seed_file)
3265            .expect_err("invalid seed phrase should be rejected");
3266
3267        assert!(err.to_string().contains("Invalid seed phrase"));
3268        assert_eq!(settings.info.mnemonic, None);
3269
3270        let _ = fs::remove_file(&seed_file);
3271    }
3272
3273    #[cfg(feature = "fakewallet")]
3274    #[test]
3275    fn load_settings_from_args_applies_seed_file_before_validation() {
3276        let _env_lock = crate::test_utils::env_lock();
3277        clear_mintd_env();
3278
3279        let temp_dir = crate::test_utils::unique_temp_path("seed_file_only_signing");
3280        fs::create_dir_all(&temp_dir).expect("temp directory should be created");
3281        let config_path = temp_dir.join("config.toml");
3282        fs::write(
3283            &config_path,
3284            r#"
3285[database]
3286engine = "sqlite"
3287
3288[payment_backend]
3289backend = "fakewallet"
3290"#,
3291        )
3292        .expect("config file should be written");
3293        let seed_file = temp_dir.join("seed.txt");
3294        fs::write(&seed_file, TEST_MNEMONIC).expect("seed file should be written");
3295
3296        let args = CLIArgs {
3297            work_dir: None,
3298            #[cfg(feature = "sqlcipher")]
3299            password: Some("test-password".to_string()),
3300            config: Some(config_path),
3301            seed_file: Some(seed_file),
3302            enable_logging: false,
3303            command: None,
3304        };
3305
3306        let settings = load_settings_from_args(&temp_dir, &args)
3307            .expect("seed-file-only signing should pass validation");
3308
3309        assert_eq!(settings.info.mnemonic.as_deref(), Some(TEST_MNEMONIC));
3310        let _ = fs::remove_dir_all(&temp_dir);
3311        clear_mintd_env();
3312    }
3313
3314    #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
3315    #[tokio::test]
3316    async fn fakewallet_dispatcher_uses_payment_backend_entry_unit() {
3317        use cdk::mint::MintBuilder;
3318        use cdk_sqlite::mint::memory;
3319
3320        use crate::config::{FakeWallet, PaymentBackend, PaymentBackendType};
3321
3322        let settings = config::Settings {
3323            payment_backend: vec![PaymentBackend {
3324                backend: PaymentBackendType::FakeWallet,
3325                unit: CurrencyUnit::Eur,
3326                ..Default::default()
3327            }],
3328            fake_wallet: Some(FakeWallet::default()),
3329            ..Default::default()
3330        };
3331
3332        let localstore = Arc::new(memory::empty().await.unwrap());
3333        let builder = MintBuilder::new(localstore);
3334        let builder =
3335            configure_payment_backends(&settings, builder, None, &std::env::temp_dir(), None)
3336                .await
3337                .expect("dispatcher should succeed");
3338
3339        let mint_info = builder.current_mint_info();
3340        let units: Vec<_> = mint_info
3341            .nuts
3342            .nut04
3343            .methods
3344            .iter()
3345            .map(|m| m.unit.clone())
3346            .collect();
3347        assert!(
3348            units.contains(&CurrencyUnit::Eur),
3349            "expected Eur, got {units:?}"
3350        );
3351        assert!(
3352            !units.contains(&CurrencyUnit::Sat),
3353            "Sat would only appear if supported_units leaked through; got {units:?}"
3354        );
3355    }
3356
3357    #[test]
3358    fn backend_unit_validation_allows_matching_units() {
3359        validate_backend_unit(&CurrencyUnit::Eur, "EUR").expect("matching units should pass");
3360    }
3361
3362    #[test]
3363    fn backend_unit_validation_allows_sat_msat_pair() {
3364        validate_backend_unit(&CurrencyUnit::Sat, "MSAT")
3365            .expect("sat/msat compatible units should pass");
3366        validate_backend_unit(&CurrencyUnit::Msat, "SAT")
3367            .expect("msat/sat compatible units should pass");
3368    }
3369
3370    #[test]
3371    fn backend_unit_validation_rejects_unsupported_conversion() {
3372        let err = validate_backend_unit(&CurrencyUnit::Eur, "SAT")
3373            .expect_err("sat backend should not advertise eur");
3374
3375        assert!(
3376            err.to_string().contains("only matching units"),
3377            "error should explain the supported conversions: {err}"
3378        );
3379    }
3380
3381    #[cfg(feature = "cln")]
3382    #[test]
3383    fn expand_path_expands_bare_tilde_without_panic() {
3384        let expanded = expand_path("~");
3385
3386        assert_eq!(expanded, home::home_dir());
3387    }
3388
3389    #[cfg(feature = "cln")]
3390    #[test]
3391    fn expand_path_keeps_named_tilde_paths_literal() {
3392        let expanded = expand_path("~foo").expect("path should be returned");
3393
3394        assert_eq!(expanded, PathBuf::from("~foo"));
3395    }
3396
3397    #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
3398    #[tokio::test]
3399    async fn duplicate_payment_backend_unit_method_pair_is_rejected() {
3400        use cdk::mint::MintBuilder;
3401        use cdk_sqlite::mint::memory;
3402
3403        use crate::config::{FakeWallet, PaymentBackend, PaymentBackendType};
3404
3405        let settings = config::Settings {
3406            payment_backend: vec![
3407                PaymentBackend {
3408                    backend: PaymentBackendType::FakeWallet,
3409                    unit: CurrencyUnit::Sat,
3410                    ..Default::default()
3411                },
3412                PaymentBackend {
3413                    backend: PaymentBackendType::FakeWallet,
3414                    unit: CurrencyUnit::Sat,
3415                    ..Default::default()
3416                },
3417            ],
3418            fake_wallet: Some(FakeWallet::default()),
3419            ..Default::default()
3420        };
3421
3422        let localstore = Arc::new(memory::empty().await.unwrap());
3423        let builder = MintBuilder::new(localstore);
3424        let err = configure_payment_backends(&settings, builder, None, &std::env::temp_dir(), None)
3425            .await
3426            .expect_err("duplicate unit/method pair should be rejected");
3427
3428        assert!(err.to_string().contains("Duplicate payment processor"));
3429    }
3430
3431    #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
3432    #[tokio::test]
3433    async fn empty_payment_backend_vec_returns_unchanged_builder() {
3434        use cdk::mint::MintBuilder;
3435        use cdk_sqlite::mint::memory;
3436
3437        let settings = config::Settings {
3438            payment_backend: vec![],
3439            ..Default::default()
3440        };
3441
3442        let localstore = Arc::new(memory::empty().await.unwrap());
3443        let builder = MintBuilder::new(localstore);
3444        let builder =
3445            configure_payment_backends(&settings, builder, None, &std::env::temp_dir(), None)
3446                .await
3447                .expect("empty payment_backend should succeed");
3448
3449        let mint_info = builder.current_mint_info();
3450        assert!(
3451            mint_info.nuts.nut04.methods.is_empty(),
3452            "no backends should be registered"
3453        );
3454    }
3455
3456    #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
3457    #[tokio::test]
3458    async fn payment_backend_none_logs_and_continues() {
3459        use cdk::mint::MintBuilder;
3460        use cdk_sqlite::mint::memory;
3461
3462        use crate::config::{PaymentBackend, PaymentBackendType};
3463
3464        let settings = config::Settings {
3465            payment_backend: vec![PaymentBackend {
3466                backend: PaymentBackendType::None,
3467                unit: CurrencyUnit::Sat,
3468                ..Default::default()
3469            }],
3470            ..Default::default()
3471        };
3472
3473        let localstore = Arc::new(memory::empty().await.unwrap());
3474        let builder = MintBuilder::new(localstore);
3475        let builder =
3476            configure_payment_backends(&settings, builder, None, &std::env::temp_dir(), None)
3477                .await
3478                .expect("PaymentBackendType::None should succeed");
3479
3480        let mint_info = builder.current_mint_info();
3481        assert!(
3482            mint_info.nuts.nut04.methods.is_empty(),
3483            "PaymentBackendType::None should not register any methods"
3484        );
3485    }
3486
3487    #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
3488    #[tokio::test]
3489    async fn onchain_backend_none_returns_unchanged() {
3490        use cdk::mint::MintBuilder;
3491        use cdk_sqlite::mint::memory;
3492
3493        use crate::config::{Onchain, OnchainBackend};
3494
3495        let settings = config::Settings {
3496            onchain: Some(Onchain {
3497                onchain_backend: OnchainBackend::None,
3498                ..Default::default()
3499            }),
3500            ..Default::default()
3501        };
3502
3503        let localstore = Arc::new(memory::empty().await.unwrap());
3504        let builder = MintBuilder::new(localstore);
3505        let builder =
3506            configure_onchain_backend(&settings, builder, None, &std::env::temp_dir(), None)
3507                .await
3508                .expect("OnchainBackend::None should succeed");
3509
3510        let mint_info = builder.current_mint_info();
3511        assert!(
3512            mint_info.nuts.nut04.methods.is_empty(),
3513            "OnchainBackend::None should not register any methods"
3514        );
3515    }
3516
3517    #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
3518    #[tokio::test]
3519    async fn fakewallet_onchain_no_payment_backend_configures_onchain_methods() {
3520        use cdk::mint::MintBuilder;
3521        use cdk_sqlite::mint::memory;
3522
3523        use crate::config::{
3524            FakeWallet, Onchain, OnchainBackend, PaymentBackend, PaymentBackendType,
3525        };
3526
3527        let settings = config::Settings {
3528            payment_backend: vec![PaymentBackend {
3529                backend: PaymentBackendType::None,
3530                ..Default::default()
3531            }],
3532            onchain: Some(Onchain {
3533                onchain_backend: OnchainBackend::FakeWallet,
3534                ..Default::default()
3535            }),
3536            fake_wallet: Some(FakeWallet::default()),
3537            ..Default::default()
3538        };
3539
3540        let localstore = Arc::new(memory::empty().await.unwrap());
3541        let builder = MintBuilder::new(localstore);
3542        let builder =
3543            configure_onchain_backend(&settings, builder, None, &std::env::temp_dir(), None)
3544                .await
3545                .expect("fakewallet onchain should succeed");
3546
3547        let mint_info = builder.current_mint_info();
3548        let methods: Vec<_> = mint_info
3549            .nuts
3550            .nut04
3551            .methods
3552            .iter()
3553            .map(|m| m.method.clone())
3554            .collect();
3555        assert!(
3556            methods.contains(&PaymentMethod::Known(KnownMethod::Onchain)),
3557            "expected onchain method, got {methods:?}"
3558        );
3559    }
3560
3561    #[cfg(all(feature = "fakewallet", feature = "cln", feature = "sqlite"))]
3562    #[tokio::test]
3563    async fn fakewallet_onchain_with_real_payment_backend_bails() {
3564        use cdk::mint::MintBuilder;
3565        use cdk_sqlite::mint::memory;
3566
3567        use crate::config::{Onchain, OnchainBackend, PaymentBackend, PaymentBackendType};
3568
3569        let settings = config::Settings {
3570            payment_backend: vec![PaymentBackend {
3571                backend: PaymentBackendType::Cln,
3572                unit: CurrencyUnit::Sat,
3573                ..Default::default()
3574            }],
3575            onchain: Some(Onchain {
3576                onchain_backend: OnchainBackend::FakeWallet,
3577                ..Default::default()
3578            }),
3579            ..Default::default()
3580        };
3581
3582        let localstore = Arc::new(memory::empty().await.unwrap());
3583        let builder = MintBuilder::new(localstore);
3584        let err = configure_onchain_backend(&settings, builder, None, &std::env::temp_dir(), None)
3585            .await
3586            .expect_err("fakewallet onchain with real payment backend should bail");
3587
3588        assert!(
3589            err.to_string().contains("fakewallet"),
3590            "error should mention fakewallet: {err}"
3591        );
3592    }
3593
3594    #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
3595    #[tokio::test]
3596    async fn configure_mint_builder_no_backends_bails() {
3597        use cdk::mint::MintBuilder;
3598        use cdk_sqlite::mint::memory;
3599
3600        use crate::config::{PaymentBackend, PaymentBackendType};
3601
3602        let settings = config::Settings {
3603            payment_backend: vec![PaymentBackend {
3604                backend: PaymentBackendType::None,
3605                ..Default::default()
3606            }],
3607            ..Default::default()
3608        };
3609
3610        let localstore = Arc::new(memory::empty().await.unwrap());
3611        let builder = MintBuilder::new(localstore);
3612        let err = configure_mint_builder(&settings, builder, None, &std::env::temp_dir(), None)
3613            .await
3614            .expect_err("no payment backends should bail");
3615
3616        assert!(
3617            err.to_string().contains("At least one payment backend"),
3618            "error should mention missing backends: {err}"
3619        );
3620    }
3621
3622    #[cfg(all(feature = "fakewallet", feature = "sqlite", feature = "bdk"))]
3623    #[tokio::test]
3624    async fn configure_mint_builder_fake_wallet_with_bdk_onchain_bails() {
3625        use cdk::mint::MintBuilder;
3626        use cdk_sqlite::mint::memory;
3627
3628        use crate::config::{
3629            Bdk, FakeWallet, Onchain, OnchainBackend, PaymentBackend, PaymentBackendType,
3630        };
3631
3632        let settings = config::Settings {
3633            payment_backend: vec![PaymentBackend {
3634                backend: PaymentBackendType::FakeWallet,
3635                ..Default::default()
3636            }],
3637            onchain: Some(Onchain {
3638                onchain_backend: OnchainBackend::Bdk,
3639                ..Default::default()
3640            }),
3641            fake_wallet: Some(FakeWallet::default()),
3642            bdk: Some(Bdk {
3643                network: Some("mainnet".to_string()),
3644                ..Default::default()
3645            }),
3646            ..Default::default()
3647        };
3648
3649        let localstore = Arc::new(memory::empty().await.unwrap());
3650        let builder = MintBuilder::new(localstore);
3651        let err = configure_mint_builder(&settings, builder, None, &std::env::temp_dir(), None)
3652            .await
3653            .expect_err("fake wallet with BDK onchain should bail");
3654
3655        assert!(
3656            err.to_string().contains("fakewallet") && err.to_string().contains("bdk"),
3657            "error should mention backend pairing validation: {err}"
3658        );
3659    }
3660
3661    #[cfg(all(feature = "management-rpc", feature = "bdk", feature = "sqlite"))]
3662    #[tokio::test]
3663    async fn bdk_onchain_exposes_wallet_info_provider() {
3664        use cdk::mint::MintBuilder;
3665        use cdk_sqlite::mint::memory;
3666
3667        use crate::config::{Bdk, Onchain, OnchainBackend};
3668
3669        let work_dir = test_utils::unique_temp_path("cdk_mintd_wallet_info_provider");
3670        let settings = config::Settings {
3671            onchain: Some(Onchain {
3672                onchain_backend: OnchainBackend::Bdk,
3673                ..Default::default()
3674            }),
3675            bdk: Some(Bdk {
3676                network: Some("regtest".to_string()),
3677                chain_source_type: Some("esplora".to_string()),
3678                esplora_url: Some("http://127.0.0.1:1".to_string()),
3679                mnemonic: Some(
3680                    "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
3681                        .to_string(),
3682                ),
3683                ..Default::default()
3684            }),
3685            ..Default::default()
3686        };
3687
3688        let localstore = Arc::new(memory::empty().await.expect("in-memory database"));
3689        let builder = MintBuilder::new(localstore.clone());
3690        let (builder, provider) = configure_onchain_backend_with_wallet_info(
3691            &settings,
3692            builder,
3693            None,
3694            &work_dir,
3695            Some(localstore),
3696        )
3697        .await
3698        .expect("configure BDK backend");
3699
3700        assert!(builder
3701            .current_mint_info()
3702            .nuts
3703            .nut04
3704            .methods
3705            .iter()
3706            .any(|method| method.method == PaymentMethod::Known(KnownMethod::Onchain)));
3707
3708        let provider = provider.expect("wallet info provider");
3709        let first_address = provider
3710            .create_deposit_address()
3711            .await
3712            .expect("create first operator deposit address");
3713        let second_address = provider
3714            .create_deposit_address()
3715            .await
3716            .expect("create second operator deposit address");
3717        assert_ne!(first_address, second_address);
3718
3719        let addresses = provider
3720            .list_addresses(0, 20)
3721            .await
3722            .expect("list wallet addresses");
3723        assert_eq!(addresses.total, 2);
3724        assert!(addresses
3725            .addresses
3726            .iter()
3727            .any(|address| address.address == first_address));
3728        assert!(addresses
3729            .addresses
3730            .iter()
3731            .any(|address| address.address == second_address));
3732
3733        let balance = provider.get_balance().await.expect("get wallet balance");
3734        assert_eq!(balance.network, "regtest");
3735        assert_eq!(balance.total_sat, 0);
3736
3737        drop(builder);
3738        let _ = std::fs::remove_dir_all(work_dir);
3739    }
3740
3741    #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
3742    #[tokio::test]
3743    async fn configure_backend_for_methods_registers_websockets_and_fee() {
3744        use cdk::mint::MintBuilder;
3745        use cdk_sqlite::mint::memory;
3746
3747        use crate::config::{FakeWallet, PaymentBackend, PaymentBackendType};
3748
3749        let settings = config::Settings {
3750            payment_backend: vec![PaymentBackend {
3751                backend: PaymentBackendType::FakeWallet,
3752                unit: CurrencyUnit::Sat,
3753                ..Default::default()
3754            }],
3755            fake_wallet: Some(FakeWallet::default()),
3756            info: config::Info {
3757                input_fee_ppk: Some(100),
3758                ..Default::default()
3759            },
3760            ..Default::default()
3761        };
3762
3763        let localstore = Arc::new(memory::empty().await.unwrap());
3764        let builder = MintBuilder::new(localstore);
3765
3766        let fake_wallet = settings.fake_wallet.clone().expect("fake wallet config");
3767        let fake = fake_wallet
3768            .setup(
3769                &settings,
3770                CurrencyUnit::Sat,
3771                None,
3772                &std::env::temp_dir(),
3773                None,
3774            )
3775            .await
3776            .expect("fake wallet setup");
3777
3778        let mint_melt_limits = cdk::mint::MintMeltLimits {
3779            mint_min: 1.into(),
3780            mint_max: 500_000.into(),
3781            melt_min: 1.into(),
3782            melt_max: 500_000.into(),
3783        };
3784
3785        let builder = configure_backend_for_methods(
3786            &settings,
3787            builder,
3788            CurrencyUnit::Sat,
3789            mint_melt_limits,
3790            Arc::new(fake),
3791            vec![PaymentMethod::Known(KnownMethod::Bolt11)],
3792        )
3793        .await
3794        .expect("configure_backend_for_methods should succeed");
3795
3796        let mint_info = builder.current_mint_info();
3797        assert!(
3798            !mint_info.nuts.nut04.methods.is_empty(),
3799            "bolt11 method should be registered"
3800        );
3801        assert!(
3802            !mint_info.nuts.nut17.supported.is_empty(),
3803            "websocket support should be configured"
3804        );
3805    }
3806
3807    #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
3808    #[tokio::test]
3809    async fn fakewallet_onchain_with_fake_payment_backend_does_not_duplicate() {
3810        use cdk::mint::MintBuilder;
3811        use cdk_sqlite::mint::memory;
3812
3813        use crate::config::{
3814            FakeWallet, Onchain, OnchainBackend, PaymentBackend, PaymentBackendType,
3815        };
3816
3817        let settings = config::Settings {
3818            payment_backend: vec![PaymentBackend {
3819                backend: PaymentBackendType::FakeWallet,
3820                unit: CurrencyUnit::Sat,
3821                ..Default::default()
3822            }],
3823            onchain: Some(Onchain {
3824                onchain_backend: OnchainBackend::FakeWallet,
3825                ..Default::default()
3826            }),
3827            fake_wallet: Some(FakeWallet::default()),
3828            ..Default::default()
3829        };
3830
3831        let localstore = Arc::new(memory::empty().await.unwrap());
3832        let builder = MintBuilder::new(localstore);
3833        let builder = configure_onchain_backend(
3834            &settings,
3835            builder,
3836            None,
3837            &std::env::temp_dir(),
3838            None,
3839        )
3840        .await
3841        .expect("fakewallet onchain with fake payment backend should succeed without duplicating");
3842
3843        let mint_info = builder.current_mint_info();
3844        assert!(
3845            mint_info.nuts.nut04.methods.is_empty(),
3846            "when has_payment_backend is true and no real payment backend, fakewallet onchain should skip; got {:?}",
3847            mint_info.nuts.nut04.methods
3848        );
3849    }
3850
3851    #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
3852    #[tokio::test]
3853    async fn fakewallet_onchain_missing_fake_wallet_config_bails() {
3854        use cdk::mint::MintBuilder;
3855        use cdk_sqlite::mint::memory;
3856
3857        use crate::config::{Onchain, OnchainBackend, PaymentBackend, PaymentBackendType};
3858
3859        let settings = config::Settings {
3860            payment_backend: vec![PaymentBackend {
3861                backend: PaymentBackendType::None,
3862                ..Default::default()
3863            }],
3864            onchain: Some(Onchain {
3865                onchain_backend: OnchainBackend::FakeWallet,
3866                ..Default::default()
3867            }),
3868            fake_wallet: None,
3869            ..Default::default()
3870        };
3871
3872        let localstore = Arc::new(memory::empty().await.unwrap());
3873        let builder = MintBuilder::new(localstore);
3874        let err = configure_onchain_backend(&settings, builder, None, &std::env::temp_dir(), None)
3875            .await
3876            .expect_err("missing fake_wallet config should bail");
3877
3878        assert!(
3879            err.to_string().contains("Fake wallet config"),
3880            "error should mention missing config: {err}"
3881        );
3882    }
3883
3884    #[test]
3885    fn test_postgres_auth_url_validation() {
3886        // Test that the auth database config requires explicit configuration
3887
3888        // Test empty URL
3889        let auth_config = config::PostgresAuthConfig {
3890            url: "".to_string(),
3891            ..Default::default()
3892        };
3893        assert!(auth_config.url.is_empty());
3894
3895        // Test non-empty URL
3896        let auth_config = config::PostgresAuthConfig {
3897            url: "postgresql://user:password@localhost:5432/auth_db".to_string(),
3898            ..Default::default()
3899        };
3900        assert!(!auth_config.url.is_empty());
3901    }
3902
3903    #[test]
3904    fn test_extract_supported_payment_methods_unique_ordered() {
3905        let mut mint_info = cdk::nuts::MintInfo::default();
3906        mint_info.nuts.nut04.methods = vec![
3907            MintMethodSettings {
3908                method: PaymentMethod::Known(KnownMethod::Bolt11),
3909                unit: CurrencyUnit::Sat,
3910                method_name: None,
3911                min_amount: None,
3912                max_amount: None,
3913                options: None,
3914            },
3915            MintMethodSettings {
3916                method: PaymentMethod::Known(KnownMethod::Bolt12),
3917                unit: CurrencyUnit::Sat,
3918                method_name: None,
3919                min_amount: None,
3920                max_amount: None,
3921                options: None,
3922            },
3923            MintMethodSettings {
3924                method: PaymentMethod::Known(KnownMethod::Bolt11),
3925                unit: CurrencyUnit::Msat,
3926                method_name: None,
3927                min_amount: None,
3928                max_amount: None,
3929                options: None,
3930            },
3931            MintMethodSettings {
3932                method: PaymentMethod::Custom("paypal".to_string()),
3933                unit: CurrencyUnit::Usd,
3934                method_name: None,
3935                min_amount: None,
3936                max_amount: None,
3937                options: None,
3938            },
3939            MintMethodSettings {
3940                method: PaymentMethod::Custom("paypal".to_string()),
3941                unit: CurrencyUnit::Eur,
3942                method_name: None,
3943                min_amount: None,
3944                max_amount: None,
3945                options: None,
3946            },
3947        ];
3948
3949        let methods = extract_supported_payment_methods(&mint_info);
3950
3951        assert_eq!(methods, vec!["bolt11", "bolt12", "paypal"]);
3952    }
3953
3954    fn clear_mintd_env() {
3955        for var in [
3956            "CDK_MINTD_DATABASE",
3957            "CDK_MINTD_DATABASE_URL",
3958            "CDK_MINTD_POSTGRES_URL",
3959            "CDK_MINTD_POSTGRES_TLS_MODE",
3960            "CDK_MINTD_POSTGRES_MAX_CONNECTIONS",
3961            "CDK_MINTD_POSTGRES_CONNECTION_TIMEOUT_SECONDS",
3962            "CDK_MINTD_SEED",
3963            "CDK_MINTD_MNEMONIC",
3964            "CDK_MINTD_SIGNATORY_ENABLED",
3965            "CDK_MINTD_SIGNATORY_ADDRESS",
3966            "CDK_MINTD_SIGNATORY_PORT",
3967            "CDK_MINTD_SIGNATORY_TLS_DIR",
3968            "CDK_MINTD_SIGNATORY_ALLOW_INSECURE",
3969            "CDK_MINTD_LISTEN_HOST",
3970            "CDK_MINTD_LISTEN_PORT",
3971            "CDK_MINTD_PAYMENT_BACKEND",
3972            "CDK_MINTD_PAYMENT_BACKEND_MIN_MINT",
3973            "CDK_MINTD_PAYMENT_BACKEND_MAX_MINT",
3974            "CDK_MINTD_PAYMENT_BACKEND_MIN_MELT",
3975            "CDK_MINTD_PAYMENT_BACKEND_MAX_MELT",
3976            "CDK_MINTD_AUTH_ENABLED",
3977            "CDK_MINTD_AUTH_OPENID_DISCOVERY",
3978            "CDK_MINTD_AUTH_OPENID_CLIENT_ID",
3979            "CDK_MINTD_AUTH_MINT_MAX_BAT",
3980            "CDK_MINTD_AUTH_MINT",
3981            "CDK_MINTD_AUTH_GET_MINT_QUOTE",
3982            "CDK_MINTD_AUTH_CHECK_MINT_QUOTE",
3983            "CDK_MINTD_AUTH_MELT",
3984            "CDK_MINTD_AUTH_GET_MELT_QUOTE",
3985            "CDK_MINTD_AUTH_CHECK_MELT_QUOTE",
3986            "CDK_MINTD_AUTH_SWAP",
3987            "CDK_MINTD_AUTH_RESTORE",
3988            "CDK_MINTD_AUTH_CHECK_PROOF_STATE",
3989            "CDK_MINTD_AUTH_WEBSOCKET",
3990            "CDK_MINTD_AUTH_POSTGRES_URL",
3991            "CDK_MINTD_AUTH_POSTGRES_TLS_MODE",
3992            "CDK_MINTD_AUTH_POSTGRES_MAX_CONNECTIONS",
3993            "CDK_MINTD_AUTH_POSTGRES_CONNECTION_TIMEOUT_SECONDS",
3994            "CDK_MINTD_CLN_RPC_PATH",
3995            "CDK_MINTD_CLN_FEE_PERCENT",
3996            "CDK_MINTD_LND_ADDRESS",
3997            "CDK_MINTD_LND_CERT_FILE",
3998            "CDK_MINTD_LND_MACAROON_FILE",
3999            "CDK_MINTD_LND_FEE_PERCENT",
4000            "CDK_MINTD_FAKE_WALLET_SUPPORTED_UNITS",
4001            "CDK_MINTD_FAKE_WALLET_FEE_PERCENT",
4002            "CDK_MINTD_FAKE_WALLET_RESERVE_FEE_MIN",
4003            "CDK_MINTD_FAKE_WALLET_MIN_DELAY",
4004            "CDK_MINTD_FAKE_WALLET_MAX_DELAY",
4005            "CDK_MINTD_GRPC_PAYMENT_PROCESSOR_SUPPORTED_UNITS",
4006            "CDK_MINTD_GRPC_PAYMENT_PROCESSOR_ADDRESS",
4007            "CDK_MINTD_GRPC_PAYMENT_PROCESSOR_PORT",
4008            "CDK_MINTD_PROMETHEUS_ENABLED",
4009            "CDK_MINTD_PROMETHEUS_ADDRESS",
4010            "CDK_MINTD_PROMETHEUS_PORT",
4011            "CDK_MINTD_MINT_MANAGEMENT_ENABLED",
4012            "CDK_MINTD_MANAGEMENT_ADDRESS",
4013            "CDK_MINTD_MANAGEMENT_PORT",
4014        ] {
4015            std::env::remove_var(var);
4016        }
4017    }
4018
4019    fn load_settings_from_toml(name: &str, config_content: &str) -> Result<config::Settings> {
4020        use std::fs;
4021
4022        let temp_dir = crate::test_utils::unique_temp_path(name);
4023        let _ = fs::remove_dir_all(&temp_dir);
4024        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
4025        let config_path = temp_dir.join("config.toml");
4026        fs::write(&config_path, config_content).expect("Failed to write config file");
4027
4028        let result = load_settings(&temp_dir, Some(config_path));
4029
4030        let _ = fs::remove_dir_all(&temp_dir);
4031
4032        result
4033    }
4034
4035    fn assert_load_settings_error(config_content: &str, expected: &str) {
4036        let _env_lock = crate::test_utils::env_lock();
4037        clear_mintd_env();
4038        let err = load_settings_from_toml("cdk_mintd_invalid_config", config_content)
4039            .expect_err("Settings should fail validation");
4040        assert!(
4041            err.to_string().contains(expected),
4042            "expected error containing `{expected}`, got `{err}`"
4043        );
4044    }
4045
4046    #[cfg(all(feature = "prometheus", feature = "fakewallet"))]
4047    #[test]
4048    fn test_load_settings_merges_partial_postgres_toml_with_env() {
4049        use std::{env, fs};
4050
4051        let _env_lock = crate::test_utils::env_lock();
4052        clear_mintd_env();
4053        env::remove_var(crate::env_vars::DATABASE_URL_ENV_VAR);
4054        env::remove_var(crate::env_vars::ENV_POSTGRES_URL);
4055        env::remove_var(crate::env_vars::ENV_PROMETHEUS_ENABLED);
4056        env::remove_var(crate::env_vars::ENV_PROMETHEUS_ADDRESS);
4057        env::remove_var(crate::env_vars::ENV_PROMETHEUS_PORT);
4058
4059        let postgres_url = "postgresql://user:password@localhost:5432/cdk_mint";
4060        env::set_var(crate::env_vars::ENV_POSTGRES_URL, postgres_url);
4061
4062        let temp_dir = crate::test_utils::unique_temp_path("cdk_mintd_partial_config");
4063        let _ = fs::remove_dir_all(&temp_dir);
4064        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
4065        let config_path = temp_dir.join("config.toml");
4066
4067        let config_content = r#"
4068[info]
4069mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
4070
4071[database]
4072engine = "postgres"
4073
4074[database.postgres]
4075tls_mode = "require"
4076max_connections = 30
4077connection_timeout_seconds = 15
4078
4079[payment_backend]
4080backend = "fakewallet"
4081
4082[prometheus]
4083enabled = true
4084address = "0.0.0.0"
4085port = 9090
4086"#;
4087        fs::write(&config_path, config_content).expect("Failed to write config file");
4088
4089        let settings =
4090            load_settings(&temp_dir, Some(config_path)).expect("Failed to load settings");
4091
4092        let postgres = settings
4093            .database
4094            .postgres
4095            .as_ref()
4096            .expect("Postgres config should be present");
4097        assert_eq!(postgres.url, postgres_url);
4098        assert_eq!(postgres.tls_mode.as_deref(), Some("require"));
4099
4100        let prometheus = settings
4101            .prometheus
4102            .as_ref()
4103            .expect("Prometheus config should be loaded from TOML");
4104        assert!(prometheus.enabled);
4105        assert_eq!(prometheus.address.as_deref(), Some("0.0.0.0"));
4106        assert_eq!(prometheus.port, Some(9090));
4107
4108        env::remove_var(crate::env_vars::ENV_POSTGRES_URL);
4109        let _ = fs::remove_dir_all(&temp_dir);
4110    }
4111
4112    #[cfg(feature = "fakewallet")]
4113    #[test]
4114    fn test_load_settings_reports_missing_postgres_url_after_merge() {
4115        use std::{env, fs};
4116
4117        let _env_lock = crate::test_utils::env_lock();
4118        clear_mintd_env();
4119        env::remove_var(crate::env_vars::DATABASE_URL_ENV_VAR);
4120        env::remove_var(crate::env_vars::ENV_POSTGRES_URL);
4121
4122        let temp_dir = crate::test_utils::unique_temp_path("cdk_mintd_invalid_config");
4123        let _ = fs::remove_dir_all(&temp_dir);
4124        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
4125        let config_path = temp_dir.join("config.toml");
4126
4127        let config_content = r#"
4128[info]
4129mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
4130
4131[database]
4132engine = "postgres"
4133
4134[database.postgres]
4135tls_mode = "require"
4136
4137[payment_backend]
4138backend = "fakewallet"
4139"#;
4140        fs::write(&config_path, config_content).expect("Failed to write config file");
4141
4142        let err = load_settings(&temp_dir, Some(config_path))
4143            .expect_err("Settings should fail validation without a Postgres URL");
4144        assert!(err.to_string().contains("PostgreSQL URL is required"));
4145
4146        let _ = fs::remove_dir_all(&temp_dir);
4147    }
4148
4149    #[cfg(feature = "fakewallet")]
4150    #[test]
4151    fn test_load_settings_reports_short_seed() {
4152        assert_load_settings_error(
4153            r#"
4154[info]
4155seed = "tooshort"
4156
4157[database]
4158engine = "sqlite"
4159
4160[payment_backend]
4161backend = "fakewallet"
4162"#,
4163            "Seed in [info].seed is too short",
4164        );
4165    }
4166
4167    #[cfg(feature = "fakewallet")]
4168    #[test]
4169    fn test_load_settings_reports_missing_signing_source() {
4170        assert_load_settings_error(
4171            r#"
4172[database]
4173engine = "sqlite"
4174
4175[payment_backend]
4176backend = "fakewallet"
4177"#,
4178            "No signing source configured",
4179        );
4180    }
4181
4182    #[test]
4183    fn test_load_settings_reports_missing_payment_backend() {
4184        assert_load_settings_error(
4185            &format!(
4186                r#"
4187[info]
4188mnemonic = "{TEST_MNEMONIC}"
4189
4190[database]
4191engine = "sqlite"
4192"#
4193            ),
4194            "At least one payment backend",
4195        );
4196    }
4197
4198    #[cfg(feature = "cln")]
4199    #[test]
4200    fn test_load_settings_reports_missing_cln_config() {
4201        assert_load_settings_error(
4202            &format!(
4203                r#"
4204[info]
4205mnemonic = "{TEST_MNEMONIC}"
4206
4207[database]
4208engine = "sqlite"
4209
4210[payment_backend]
4211backend = "cln"
4212"#
4213            ),
4214            "CLN backend selected but [cln] config section is missing",
4215        );
4216    }
4217
4218    #[cfg(feature = "lnd")]
4219    #[test]
4220    fn test_load_settings_reports_missing_lnd_config() {
4221        assert_load_settings_error(
4222            &format!(
4223                r#"
4224[info]
4225mnemonic = "{TEST_MNEMONIC}"
4226
4227[database]
4228engine = "sqlite"
4229
4230[payment_backend]
4231backend = "lnd"
4232"#
4233            ),
4234            "LND backend selected but [lnd] config section is missing",
4235        );
4236    }
4237
4238    #[cfg(feature = "grpc-processor")]
4239    #[test]
4240    fn test_load_settings_reports_missing_grpc_supported_units() {
4241        assert_load_settings_error(
4242            &format!(
4243                r#"
4244[info]
4245mnemonic = "{TEST_MNEMONIC}"
4246
4247[database]
4248engine = "sqlite"
4249
4250[payment_backend]
4251backend = "grpcprocessor"
4252
4253[grpc_processor]
4254addr = "http://127.0.0.1"
4255"#
4256            ),
4257            "gRPC payment processor supported_units must contain at least one unit",
4258        );
4259    }
4260
4261    #[cfg(feature = "fakewallet")]
4262    #[test]
4263    fn test_load_settings_reports_invalid_fakewallet_delay_range() {
4264        assert_load_settings_error(
4265            &format!(
4266                r#"
4267[info]
4268mnemonic = "{TEST_MNEMONIC}"
4269
4270[database]
4271engine = "sqlite"
4272
4273[payment_backend]
4274backend = "fakewallet"
4275
4276[fake_wallet]
4277min_delay_time = 10
4278max_delay_time = 1
4279"#
4280            ),
4281            "Fake wallet min_delay_time cannot be greater than max_delay_time",
4282        );
4283    }
4284
4285    #[cfg(feature = "fakewallet")]
4286    #[test]
4287    fn test_load_settings_reports_missing_auth_openid_config() {
4288        assert_load_settings_error(
4289            &format!(
4290                r#"
4291[info]
4292mnemonic = "{TEST_MNEMONIC}"
4293
4294[database]
4295engine = "sqlite"
4296
4297[payment_backend]
4298backend = "fakewallet"
4299
4300[auth]
4301auth_enabled = true
4302"#
4303            ),
4304            "Auth openid_discovery must be set",
4305        );
4306    }
4307
4308    #[test]
4309    fn test_load_settings_reports_toml_parse_errors() {
4310        assert_load_settings_error(
4311            r#"
4312[info
4313mnemonic = "not valid toml"
4314"#,
4315            "Failed to read config file",
4316        );
4317    }
4318
4319    #[cfg(feature = "fakewallet")]
4320    #[test]
4321    fn test_load_settings_reports_invalid_payment_backend_limit_range() {
4322        assert_load_settings_error(
4323            &format!(
4324                r#"
4325[info]
4326mnemonic = "{TEST_MNEMONIC}"
4327
4328[database]
4329engine = "sqlite"
4330
4331[payment_backend]
4332backend = "fakewallet"
4333min_mint = 10
4334max_mint = 1
4335"#
4336            ),
4337            "Payment backend min_mint cannot be greater than max_mint",
4338        );
4339    }
4340
4341    #[cfg(feature = "fakewallet")]
4342    #[test]
4343    fn test_load_settings_merges_partial_onchain_config_with_defaults() {
4344        let _env_lock = crate::test_utils::env_lock();
4345        clear_mintd_env();
4346
4347        let settings = load_settings_from_toml(
4348            "cdk_mintd_partial_onchain_config",
4349            &format!(
4350                r#"
4351[info]
4352mnemonic = "{TEST_MNEMONIC}"
4353
4354[database]
4355engine = "sqlite"
4356
4357[onchain]
4358onchain_backend = "fakewallet"
4359
4360[fake_wallet]
4361"#
4362            ),
4363        )
4364        .expect("partial on-chain config should use defaults");
4365
4366        let onchain = settings.onchain.expect("on-chain config should be present");
4367        assert_eq!(onchain.min_mint, 1.into());
4368        assert_eq!(onchain.max_mint, 500_000.into());
4369        assert_eq!(onchain.min_melt, 1.into());
4370        assert_eq!(onchain.max_melt, 500_000.into());
4371    }
4372
4373    #[cfg(feature = "fakewallet")]
4374    #[test]
4375    fn test_load_settings_reports_invalid_onchain_mint_limit_range() {
4376        assert_load_settings_error(
4377            &format!(
4378                r#"
4379[info]
4380mnemonic = "{TEST_MNEMONIC}"
4381
4382[database]
4383engine = "sqlite"
4384
4385[onchain]
4386onchain_backend = "fakewallet"
4387min_mint = 10
4388max_mint = 1
4389"#
4390            ),
4391            "On-chain min_mint cannot be greater than max_mint",
4392        );
4393    }
4394
4395    #[cfg(feature = "fakewallet")]
4396    #[test]
4397    fn test_load_settings_reports_invalid_onchain_melt_limit_range() {
4398        assert_load_settings_error(
4399            &format!(
4400                r#"
4401[info]
4402mnemonic = "{TEST_MNEMONIC}"
4403
4404[database]
4405engine = "sqlite"
4406
4407[onchain]
4408onchain_backend = "fakewallet"
4409min_melt = 10
4410max_melt = 1
4411"#
4412            ),
4413            "On-chain min_melt cannot be greater than max_melt",
4414        );
4415    }
4416
4417    #[cfg(all(feature = "prometheus", feature = "fakewallet"))]
4418    #[test]
4419    fn test_load_settings_reports_invalid_prometheus_address() {
4420        assert_load_settings_error(
4421            &format!(
4422                r#"
4423[info]
4424mnemonic = "{TEST_MNEMONIC}"
4425
4426[database]
4427engine = "sqlite"
4428
4429[payment_backend]
4430backend = "fakewallet"
4431
4432[prometheus]
4433enabled = true
4434address = "localhost"
4435port = 9090
4436"#
4437            ),
4438            "Invalid Prometheus address",
4439        );
4440    }
4441
4442    #[cfg(all(feature = "management-rpc", feature = "fakewallet"))]
4443    #[test]
4444    fn test_load_settings_reports_invalid_management_rpc_address() {
4445        assert_load_settings_error(
4446            &format!(
4447                r#"
4448[info]
4449mnemonic = "{TEST_MNEMONIC}"
4450
4451[database]
4452engine = "sqlite"
4453
4454[payment_backend]
4455backend = "fakewallet"
4456
4457[mint_management_rpc]
4458enabled = true
4459address = "localhost"
4460port = 8086
4461"#
4462            ),
4463            "Invalid mint management RPC address",
4464        );
4465    }
4466
4467    #[cfg(feature = "fakewallet")]
4468    #[test]
4469    fn test_load_settings_valid_config() {
4470        let _env_lock = crate::test_utils::env_lock();
4471        clear_mintd_env();
4472        load_settings_from_toml(
4473            "cdk_mintd_valid",
4474            &format!(
4475                r#"
4476[info]
4477mnemonic = "{TEST_MNEMONIC}"
4478
4479[database]
4480engine = "sqlite"
4481
4482[payment_backend]
4483backend = "fakewallet"
4484"#
4485            ),
4486        )
4487        .expect("valid config should load without error");
4488    }
4489
4490    #[cfg(feature = "fakewallet")]
4491    #[test]
4492    fn test_load_settings_valid_config_with_insecure_signatory() {
4493        let _env_lock = crate::test_utils::env_lock();
4494        clear_mintd_env();
4495        load_settings_from_toml(
4496            "cdk_mintd_valid_signatory",
4497            r#"
4498[signatory]
4499enabled = true
4500allow_insecure = true
4501
4502[database]
4503engine = "sqlite"
4504
4505[payment_backend]
4506backend = "fakewallet"
4507"#,
4508        )
4509        .expect("valid config with an insecure signatory should load without error");
4510    }
4511
4512    #[cfg(feature = "fakewallet")]
4513    #[test]
4514    fn test_load_settings_rejects_signatory_without_tls() {
4515        assert_load_settings_error(
4516            r#"
4517[signatory]
4518enabled = true
4519
4520[database]
4521engine = "sqlite"
4522
4523[payment_backend]
4524backend = "fakewallet"
4525"#,
4526            "gRPC signatory TLS is not configured",
4527        );
4528    }
4529
4530    #[cfg(feature = "fakewallet")]
4531    #[test]
4532    fn test_load_settings_rejects_empty_seed_before_mnemonic() {
4533        assert_load_settings_error(
4534            &format!(
4535                r#"
4536[info]
4537seed = ""
4538mnemonic = "{TEST_MNEMONIC}"
4539
4540[database]
4541engine = "sqlite"
4542
4543[payment_backend]
4544backend = "fakewallet"
4545"#
4546            ),
4547            "Seed in [info].seed must not be empty",
4548        );
4549    }
4550
4551    #[cfg(feature = "fakewallet")]
4552    #[test]
4553    fn test_load_settings_reports_invalid_mnemonic() {
4554        assert_load_settings_error(
4555            r#"
4556[info]
4557mnemonic = "not a valid mnemonic phrase at all"
4558
4559[database]
4560engine = "sqlite"
4561
4562[payment_backend]
4563backend = "fakewallet"
4564"#,
4565            "Invalid mnemonic",
4566        );
4567    }
4568
4569    #[cfg(feature = "fakewallet")]
4570    #[test]
4571    fn test_load_settings_reports_invalid_listen_address() {
4572        assert_load_settings_error(
4573            &format!(
4574                r#"
4575[info]
4576mnemonic = "{TEST_MNEMONIC}"
4577listen_host = "999.999.999.999"
4578
4579[database]
4580engine = "sqlite"
4581
4582[payment_backend]
4583backend = "fakewallet"
4584"#
4585            ),
4586            "Invalid mint listen address",
4587        );
4588    }
4589
4590    #[cfg(feature = "fakewallet")]
4591    #[test]
4592    fn test_load_settings_reports_missing_auth_openid_client_id() {
4593        assert_load_settings_error(
4594            &format!(
4595                r#"
4596[info]
4597mnemonic = "{TEST_MNEMONIC}"
4598
4599[database]
4600engine = "sqlite"
4601
4602[payment_backend]
4603backend = "fakewallet"
4604
4605[auth]
4606auth_enabled = true
4607openid_discovery = "https://issuer.example.com/.well-known/openid-configuration"
4608"#
4609            ),
4610            "Auth openid_client_id must be set",
4611        );
4612    }
4613
4614    #[cfg(feature = "fakewallet")]
4615    #[test]
4616    fn test_load_settings_reports_invalid_melt_limit_range() {
4617        assert_load_settings_error(
4618            &format!(
4619                r#"
4620[info]
4621mnemonic = "{TEST_MNEMONIC}"
4622
4623[database]
4624engine = "sqlite"
4625
4626[payment_backend]
4627backend = "fakewallet"
4628min_melt = 10
4629max_melt = 1
4630"#
4631            ),
4632            "Payment backend min_melt cannot be greater than max_melt",
4633        );
4634    }
4635
4636    #[cfg(feature = "fakewallet")]
4637    #[test]
4638    fn test_load_settings_reports_missing_fakewallet_supported_units() {
4639        assert_load_settings_error(
4640            &format!(
4641                r#"
4642[info]
4643mnemonic = "{TEST_MNEMONIC}"
4644
4645[database]
4646engine = "sqlite"
4647
4648[payment_backend]
4649backend = "fakewallet"
4650
4651[fake_wallet]
4652supported_units = []
4653"#
4654            ),
4655            "Fake wallet supported_units must contain at least one unit",
4656        );
4657    }
4658
4659    #[cfg(feature = "lnd")]
4660    #[test]
4661    fn test_load_settings_reports_missing_lnd_cert_file() {
4662        assert_load_settings_error(
4663            &format!(
4664                r#"
4665[info]
4666mnemonic = "{TEST_MNEMONIC}"
4667
4668[database]
4669engine = "sqlite"
4670
4671[payment_backend]
4672backend = "lnd"
4673
4674[lnd]
4675address = "127.0.0.1:10009"
4676"#
4677            ),
4678            "LND cert_file must be set",
4679        );
4680    }
4681
4682    #[cfg(feature = "lnd")]
4683    #[test]
4684    fn test_load_settings_reports_missing_lnd_macaroon_file() {
4685        assert_load_settings_error(
4686            &format!(
4687                r#"
4688[info]
4689mnemonic = "{TEST_MNEMONIC}"
4690
4691[database]
4692engine = "sqlite"
4693
4694[payment_backend]
4695backend = "lnd"
4696
4697[lnd]
4698address = "127.0.0.1:10009"
4699cert_file = "/path/to/tls.cert"
4700"#
4701            ),
4702            "LND macaroon_file must be set",
4703        );
4704    }
4705
4706    #[cfg(feature = "grpc-processor")]
4707    #[test]
4708    fn test_load_settings_reports_missing_grpc_processor_address() {
4709        assert_load_settings_error(
4710            &format!(
4711                r#"
4712[info]
4713mnemonic = "{TEST_MNEMONIC}"
4714
4715[database]
4716engine = "sqlite"
4717
4718[payment_backend]
4719backend = "grpcprocessor"
4720
4721[grpc_processor]
4722supported_units = ["sat"]
4723address = ""
4724"#
4725            ),
4726            "gRPC payment processor address must be set",
4727        );
4728    }
4729
4730    #[cfg(feature = "fakewallet")]
4731    #[test]
4732    fn test_load_settings_reports_missing_auth_postgres_url() {
4733        assert_load_settings_error(
4734            &format!(
4735                r#"
4736[info]
4737mnemonic = "{TEST_MNEMONIC}"
4738
4739[database]
4740engine = "postgres"
4741
4742[database.postgres]
4743url = "postgresql://user:password@localhost:5432/cdk_mint"
4744
4745[payment_backend]
4746backend = "fakewallet"
4747
4748[auth]
4749auth_enabled = true
4750openid_discovery = "https://issuer.example.com/.well-known/openid-configuration"
4751openid_client_id = "mintd"
4752"#
4753            ),
4754            "Auth database PostgreSQL URL is required",
4755        );
4756    }
4757
4758    fn load_settings_with_env(
4759        name: &str,
4760        config_content: &str,
4761        setup_env: impl FnOnce(),
4762    ) -> Result<config::Settings> {
4763        use std::fs;
4764
4765        let _env_lock = crate::test_utils::env_lock();
4766        clear_mintd_env();
4767
4768        let temp_dir = crate::test_utils::unique_temp_path(name);
4769        let _ = fs::remove_dir_all(&temp_dir);
4770        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
4771        let config_path = temp_dir.join("config.toml");
4772        fs::write(&config_path, config_content).expect("Failed to write config file");
4773
4774        setup_env();
4775
4776        let result = load_settings(&temp_dir, Some(config_path));
4777        let _ = fs::remove_dir_all(&temp_dir);
4778        clear_mintd_env();
4779        result
4780    }
4781
4782    #[cfg(feature = "fakewallet")]
4783    #[test]
4784    fn env_only_auth_preserves_protected_endpoint_defaults() {
4785        let settings = load_settings_with_env(
4786            "cdk_mintd_env_auth_defaults",
4787            &format!(
4788                r#"
4789[info]
4790mnemonic = "{TEST_MNEMONIC}"
4791
4792[database]
4793engine = "sqlite"
4794
4795[payment_backend]
4796backend = "fakewallet"
4797"#
4798            ),
4799            || {
4800                std::env::set_var("CDK_MINTD_AUTH_ENABLED", "true");
4801                std::env::set_var(
4802                    "CDK_MINTD_AUTH_OPENID_DISCOVERY",
4803                    "https://issuer.example.com/.well-known/openid-configuration",
4804                );
4805                std::env::set_var("CDK_MINTD_AUTH_OPENID_CLIENT_ID", "mintd");
4806            },
4807        )
4808        .expect("environment-only auth configuration should load");
4809
4810        let auth = settings.auth.expect("auth should be enabled");
4811        assert_eq!(auth.mint, config::AuthType::Blind);
4812        assert_eq!(auth.swap, config::AuthType::Blind);
4813        assert_eq!(auth.restore, config::AuthType::Blind);
4814        assert_eq!(auth.websocket_auth, config::AuthType::Blind);
4815    }
4816
4817    #[cfg(feature = "lnd")]
4818    #[test]
4819    fn invalid_lnd_fee_percent_from_env_is_rejected() {
4820        for (name, fee_percent) in [
4821            ("nan", "NaN"),
4822            ("negative", "-1"),
4823            ("not-a-number", "invalid"),
4824        ] {
4825            let error = load_settings_with_env(
4826                &format!("cdk_mintd_lnd_fee_percent_{name}"),
4827                &format!(
4828                    r#"
4829[info]
4830mnemonic = "{TEST_MNEMONIC}"
4831
4832[database]
4833engine = "sqlite"
4834
4835[payment_backend]
4836backend = "lnd"
4837"#
4838                ),
4839                || {
4840                    std::env::set_var("CDK_MINTD_LND_ADDRESS", "https://127.0.0.1:10009");
4841                    std::env::set_var("CDK_MINTD_LND_CERT_FILE", "/certs/tls.cert");
4842                    std::env::set_var("CDK_MINTD_LND_MACAROON_FILE", "/data/admin.macaroon");
4843                    std::env::set_var("CDK_MINTD_LND_FEE_PERCENT", fee_percent);
4844                },
4845            )
4846            .expect_err("invalid LND fee percentage should fail startup validation");
4847
4848            assert!(
4849                error.to_string().contains("CDK_MINTD_LND_FEE_PERCENT"),
4850                "unexpected error for {fee_percent}: {error}"
4851            );
4852        }
4853    }
4854
4855    #[cfg(feature = "fakewallet")]
4856    #[test]
4857    fn test_env_var_provides_mnemonic_when_toml_has_none() {
4858        let settings = load_settings_with_env(
4859            "cdk_mintd_env_mnemonic",
4860            r#"
4861[database]
4862engine = "sqlite"
4863
4864[payment_backend]
4865backend = "fakewallet"
4866"#,
4867            || std::env::set_var("CDK_MINTD_MNEMONIC", TEST_MNEMONIC),
4868        )
4869        .expect("valid config with env mnemonic should load");
4870
4871        let mnemonic = settings
4872            .info
4873            .mnemonic
4874            .expect("mnemonic should be set from env");
4875        assert_eq!(mnemonic, TEST_MNEMONIC);
4876    }
4877
4878    #[cfg(feature = "fakewallet")]
4879    #[test]
4880    fn test_env_var_provides_seed_when_toml_has_none() {
4881        let seed = "a".repeat(32);
4882        let settings = load_settings_with_env(
4883            "cdk_mintd_env_seed",
4884            r#"
4885[database]
4886engine = "sqlite"
4887
4888[payment_backend]
4889backend = "fakewallet"
4890"#,
4891            || std::env::set_var("CDK_MINTD_SEED", &seed),
4892        )
4893        .expect("valid config with env seed should load");
4894
4895        let loaded_seed = settings.info.seed.expect("seed should be set from env");
4896        assert_eq!(loaded_seed, seed);
4897    }
4898
4899    #[cfg(feature = "fakewallet")]
4900    #[test]
4901    fn test_env_var_provides_payment_backend_when_toml_has_none() {
4902        let settings = load_settings_with_env(
4903            "cdk_mintd_env_payment_backend_only",
4904            &format!(
4905                r#"
4906[info]
4907mnemonic = "{TEST_MNEMONIC}"
4908
4909[database]
4910engine = "sqlite"
4911"#
4912            ),
4913            || {
4914                std::env::set_var("CDK_MINTD_PAYMENT_BACKEND", "fakewallet");
4915                std::env::set_var("CDK_MINTD_PAYMENT_BACKEND_MIN_MINT", "10");
4916            },
4917        )
4918        .expect("env-only payment backend config should load");
4919
4920        assert_eq!(settings.payment_backend.len(), 1);
4921        assert_eq!(
4922            settings.payment_backend[0].backend,
4923            config::PaymentBackendType::FakeWallet
4924        );
4925    }
4926
4927    #[cfg(feature = "fakewallet")]
4928    #[test]
4929    fn test_env_var_overrides_toml_listen_host() {
4930        let settings = load_settings_with_env(
4931            "cdk_mintd_env_override_listen",
4932            &format!(
4933                r#"
4934[info]
4935mnemonic = "{TEST_MNEMONIC}"
4936listen_host = "127.0.0.1"
4937
4938[database]
4939engine = "sqlite"
4940
4941[payment_backend]
4942backend = "fakewallet"
4943"#
4944            ),
4945            || std::env::set_var("CDK_MINTD_LISTEN_HOST", "0.0.0.0"),
4946        )
4947        .expect("config with env override should load");
4948
4949        assert_eq!(settings.info.listen_host, "0.0.0.0");
4950    }
4951
4952    #[cfg(feature = "fakewallet")]
4953    #[test]
4954    fn test_env_var_overrides_toml_listen_port() {
4955        let settings = load_settings_with_env(
4956            "cdk_mintd_env_override_port",
4957            &format!(
4958                r#"
4959[info]
4960mnemonic = "{TEST_MNEMONIC}"
4961listen_port = 8080
4962
4963[database]
4964engine = "sqlite"
4965
4966[payment_backend]
4967backend = "fakewallet"
4968"#
4969            ),
4970            || std::env::set_var("CDK_MINTD_LISTEN_PORT", "9090"),
4971        )
4972        .expect("config with env port override should load");
4973
4974        assert_eq!(settings.info.listen_port, 9090);
4975    }
4976}