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