Skip to main content

cdk_mintd/
lib.rs

1#![allow(missing_docs)]
2//! Cdk mintd lib
3
4// std
5use std::collections::HashMap;
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, Result};
14use axum::Router;
15use bip39::Mnemonic;
16use cdk::cdk_database::{self, KVStore, MintDatabase, MintKeysDatabase};
17use cdk::mint::{Mint, MintBuilder, MintMeltLimits};
18use cdk::nuts::nut00::KnownMethod;
19#[cfg(any(
20    feature = "cln",
21    feature = "lnbits",
22    feature = "lnd",
23    feature = "ldk-node",
24    feature = "fakewallet",
25    feature = "grpc-processor"
26))]
27use cdk::nuts::nut17::SupportedMethods;
28use cdk::nuts::nut19::{CachedEndpoint, Method as NUT19Method, Path as NUT19Path};
29#[cfg(any(
30    feature = "cln",
31    feature = "lnbits",
32    feature = "lnd",
33    feature = "ldk-node"
34))]
35use cdk::nuts::CurrencyUnit;
36use cdk::nuts::{
37    AuthRequired, ContactInfo, Method, MintVersion, PaymentMethod, ProtectedEndpoint, RoutePath,
38};
39use cdk_axum::cache::HttpCache;
40use cdk_common::common::QuoteTTL;
41use cdk_common::database::DynMintDatabase;
42// internal crate modules
43#[cfg(feature = "prometheus")]
44use cdk_common::payment::MetricsMintPayment;
45use cdk_common::payment::MintPayment;
46#[cfg(feature = "postgres")]
47use cdk_postgres::MintPgAuthDatabase;
48#[cfg(feature = "postgres")]
49use cdk_postgres::MintPgDatabase;
50#[cfg(feature = "sqlite")]
51use cdk_sqlite::mint::MintSqliteAuthDatabase;
52#[cfg(feature = "sqlite")]
53use cdk_sqlite::MintSqliteDatabase;
54use cli::CLIArgs;
55use config::{AuthType, DatabaseEngine, LnBackend};
56use env_vars::ENV_WORK_DIR;
57use setup::LnBackendSetup;
58use tower::ServiceBuilder;
59use tower_http::compression::CompressionLayer;
60use tower_http::decompression::RequestDecompressionLayer;
61use tower_http::trace::TraceLayer;
62use tracing_appender::{non_blocking, rolling};
63use tracing_subscriber::fmt::writer::MakeWriterExt;
64use tracing_subscriber::EnvFilter;
65#[cfg(feature = "swagger")]
66use utoipa::OpenApi;
67
68pub mod cli;
69pub mod config;
70pub mod env_vars;
71pub mod setup;
72
73const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
74
75#[cfg(feature = "cln")]
76fn expand_path(path: &str) -> Option<PathBuf> {
77    if path.starts_with('~') {
78        if let Some(home_dir) = home::home_dir().as_mut() {
79            let remainder = &path[2..];
80            home_dir.push(remainder);
81            let expanded_path = home_dir;
82            Some(expanded_path.clone())
83        } else {
84            None
85        }
86    } else {
87        Some(PathBuf::from(path))
88    }
89}
90
91/// Performs the initial setup for the application, including configuring tracing,
92/// parsing CLI arguments, setting up the working directory, loading settings,
93/// and initializing the database connection.
94async fn initial_setup(
95    work_dir: &Path,
96    settings: &config::Settings,
97    db_password: Option<String>,
98) -> Result<(
99    DynMintDatabase,
100    Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
101    Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync>,
102)> {
103    tracing::info!("Initializing database...");
104    let (localstore, keystore, kv) = setup_database(settings, work_dir, db_password).await?;
105    tracing::info!("Database initialized successfully");
106    Ok((localstore, keystore, kv))
107}
108
109/// Sets up and initializes a tracing subscriber with custom log filtering.
110/// Logs can be configured to output to stdout only, file only, or both.
111/// Returns a guard that must be kept alive and properly dropped on shutdown.
112pub fn setup_tracing(
113    work_dir: &Path,
114    logging_config: &config::LoggingConfig,
115) -> Result<Option<tracing_appender::non_blocking::WorkerGuard>> {
116    let default_filter = "debug";
117    let hyper_filter = "hyper=warn,rustls=warn,reqwest=warn";
118    let h2_filter = "h2=warn";
119    let tower_filter = "tower=warn";
120    let tower_http = "tower_http=warn";
121    let rustls = "rustls=warn";
122    let tungstenite = "tungstenite=warn";
123    let tokio_postgres = "tokio_postgres=warn";
124
125    let env_filter = EnvFilter::new(format!(
126        "{default_filter},{hyper_filter},{h2_filter},{tower_filter},{tower_http},{rustls},{tungstenite},{tokio_postgres}"
127    ));
128
129    use config::LoggingOutput;
130    match logging_config.output {
131        LoggingOutput::Stderr => {
132            // Console output only (stderr)
133            let console_level = logging_config
134                .console_level
135                .as_deref()
136                .unwrap_or("info")
137                .parse::<tracing::Level>()
138                .unwrap_or(tracing::Level::INFO);
139
140            let stderr = std::io::stderr.with_max_level(console_level);
141
142            tracing_subscriber::fmt()
143                .with_env_filter(env_filter)
144                .with_ansi(false)
145                .with_writer(stderr)
146                .init();
147
148            tracing::info!("Logging initialized: console only ({}+)", console_level);
149            Ok(None)
150        }
151        LoggingOutput::File => {
152            // File output only
153            let file_level = logging_config
154                .file_level
155                .as_deref()
156                .unwrap_or("debug")
157                .parse::<tracing::Level>()
158                .unwrap_or(tracing::Level::DEBUG);
159
160            // Create logs directory in work_dir if it doesn't exist
161            let logs_dir = work_dir.join("logs");
162            std::fs::create_dir_all(&logs_dir)?;
163
164            // Set up file appender with daily rotation
165            let file_appender = rolling::daily(&logs_dir, "cdk-mintd.log");
166            let (non_blocking_appender, guard) = non_blocking(file_appender);
167
168            let file_writer = non_blocking_appender.with_max_level(file_level);
169
170            tracing_subscriber::fmt()
171                .with_env_filter(env_filter)
172                .with_ansi(false)
173                .with_writer(file_writer)
174                .init();
175
176            tracing::info!(
177                "Logging initialized: file only at {}/cdk-mintd.log ({}+)",
178                logs_dir.display(),
179                file_level
180            );
181            Ok(Some(guard))
182        }
183        LoggingOutput::Both => {
184            // Both console and file output (stderr + file)
185            let console_level = logging_config
186                .console_level
187                .as_deref()
188                .unwrap_or("info")
189                .parse::<tracing::Level>()
190                .unwrap_or(tracing::Level::INFO);
191            let file_level = logging_config
192                .file_level
193                .as_deref()
194                .unwrap_or("debug")
195                .parse::<tracing::Level>()
196                .unwrap_or(tracing::Level::DEBUG);
197
198            // Create logs directory in work_dir if it doesn't exist
199            let logs_dir = work_dir.join("logs");
200            std::fs::create_dir_all(&logs_dir)?;
201
202            // Set up file appender with daily rotation
203            let file_appender = rolling::daily(&logs_dir, "cdk-mintd.log");
204            let (non_blocking_appender, guard) = non_blocking(file_appender);
205
206            // Combine console output (stderr) and file output
207            let stderr = std::io::stderr.with_max_level(console_level);
208            let file_writer = non_blocking_appender.with_max_level(file_level);
209
210            tracing_subscriber::fmt()
211                .with_env_filter(env_filter)
212                .with_ansi(false)
213                .with_writer(stderr.and(file_writer))
214                .init();
215
216            tracing::info!(
217                "Logging initialized: console ({}+) and file at {}/cdk-mintd.log ({}+)",
218                console_level,
219                logs_dir.display(),
220                file_level
221            );
222            Ok(Some(guard))
223        }
224    }
225}
226
227/// Retrieves the work directory based on command-line arguments, environment variables, or system defaults.
228pub async fn get_work_directory(args: &CLIArgs) -> Result<PathBuf> {
229    let work_dir = if let Some(work_dir) = &args.work_dir {
230        tracing::info!("Using work dir from cmd arg");
231        work_dir.clone()
232    } else if let Ok(env_work_dir) = env::var(ENV_WORK_DIR) {
233        tracing::info!("Using work dir from env var");
234        env_work_dir.into()
235    } else {
236        work_dir()?
237    };
238    tracing::info!("Using work dir: {}", work_dir.display());
239    Ok(work_dir)
240}
241
242/// Loads the application settings based on a configuration file and environment variables.
243pub fn load_settings(work_dir: &Path, config_path: Option<PathBuf>) -> Result<config::Settings> {
244    // get config file name from args
245    let config_file_arg = match config_path {
246        Some(c) => c,
247        None => work_dir.join("config.toml"),
248    };
249
250    let mut settings = if config_file_arg.exists() {
251        config::Settings::new(Some(config_file_arg))
252    } else {
253        tracing::info!("Config file does not exist. Attempting to read env vars");
254        config::Settings::default()
255    };
256
257    // This check for any settings defined in ENV VARs
258    // ENV VARS will take **priority** over those in the config
259    settings.from_env()
260}
261
262async fn setup_database(
263    settings: &config::Settings,
264    _work_dir: &Path,
265    _db_password: Option<String>,
266) -> Result<(
267    DynMintDatabase,
268    Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
269    Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync>,
270)> {
271    tracing::info!("Using database engine: {:?}", settings.database.engine);
272    match settings.database.engine {
273        #[cfg(feature = "sqlite")]
274        DatabaseEngine::Sqlite => {
275            let db = setup_sqlite_database(_work_dir, _db_password).await?;
276            let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> = db.clone();
277            let kv: Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync> = db.clone();
278            let keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync> = db;
279            Ok((localstore, keystore, kv))
280        }
281        #[cfg(feature = "postgres")]
282        DatabaseEngine::Postgres => {
283            // Get the PostgreSQL configuration, ensuring it exists
284            let pg_config = settings.database.postgres.as_ref().ok_or_else(|| {
285                anyhow!("PostgreSQL configuration is required when using PostgreSQL engine")
286            })?;
287
288            if pg_config.url.is_empty() {
289                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");
290            }
291
292            #[cfg(feature = "postgres")]
293            let pg_db = Arc::new(MintPgDatabase::new(pg_config.url.as_str()).await?);
294            tracing::info!("PostgreSQL database connection established");
295            #[cfg(feature = "postgres")]
296            let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> =
297                pg_db.clone();
298            #[cfg(feature = "postgres")]
299            let kv: Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync> = pg_db.clone();
300            #[cfg(feature = "postgres")]
301            let keystore: Arc<
302                dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync,
303            > = pg_db;
304            #[cfg(feature = "postgres")]
305            return Ok((localstore, keystore, kv));
306
307            #[cfg(not(feature = "postgres"))]
308            bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
309        }
310        #[cfg(not(feature = "sqlite"))]
311        DatabaseEngine::Sqlite => {
312            bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
313        }
314        #[cfg(not(feature = "postgres"))]
315        DatabaseEngine::Postgres => {
316            bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
317        }
318    }
319}
320
321#[cfg(feature = "sqlite")]
322async fn setup_sqlite_database(
323    work_dir: &Path,
324    _password: Option<String>,
325) -> Result<Arc<MintSqliteDatabase>> {
326    let sql_db_path = work_dir.join("cdk-mintd.sqlite");
327    tracing::info!("SQLite database path: {}", sql_db_path.display());
328
329    #[cfg(not(feature = "sqlcipher"))]
330    let db = MintSqliteDatabase::new(&sql_db_path).await?;
331    #[cfg(feature = "sqlcipher")]
332    let db = {
333        // Get password from command line arguments for sqlcipher
334        let password = _password
335            .ok_or_else(|| anyhow!("Password required when sqlcipher feature is enabled"))?;
336        tracing::info!("Using SQLCipher encryption for SQLite database");
337        MintSqliteDatabase::new((sql_db_path, password)).await?
338    };
339
340    tracing::info!("SQLite database initialized successfully");
341    Ok(Arc::new(db))
342}
343
344/**
345 * Configures a `MintBuilder` instance with provided settings and initializes
346 * routers for Lightning Network backends.
347 */
348async fn configure_mint_builder(
349    settings: &config::Settings,
350    mint_builder: MintBuilder,
351    runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
352    work_dir: &Path,
353    kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
354) -> Result<MintBuilder> {
355    // Configure basic mint information
356    let mint_builder = configure_basic_info(settings, mint_builder);
357
358    // Configure lightning backend
359    let mint_builder =
360        configure_lightning_backend(settings, mint_builder, runtime, work_dir, kv_store).await?;
361
362    // Extract configured payment methods from mint_builder
363    let mint_info = mint_builder.current_mint_info();
364    let payment_methods: Vec<String> = mint_info
365        .nuts
366        .nut04
367        .methods
368        .iter()
369        .map(|m| m.method.to_string())
370        .collect();
371
372    // Configure caching with payment methods
373    let mint_builder = configure_cache(settings, mint_builder, &payment_methods);
374
375    // Configure transaction limits
376    let mint_builder =
377        mint_builder.with_limits(settings.limits.max_inputs, settings.limits.max_outputs);
378
379    Ok(mint_builder)
380}
381
382/// Configures basic mint information (name, contact info, descriptions, etc.)
383fn configure_basic_info(settings: &config::Settings, mint_builder: MintBuilder) -> MintBuilder {
384    // Add contact information
385    let mut contacts = Vec::new();
386    if let Some(nostr_key) = &settings.mint_info.contact_nostr_public_key {
387        if !nostr_key.is_empty() {
388            contacts.push(ContactInfo::new("nostr".to_string(), nostr_key.to_string()));
389        }
390    }
391    if let Some(email) = &settings.mint_info.contact_email {
392        if !email.is_empty() {
393            contacts.push(ContactInfo::new("email".to_string(), email.to_string()));
394        }
395    }
396
397    // Add version information
398    let mint_version = MintVersion::new(
399        "cdk-mintd".to_string(),
400        CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
401    );
402
403    // Configure mint builder with basic info
404    let mut builder = mint_builder.with_version(mint_version);
405
406    // Only set name if it's not empty
407    if !settings.mint_info.name.is_empty() {
408        builder = builder.with_name(settings.mint_info.name.clone());
409    }
410
411    // Only set description if it's not empty
412    if !settings.mint_info.description.is_empty() {
413        builder = builder.with_description(settings.mint_info.description.clone());
414    }
415
416    // Add optional information
417    if let Some(long_description) = &settings.mint_info.description_long {
418        if !long_description.is_empty() {
419            builder = builder.with_long_description(long_description.to_string());
420        }
421    }
422
423    for contact in contacts {
424        builder = builder.with_contact_info(contact);
425    }
426
427    if let Some(pubkey) = settings.mint_info.pubkey {
428        builder = builder.with_pubkey(pubkey);
429    }
430
431    if let Some(icon_url) = &settings.mint_info.icon_url {
432        if !icon_url.is_empty() {
433            builder = builder.with_icon_url(icon_url.to_string());
434        }
435    }
436
437    if let Some(motd) = &settings.mint_info.motd {
438        if !motd.is_empty() {
439            builder = builder.with_motd(motd.to_string());
440        }
441    }
442
443    if let Some(tos_url) = &settings.mint_info.tos_url {
444        if !tos_url.is_empty() {
445            builder = builder.with_tos_url(tos_url.to_string());
446        }
447    }
448
449    builder = builder.with_keyset_v2(settings.info.use_keyset_v2);
450
451    builder
452}
453/// Configures Lightning Network backend based on the specified backend type
454async fn configure_lightning_backend(
455    settings: &config::Settings,
456    mut mint_builder: MintBuilder,
457    _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
458    work_dir: &Path,
459    _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
460) -> Result<MintBuilder> {
461    let mint_melt_limits = MintMeltLimits {
462        mint_min: settings.ln.min_mint,
463        mint_max: settings.ln.max_mint,
464        melt_min: settings.ln.min_melt,
465        melt_max: settings.ln.max_melt,
466    };
467
468    tracing::debug!("Ln backend: {:?}", settings.ln.ln_backend);
469
470    match settings.ln.ln_backend {
471        #[cfg(feature = "cln")]
472        LnBackend::Cln => {
473            let cln_settings = settings
474                .cln
475                .clone()
476                .expect("Config checked at load that cln is some");
477            let cln = cln_settings
478                .setup(settings, CurrencyUnit::Msat, None, work_dir, _kv_store)
479                .await?;
480            #[cfg(feature = "prometheus")]
481            let cln = MetricsMintPayment::new(cln);
482
483            mint_builder = configure_backend_for_unit(
484                settings,
485                mint_builder,
486                CurrencyUnit::Sat,
487                mint_melt_limits,
488                Arc::new(cln),
489            )
490            .await?;
491        }
492        #[cfg(feature = "lnbits")]
493        LnBackend::LNbits => {
494            let lnbits_settings = settings.clone().lnbits.expect("Checked on config load");
495            let lnbits = lnbits_settings
496                .setup(settings, CurrencyUnit::Sat, None, work_dir, None)
497                .await?;
498            #[cfg(feature = "prometheus")]
499            let lnbits = MetricsMintPayment::new(lnbits);
500
501            mint_builder = configure_backend_for_unit(
502                settings,
503                mint_builder,
504                CurrencyUnit::Sat,
505                mint_melt_limits,
506                Arc::new(lnbits),
507            )
508            .await?;
509        }
510        #[cfg(feature = "lnd")]
511        LnBackend::Lnd => {
512            let lnd_settings = settings.clone().lnd.expect("Checked at config load");
513            let lnd = lnd_settings
514                .setup(settings, CurrencyUnit::Msat, None, work_dir, _kv_store)
515                .await?;
516            #[cfg(feature = "prometheus")]
517            let lnd = MetricsMintPayment::new(lnd);
518
519            mint_builder = configure_backend_for_unit(
520                settings,
521                mint_builder,
522                CurrencyUnit::Sat,
523                mint_melt_limits,
524                Arc::new(lnd),
525            )
526            .await?;
527        }
528        #[cfg(feature = "fakewallet")]
529        LnBackend::FakeWallet => {
530            let fake_wallet = settings.clone().fake_wallet.expect("Fake wallet defined");
531            tracing::info!("Using fake wallet: {:?}", fake_wallet);
532
533            for unit in fake_wallet.clone().supported_units {
534                let fake = fake_wallet
535                    .setup(settings, unit.clone(), None, work_dir, _kv_store.clone())
536                    .await?;
537                #[cfg(feature = "prometheus")]
538                let fake = MetricsMintPayment::new(fake);
539
540                mint_builder = configure_backend_for_unit(
541                    settings,
542                    mint_builder,
543                    unit.clone(),
544                    mint_melt_limits,
545                    Arc::new(fake),
546                )
547                .await?;
548            }
549        }
550        #[cfg(feature = "grpc-processor")]
551        LnBackend::GrpcProcessor => {
552            let grpc_processor = settings
553                .clone()
554                .grpc_processor
555                .expect("grpc processor config defined");
556
557            tracing::info!(
558                "Attempting to start with gRPC payment processor at {}:{}.",
559                grpc_processor.addr,
560                grpc_processor.port
561            );
562
563            for unit in grpc_processor.clone().supported_units {
564                tracing::debug!("Adding unit: {:?}", unit);
565                let processor = grpc_processor
566                    .setup(settings, unit.clone(), None, work_dir, None)
567                    .await?;
568                #[cfg(feature = "prometheus")]
569                let processor = MetricsMintPayment::new(processor);
570
571                mint_builder = configure_backend_for_unit(
572                    settings,
573                    mint_builder,
574                    unit.clone(),
575                    mint_melt_limits,
576                    Arc::new(processor),
577                )
578                .await?;
579            }
580        }
581        #[cfg(feature = "ldk-node")]
582        LnBackend::LdkNode => {
583            let ldk_node_settings = settings.clone().ldk_node.expect("Checked at config load");
584            tracing::info!("Using LDK Node backend: {:?}", ldk_node_settings);
585
586            let ldk_node = ldk_node_settings
587                .setup(settings, CurrencyUnit::Sat, _runtime, work_dir, None)
588                .await?;
589
590            mint_builder = configure_backend_for_unit(
591                settings,
592                mint_builder,
593                CurrencyUnit::Sat,
594                mint_melt_limits,
595                Arc::new(ldk_node),
596            )
597            .await?;
598        }
599        LnBackend::None => {
600            tracing::error!(
601                "Payment backend was not set or feature disabled. {:?}",
602                settings.ln.ln_backend
603            );
604            bail!("Lightning backend must be configured");
605        }
606    };
607
608    Ok(mint_builder)
609}
610
611/// Helper function to configure a mint builder with a lightning backend for a specific currency unit
612async fn configure_backend_for_unit(
613    settings: &config::Settings,
614    mut mint_builder: MintBuilder,
615    unit: cdk::nuts::CurrencyUnit,
616    mint_melt_limits: MintMeltLimits,
617    backend: Arc<dyn MintPayment<Err = cdk_common::payment::Error> + Send + Sync>,
618) -> Result<MintBuilder> {
619    let payment_settings = backend.get_settings().await?;
620
621    let mut methods = Vec::new();
622
623    // Add bolt11 if supported by payment processor
624    if payment_settings.bolt11.is_some() {
625        methods.push(PaymentMethod::Known(KnownMethod::Bolt11));
626    }
627
628    // Add bolt12 if supported by payment processor
629    if payment_settings.bolt12.is_some() {
630        methods.push(PaymentMethod::Known(KnownMethod::Bolt12));
631    }
632
633    // Add custom methods from payment settings
634    for method_name in payment_settings.custom.keys() {
635        methods.push(PaymentMethod::from(method_name.as_str()));
636    }
637
638    // Add all supported payment methods to the mint builder
639    for method in &methods {
640        mint_builder
641            .add_payment_processor(
642                unit.clone(),
643                method.clone(),
644                mint_melt_limits,
645                backend.clone(),
646            )
647            .await?;
648    }
649
650    // Configure NUT17 (WebSocket support) for all payment methods
651    for method in &methods {
652        let method_str = method.to_string();
653        let nut17_supported = match method_str.as_str() {
654            "bolt11" => SupportedMethods::default_bolt11(unit.clone()),
655            "bolt12" => SupportedMethods::default_bolt12(unit.clone()),
656            _ => SupportedMethods::default_custom(method.clone(), unit.clone()),
657        };
658        mint_builder = mint_builder.with_supported_websockets(nut17_supported);
659    }
660
661    if let Some(input_fee) = settings.info.input_fee_ppk {
662        mint_builder.set_unit_fee(&unit, input_fee)?;
663    }
664
665    Ok(mint_builder)
666}
667
668/// Configures cache settings with support for custom payment methods
669fn configure_cache(
670    settings: &config::Settings,
671    mint_builder: MintBuilder,
672    payment_methods: &[String],
673) -> MintBuilder {
674    let mut cached_endpoints = vec![
675        // Always include swap endpoint
676        CachedEndpoint::new(NUT19Method::Post, NUT19Path::Swap),
677    ];
678
679    // Add cache endpoints for each configured payment method
680    for method in payment_methods {
681        // All payment methods (including bolt11, bolt12) use custom paths now
682        cached_endpoints.push(CachedEndpoint::new(
683            NUT19Method::Post,
684            NUT19Path::custom_mint(method),
685        ));
686        cached_endpoints.push(CachedEndpoint::new(
687            NUT19Method::Post,
688            NUT19Path::custom_melt(method),
689        ));
690    }
691
692    let cache: HttpCache = settings.info.http_cache.clone().into();
693    mint_builder.with_cache(Some(cache.ttl.as_secs()), cached_endpoints)
694}
695
696async fn setup_authentication(
697    settings: &config::Settings,
698    _work_dir: &Path,
699    mut mint_builder: MintBuilder,
700    _password: Option<String>,
701) -> Result<(
702    MintBuilder,
703    Option<cdk_common::database::DynMintAuthDatabase>,
704)> {
705    if let Some(auth_settings) = settings.auth.clone() {
706        use cdk_common::database::DynMintAuthDatabase;
707
708        tracing::info!("Auth settings are defined. {:?}", auth_settings);
709        let auth_localstore: DynMintAuthDatabase = match settings.database.engine {
710            #[cfg(feature = "sqlite")]
711            DatabaseEngine::Sqlite => {
712                #[cfg(feature = "sqlite")]
713                {
714                    let sql_db_path = _work_dir.join("cdk-mintd-auth.sqlite");
715                    #[cfg(not(feature = "sqlcipher"))]
716                    let sqlite_db = MintSqliteAuthDatabase::new(&sql_db_path).await?;
717                    #[cfg(feature = "sqlcipher")]
718                    let sqlite_db = {
719                        // Get password from command line arguments for sqlcipher
720                        let password = _password.clone().ok_or_else(|| {
721                            anyhow!("Password required when sqlcipher feature is enabled")
722                        })?;
723                        MintSqliteAuthDatabase::new((sql_db_path, password)).await?
724                    };
725
726                    Arc::new(sqlite_db)
727                }
728                #[cfg(not(feature = "sqlite"))]
729                {
730                    bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
731                }
732            }
733            #[cfg(feature = "postgres")]
734            DatabaseEngine::Postgres => {
735                #[cfg(feature = "postgres")]
736                {
737                    // Require dedicated auth database configuration - no fallback to main database
738                    let auth_db_config = settings.auth_database.as_ref().ok_or_else(|| {
739                        anyhow!("Auth database configuration is required when using PostgreSQL with authentication. Set [auth_database] section in config file or CDK_MINTD_AUTH_POSTGRES_URL environment variable")
740                    })?;
741
742                    let auth_pg_config = auth_db_config.postgres.as_ref().ok_or_else(|| {
743                        anyhow!("PostgreSQL auth database configuration is required when using PostgreSQL with authentication. Set [auth_database.postgres] section in config file or CDK_MINTD_AUTH_POSTGRES_URL environment variable")
744                    })?;
745
746                    if auth_pg_config.url.is_empty() {
747                        bail!("Auth database PostgreSQL URL is required and cannot be empty. Set it in config file [auth_database.postgres] section or via CDK_MINTD_AUTH_POSTGRES_URL environment variable");
748                    }
749
750                    Arc::new(MintPgAuthDatabase::new(auth_pg_config.url.as_str()).await?)
751                }
752                #[cfg(not(feature = "postgres"))]
753                {
754                    bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
755                }
756            }
757            #[cfg(not(feature = "sqlite"))]
758            DatabaseEngine::Sqlite => {
759                bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
760            }
761            #[cfg(not(feature = "postgres"))]
762            DatabaseEngine::Postgres => {
763                bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
764            }
765        };
766
767        let mut protected_endpoints = HashMap::new();
768        let mut blind_auth_endpoints = vec![];
769        let mut clear_auth_endpoints = vec![];
770        let mut unprotected_endpoints = vec![];
771
772        let mint_blind_auth_endpoint =
773            ProtectedEndpoint::new(Method::Post, RoutePath::MintBlindAuth);
774
775        protected_endpoints.insert(mint_blind_auth_endpoint.clone(), AuthRequired::Clear);
776
777        clear_auth_endpoints.push(mint_blind_auth_endpoint);
778
779        // Helper function to add endpoint based on auth type
780        let mut add_endpoint = |endpoint: ProtectedEndpoint, auth_type: &AuthType| {
781            match auth_type {
782                AuthType::Blind => {
783                    protected_endpoints.insert(endpoint.clone(), AuthRequired::Blind);
784                    blind_auth_endpoints.push(endpoint);
785                }
786                AuthType::Clear => {
787                    protected_endpoints.insert(endpoint.clone(), AuthRequired::Clear);
788                    clear_auth_endpoints.push(endpoint);
789                }
790                AuthType::None => {
791                    unprotected_endpoints.push(endpoint);
792                }
793            };
794        };
795
796        // Payment method endpoints (bolt11, bolt12, custom) will be added dynamically
797        // after the mint is built and we can query the payment processors for their
798        // supported methods. See the start_services_with_shutdown function where we
799        // add auth endpoints for all configured payment methods.
800
801        // Swap endpoint
802        {
803            let swap_protected_endpoint = ProtectedEndpoint::new(Method::Post, RoutePath::Swap);
804            add_endpoint(swap_protected_endpoint, &auth_settings.swap);
805        }
806
807        // Restore endpoint
808        {
809            let restore_protected_endpoint =
810                ProtectedEndpoint::new(Method::Post, RoutePath::Restore);
811            add_endpoint(restore_protected_endpoint, &auth_settings.restore);
812        }
813
814        // Check proof state endpoint
815        {
816            let state_protected_endpoint =
817                ProtectedEndpoint::new(Method::Post, RoutePath::Checkstate);
818            add_endpoint(state_protected_endpoint, &auth_settings.check_proof_state);
819        }
820
821        // Ws endpoint
822        {
823            let ws_protected_endpoint = ProtectedEndpoint::new(Method::Get, RoutePath::Ws);
824            add_endpoint(ws_protected_endpoint, &auth_settings.websocket_auth);
825        }
826
827        // Custom protected_endpoints will be added dynamically after the mint is built
828        // and we can query the payment processors for their supported methods.
829        // For now, we don't add any custom endpoints here - they'll be added in the
830        // start_services_with_shutdown function after we have access to the mint instance.
831
832        mint_builder = mint_builder.with_auth(
833            auth_localstore.clone(),
834            auth_settings.openid_discovery,
835            auth_settings.openid_client_id,
836            clear_auth_endpoints,
837        );
838        mint_builder =
839            mint_builder.with_blind_auth(auth_settings.mint_max_bat, blind_auth_endpoints);
840
841        let mut tx = auth_localstore.begin_transaction().await?;
842
843        tx.remove_protected_endpoints(unprotected_endpoints).await?;
844        tx.add_protected_endpoints(protected_endpoints).await?;
845        tx.commit().await?;
846
847        Ok((mint_builder, Some(auth_localstore)))
848    } else {
849        Ok((mint_builder, None))
850    }
851}
852
853/// Build mints with the configured the signing method (remote signatory or local seed)
854async fn build_mint(
855    settings: &config::Settings,
856    keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
857    mint_builder: MintBuilder,
858) -> Result<Mint> {
859    if let Some(signatory_url) = settings.info.signatory_url.clone() {
860        tracing::info!(
861            "Connecting to remote signatory to {} with certs {:?}",
862            signatory_url,
863            settings.info.signatory_certs.clone()
864        );
865
866        Ok(mint_builder
867            .build_with_signatory(Arc::new(
868                cdk_signatory::SignatoryRpcClient::new(
869                    signatory_url,
870                    settings.info.signatory_certs.clone(),
871                )
872                .await?,
873            ))
874            .await?)
875    } else if let Some(seed) = settings.info.seed.clone() {
876        let seed_bytes: Vec<u8> = seed.into();
877        Ok(mint_builder.build_with_seed(keystore, &seed_bytes).await?)
878    } else if let Some(mnemonic) = settings
879        .info
880        .mnemonic
881        .clone()
882        .map(|s| Mnemonic::from_str(&s))
883        .transpose()?
884    {
885        Ok(mint_builder
886            .build_with_seed(keystore, &mnemonic.to_seed_normalized(""))
887            .await?)
888    } else {
889        bail!("No seed nor remote signatory set");
890    }
891}
892
893async fn start_services_with_shutdown(
894    mint: Arc<cdk::mint::Mint>,
895    settings: &config::Settings,
896    _work_dir: &Path,
897    mint_builder_info: cdk::nuts::MintInfo,
898    shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
899    routers: Vec<Router>,
900    auth_localstore: Option<cdk_common::database::DynMintAuthDatabase>,
901) -> Result<()> {
902    let listen_addr = settings.info.listen_host.clone();
903    let listen_port = settings.info.listen_port;
904    let cache: HttpCache = settings.info.http_cache.clone().into();
905
906    #[cfg(feature = "management-rpc")]
907    let mut rpc_enabled = false;
908    #[cfg(not(feature = "management-rpc"))]
909    let rpc_enabled = false;
910
911    #[cfg(feature = "management-rpc")]
912    let mut rpc_server: Option<cdk_mint_rpc::MintRPCServer> = None;
913
914    #[cfg(feature = "management-rpc")]
915    {
916        if let Some(rpc_settings) = settings.mint_management_rpc.clone() {
917            if rpc_settings.enabled {
918                let addr = rpc_settings.address.unwrap_or("127.0.0.1".to_string());
919                let port = rpc_settings.port.unwrap_or(8086);
920                let mut mint_rpc = cdk_mint_rpc::MintRPCServer::new(&addr, port, mint.clone())?;
921
922                let tls_dir = rpc_settings.tls_dir_path.unwrap_or(_work_dir.join("tls"));
923
924                let tls_dir = if tls_dir.exists() {
925                    Some(tls_dir)
926                } else {
927                    tracing::warn!(
928                        "TLS directory does not exist: {}. Starting RPC server in INSECURE mode without TLS encryption",
929                        tls_dir.display()
930                    );
931                    None
932                };
933
934                mint_rpc.start(tls_dir).await?;
935
936                rpc_server = Some(mint_rpc);
937
938                rpc_enabled = true;
939            }
940        }
941    }
942
943    // Determine the desired QuoteTTL from config/env or fall back to defaults
944    let desired_quote_ttl: QuoteTTL = settings.info.quote_ttl.unwrap_or_default();
945
946    if rpc_enabled {
947        if mint.mint_info().await.is_err() {
948            tracing::info!("Mint info not set on mint, setting.");
949            // First boot with RPC enabled: seed from config
950            mint.set_mint_info(mint_builder_info).await?;
951            mint.set_quote_ttl(desired_quote_ttl).await?;
952        } else {
953            // If QuoteTTL has never been persisted, seed it now from config
954            if !mint.quote_ttl_is_persisted().await? {
955                mint.set_quote_ttl(desired_quote_ttl).await?;
956            }
957            // Add/refresh version information without altering stored mint_info fields
958            let mint_version = MintVersion::new(
959                "cdk-mintd".to_string(),
960                CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
961            );
962            let mut stored_mint_info = mint.mint_info().await?;
963            stored_mint_info.version = Some(mint_version);
964            mint.set_mint_info(stored_mint_info).await?;
965
966            tracing::info!("Mint info already set, not using config file settings.");
967        }
968    } else {
969        // RPC disabled: config is source of truth on every boot
970        tracing::info!("RPC not enabled, using mint info and quote TTL from config.");
971        let mut mint_builder_info = mint_builder_info;
972
973        if let Ok(mint_info) = mint.mint_info().await {
974            if mint_builder_info.pubkey.is_none() {
975                mint_builder_info.pubkey = mint_info.pubkey;
976            }
977        }
978
979        mint.set_mint_info(mint_builder_info).await?;
980        mint.set_quote_ttl(desired_quote_ttl).await?;
981    }
982
983    let mint_info = mint.mint_info().await?;
984    let nut04_methods = mint_info.nuts.nut04.supported_methods();
985    let nut05_methods = mint_info.nuts.nut05.supported_methods();
986
987    // Get custom payment methods from payment processors
988    let mut custom_methods = mint.get_custom_payment_methods().await?;
989
990    // Add bolt11 if it's supported by any payment processor
991    let bolt11_method = PaymentMethod::Known(KnownMethod::Bolt11);
992    let bolt11_supported =
993        nut04_methods.contains(&&bolt11_method) || nut05_methods.contains(&&bolt11_method);
994    // Add bolt12 if it's supported by any payment processor
995    let bolt12_method = PaymentMethod::Known(KnownMethod::Bolt12);
996    let bolt12_supported =
997        nut04_methods.contains(&&bolt12_method) || nut05_methods.contains(&&bolt12_method);
998
999    if bolt11_supported
1000        && !custom_methods.contains(&PaymentMethod::Known(KnownMethod::Bolt11).to_string())
1001    {
1002        custom_methods.push(PaymentMethod::Known(KnownMethod::Bolt11).to_string());
1003    }
1004    if bolt12_supported
1005        && !custom_methods.contains(&PaymentMethod::Known(KnownMethod::Bolt12).to_string())
1006    {
1007        custom_methods.push(PaymentMethod::Known(KnownMethod::Bolt12).to_string());
1008    }
1009
1010    tracing::info!("Payment methods: {:?}", custom_methods);
1011
1012    // Configure auth for custom payment methods if auth is enabled
1013    if let (Some(ref auth_settings), Some(auth_db)) = (&settings.auth, &auth_localstore) {
1014        if auth_settings.auth_enabled {
1015            use std::collections::HashMap;
1016
1017            use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
1018            use cdk::nuts::AuthRequired;
1019
1020            use crate::config::AuthType;
1021
1022            // First, remove all existing payment-method-related endpoints from the database
1023            // to ensure old payment methods don't persist when configuration changes
1024            let existing_endpoints = auth_db.get_auth_for_endpoints().await?;
1025            let payment_method_endpoints_to_remove: Vec<ProtectedEndpoint> = existing_endpoints
1026                .keys()
1027                .filter(|endpoint| {
1028                    matches!(
1029                        endpoint.path,
1030                        RoutePath::MintQuote(_)
1031                            | RoutePath::Mint(_)
1032                            | RoutePath::MeltQuote(_)
1033                            | RoutePath::Melt(_)
1034                    )
1035                })
1036                .cloned()
1037                .collect();
1038
1039            if !payment_method_endpoints_to_remove.is_empty() {
1040                tracing::debug!(
1041                    "Removing {} old payment method endpoints from database",
1042                    payment_method_endpoints_to_remove.len()
1043                );
1044                let mut tx = auth_db.begin_transaction().await?;
1045                tx.remove_protected_endpoints(payment_method_endpoints_to_remove)
1046                    .await?;
1047                tx.commit().await?;
1048            }
1049
1050            // Now add endpoints for current payment methods
1051            if !custom_methods.is_empty() {
1052                let mut protected_endpoints = HashMap::new();
1053
1054                for method_name in &custom_methods {
1055                    tracing::debug!("Adding auth endpoints for payment method: {}", method_name);
1056
1057                    // Determine auth type based on settings
1058                    let mint_quote_auth = match auth_settings.get_mint_quote {
1059                        AuthType::Clear => Some(AuthRequired::Clear),
1060                        AuthType::Blind => Some(AuthRequired::Blind),
1061                        AuthType::None => None,
1062                    };
1063
1064                    let check_mint_quote_auth = match auth_settings.check_mint_quote {
1065                        AuthType::Clear => Some(AuthRequired::Clear),
1066                        AuthType::Blind => Some(AuthRequired::Blind),
1067                        AuthType::None => None,
1068                    };
1069
1070                    let mint_auth = match auth_settings.mint {
1071                        AuthType::Clear => Some(AuthRequired::Clear),
1072                        AuthType::Blind => Some(AuthRequired::Blind),
1073                        AuthType::None => None,
1074                    };
1075
1076                    let melt_quote_auth = match auth_settings.get_melt_quote {
1077                        AuthType::Clear => Some(AuthRequired::Clear),
1078                        AuthType::Blind => Some(AuthRequired::Blind),
1079                        AuthType::None => None,
1080                    };
1081
1082                    let check_melt_quote_auth = match auth_settings.check_melt_quote {
1083                        AuthType::Clear => Some(AuthRequired::Clear),
1084                        AuthType::Blind => Some(AuthRequired::Blind),
1085                        AuthType::None => None,
1086                    };
1087
1088                    let melt_auth = match auth_settings.melt {
1089                        AuthType::Clear => Some(AuthRequired::Clear),
1090                        AuthType::Blind => Some(AuthRequired::Blind),
1091                        AuthType::None => None,
1092                    };
1093
1094                    // Create endpoints for each payment method operation
1095                    if let Some(auth) = mint_quote_auth {
1096                        protected_endpoints.insert(
1097                            ProtectedEndpoint::new(
1098                                Method::Post,
1099                                RoutePath::MintQuote(method_name.clone()),
1100                            ),
1101                            auth,
1102                        );
1103                    }
1104                    if let Some(auth) = check_mint_quote_auth {
1105                        protected_endpoints.insert(
1106                            ProtectedEndpoint::new(
1107                                Method::Get,
1108                                RoutePath::MintQuote(method_name.clone()),
1109                            ),
1110                            auth,
1111                        );
1112                    }
1113                    if let Some(auth) = mint_auth {
1114                        protected_endpoints.insert(
1115                            ProtectedEndpoint::new(
1116                                Method::Post,
1117                                RoutePath::Mint(method_name.clone()),
1118                            ),
1119                            auth,
1120                        );
1121                    }
1122                    if let Some(auth) = melt_quote_auth {
1123                        protected_endpoints.insert(
1124                            ProtectedEndpoint::new(
1125                                Method::Post,
1126                                RoutePath::MeltQuote(method_name.clone()),
1127                            ),
1128                            auth,
1129                        );
1130                    }
1131                    if let Some(auth) = check_melt_quote_auth {
1132                        protected_endpoints.insert(
1133                            ProtectedEndpoint::new(
1134                                Method::Get,
1135                                RoutePath::MeltQuote(method_name.clone()),
1136                            ),
1137                            auth,
1138                        );
1139                    }
1140                    if let Some(auth) = melt_auth {
1141                        protected_endpoints.insert(
1142                            ProtectedEndpoint::new(
1143                                Method::Post,
1144                                RoutePath::Melt(method_name.clone()),
1145                            ),
1146                            auth,
1147                        );
1148                    }
1149                }
1150
1151                // Add all custom endpoints in one transaction
1152                if !protected_endpoints.is_empty() {
1153                    let mut tx = auth_db.begin_transaction().await?;
1154                    tx.add_protected_endpoints(protected_endpoints).await?;
1155                    tx.commit().await?;
1156                }
1157            }
1158        }
1159    }
1160
1161    let v1_service =
1162        cdk_axum::create_mint_router_with_custom_cache(Arc::clone(&mint), cache, custom_methods)
1163            .await?;
1164
1165    let mut mint_service = Router::new()
1166        .merge(v1_service)
1167        .layer(
1168            ServiceBuilder::new()
1169                .layer(RequestDecompressionLayer::new())
1170                .layer(CompressionLayer::new()),
1171        )
1172        .layer(TraceLayer::new_for_http());
1173
1174    for router in routers {
1175        mint_service = mint_service.merge(router);
1176    }
1177
1178    #[cfg(feature = "swagger")]
1179    {
1180        if settings.info.enable_swagger_ui.unwrap_or(false) {
1181            mint_service = mint_service.merge(
1182                utoipa_swagger_ui::SwaggerUi::new("/swagger-ui")
1183                    .url("/api-docs/openapi.json", cdk_axum::ApiDoc::openapi()),
1184            );
1185        }
1186    }
1187    // Create a broadcast channel to share shutdown signal between services
1188    let (shutdown_tx, _) = tokio::sync::broadcast::channel::<()>(1);
1189
1190    // Start Prometheus server if enabled
1191    #[cfg(feature = "prometheus")]
1192    let prometheus_handle = {
1193        if let Some(prometheus_settings) = &settings.prometheus {
1194            if prometheus_settings.enabled {
1195                let addr = prometheus_settings
1196                    .address
1197                    .clone()
1198                    .unwrap_or("127.0.0.1".to_string());
1199                let port = prometheus_settings.port.unwrap_or(9000);
1200
1201                let address = format!("{}:{}", addr, port)
1202                    .parse()
1203                    .expect("Invalid prometheus address");
1204
1205                let server = cdk_prometheus::PrometheusBuilder::new()
1206                    .bind_address(address)
1207                    .build_with_cdk_metrics()?;
1208
1209                let mut shutdown_rx = shutdown_tx.subscribe();
1210                let prometheus_shutdown = async move {
1211                    let _ = shutdown_rx.recv().await;
1212                };
1213
1214                Some(tokio::spawn(async move {
1215                    if let Err(e) = server.start(prometheus_shutdown).await {
1216                        tracing::error!("Failed to start prometheus server: {}", e);
1217                    }
1218                }))
1219            } else {
1220                None
1221            }
1222        } else {
1223            None
1224        }
1225    };
1226
1227    mint.start().await?;
1228
1229    let socket_addr = SocketAddr::from_str(&format!("{listen_addr}:{listen_port}"))?;
1230
1231    let listener = tokio::net::TcpListener::bind(socket_addr).await?;
1232
1233    tracing::info!("listening on {}", listener.local_addr()?);
1234
1235    // Create a task to wait for the shutdown signal and broadcast it
1236    let shutdown_broadcast_task = {
1237        let shutdown_tx = shutdown_tx.clone();
1238        tokio::spawn(async move {
1239            shutdown_signal.await;
1240            tracing::info!("Shutdown signal received, broadcasting to all services");
1241            let _ = shutdown_tx.send(());
1242        })
1243    };
1244
1245    // Create shutdown future for axum server
1246    let mut axum_shutdown_rx = shutdown_tx.subscribe();
1247    let axum_shutdown = async move {
1248        let _ = axum_shutdown_rx.recv().await;
1249    };
1250
1251    // Wait for axum server to complete with custom shutdown signal
1252    let axum_result = axum::serve(listener, mint_service).with_graceful_shutdown(axum_shutdown);
1253
1254    match axum_result.await {
1255        Ok(_) => {
1256            tracing::info!("Axum server stopped with okay status");
1257        }
1258        Err(err) => {
1259            tracing::warn!("Axum server stopped with error");
1260            tracing::error!("{}", err);
1261            bail!("Axum exited with error")
1262        }
1263    }
1264
1265    // Wait for the shutdown broadcast task to complete
1266    let _ = shutdown_broadcast_task.await;
1267
1268    // Wait for prometheus server to shutdown if it was started
1269    #[cfg(feature = "prometheus")]
1270    if let Some(handle) = prometheus_handle {
1271        if let Err(e) = handle.await {
1272            tracing::warn!("Prometheus server task failed: {}", e);
1273        }
1274    }
1275
1276    mint.stop().await?;
1277
1278    #[cfg(feature = "management-rpc")]
1279    {
1280        if let Some(rpc_server) = rpc_server {
1281            rpc_server.stop().await?;
1282        }
1283    }
1284
1285    Ok(())
1286}
1287
1288async fn shutdown_signal() {
1289    tokio::signal::ctrl_c()
1290        .await
1291        .expect("failed to install CTRL+C handler");
1292    tracing::info!("Shutdown signal received");
1293}
1294
1295fn work_dir() -> Result<PathBuf> {
1296    let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?;
1297    let dir = home_dir.join(".cdk-mintd");
1298
1299    std::fs::create_dir_all(&dir)?;
1300
1301    Ok(dir)
1302}
1303
1304/// The main entry point for the application when used as a library
1305pub async fn run_mintd(
1306    work_dir: &Path,
1307    settings: &config::Settings,
1308    db_password: Option<String>,
1309    enable_logging: bool,
1310    runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1311    routers: Vec<Router>,
1312) -> Result<()> {
1313    let _guard = if enable_logging {
1314        setup_tracing(work_dir, &settings.info.logging)?
1315    } else {
1316        None
1317    };
1318
1319    let result = run_mintd_with_shutdown(
1320        work_dir,
1321        settings,
1322        shutdown_signal(),
1323        db_password,
1324        runtime,
1325        routers,
1326    )
1327    .await;
1328
1329    // Explicitly drop the guard to ensure proper cleanup
1330    if let Some(guard) = _guard {
1331        tracing::info!("Shutting down logging worker thread");
1332        drop(guard);
1333        // Give the worker thread a moment to flush any remaining logs
1334        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
1335    }
1336
1337    tracing::info!("Mintd shutdown");
1338
1339    result
1340}
1341
1342/// Run mintd with a custom shutdown signal
1343pub async fn run_mintd_with_shutdown(
1344    work_dir: &Path,
1345    settings: &config::Settings,
1346    shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
1347    db_password: Option<String>,
1348    runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1349    routers: Vec<Router>,
1350) -> Result<()> {
1351    let (localstore, keystore, kv) = initial_setup(work_dir, settings, db_password.clone()).await?;
1352
1353    let mint_builder = MintBuilder::new(localstore);
1354
1355    // If RPC is enabled and DB contains mint_info already, initialize the builder from DB.
1356    // This ensures subsequent builder modifications (like version injection) can respect stored values.
1357    let maybe_mint_builder = {
1358        #[cfg(feature = "management-rpc")]
1359        {
1360            if let Some(rpc_settings) = settings.mint_management_rpc.clone() {
1361                if rpc_settings.enabled {
1362                    // Best-effort: pull DB state into builder if present
1363                    let mut tmp = mint_builder;
1364                    if let Err(e) = tmp.init_from_db_if_present().await {
1365                        tracing::warn!("Failed to init builder from DB: {}", e);
1366                    }
1367                    tmp
1368                } else {
1369                    mint_builder
1370                }
1371            } else {
1372                mint_builder
1373            }
1374        }
1375        #[cfg(not(feature = "management-rpc"))]
1376        {
1377            mint_builder
1378        }
1379    };
1380
1381    let mint_builder =
1382        configure_mint_builder(settings, maybe_mint_builder, runtime, work_dir, Some(kv)).await?;
1383    let (mint_builder, auth_localstore) =
1384        setup_authentication(settings, work_dir, mint_builder, db_password).await?;
1385
1386    let config_mint_info = mint_builder.current_mint_info();
1387
1388    let mint = build_mint(settings, keystore, mint_builder).await?;
1389
1390    tracing::debug!("Mint built from builder.");
1391
1392    let mint = Arc::new(mint);
1393
1394    start_services_with_shutdown(
1395        mint.clone(),
1396        settings,
1397        work_dir,
1398        config_mint_info,
1399        shutdown_signal,
1400        routers,
1401        auth_localstore,
1402    )
1403    .await
1404}
1405
1406#[cfg(test)]
1407mod tests {
1408    use super::*;
1409
1410    #[test]
1411    fn test_postgres_auth_url_validation() {
1412        // Test that the auth database config requires explicit configuration
1413
1414        // Test empty URL
1415        let auth_config = config::PostgresAuthConfig {
1416            url: "".to_string(),
1417            ..Default::default()
1418        };
1419        assert!(auth_config.url.is_empty());
1420
1421        // Test non-empty URL
1422        let auth_config = config::PostgresAuthConfig {
1423            url: "postgresql://user:password@localhost:5432/auth_db".to_string(),
1424            ..Default::default()
1425        };
1426        assert!(!auth_config.url.is_empty());
1427    }
1428}