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