cdk_mintd/
lib.rs

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