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        mint_builder = mint_builder.with_auth(
796            auth_localstore.clone(),
797            auth_settings.openid_discovery,
798            auth_settings.openid_client_id,
799            clear_auth_endpoints,
800        );
801        mint_builder =
802            mint_builder.with_blind_auth(auth_settings.mint_max_bat, blind_auth_endpoints);
803
804        let mut tx = auth_localstore.begin_transaction().await?;
805
806        tx.remove_protected_endpoints(unprotected_endpoints).await?;
807        tx.add_protected_endpoints(protected_endpoints).await?;
808        tx.commit().await?;
809    }
810    Ok(mint_builder)
811}
812
813/// Build mints with the configured the signing method (remote signatory or local seed)
814async fn build_mint(
815    settings: &config::Settings,
816    keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
817    mint_builder: MintBuilder,
818) -> Result<Mint> {
819    if let Some(signatory_url) = settings.info.signatory_url.clone() {
820        tracing::info!(
821            "Connecting to remote signatory to {} with certs {:?}",
822            signatory_url,
823            settings.info.signatory_certs.clone()
824        );
825
826        Ok(mint_builder
827            .build_with_signatory(Arc::new(
828                cdk_signatory::SignatoryRpcClient::new(
829                    signatory_url,
830                    settings.info.signatory_certs.clone(),
831                )
832                .await?,
833            ))
834            .await?)
835    } else if let Some(seed) = settings.info.seed.clone() {
836        let seed_bytes: Vec<u8> = seed.into();
837        Ok(mint_builder.build_with_seed(keystore, &seed_bytes).await?)
838    } else if let Some(mnemonic) = settings
839        .info
840        .mnemonic
841        .clone()
842        .map(|s| Mnemonic::from_str(&s))
843        .transpose()?
844    {
845        Ok(mint_builder
846            .build_with_seed(keystore, &mnemonic.to_seed_normalized(""))
847            .await?)
848    } else {
849        bail!("No seed nor remote signatory set");
850    }
851}
852
853async fn start_services_with_shutdown(
854    mint: Arc<cdk::mint::Mint>,
855    settings: &config::Settings,
856    work_dir: &Path,
857    mint_builder_info: cdk::nuts::MintInfo,
858    shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
859    routers: Vec<Router>,
860) -> Result<()> {
861    let listen_addr = settings.info.listen_host.clone();
862    let listen_port = settings.info.listen_port;
863    let cache: HttpCache = settings.info.http_cache.clone().into();
864
865    #[cfg(feature = "management-rpc")]
866    let mut rpc_enabled = false;
867    #[cfg(not(feature = "management-rpc"))]
868    let rpc_enabled = false;
869
870    #[cfg(feature = "management-rpc")]
871    let mut rpc_server: Option<cdk_mint_rpc::MintRPCServer> = None;
872
873    #[cfg(feature = "management-rpc")]
874    {
875        if let Some(rpc_settings) = settings.mint_management_rpc.clone() {
876            if rpc_settings.enabled {
877                let addr = rpc_settings.address.unwrap_or("127.0.0.1".to_string());
878                let port = rpc_settings.port.unwrap_or(8086);
879                let mut mint_rpc = cdk_mint_rpc::MintRPCServer::new(&addr, port, mint.clone())?;
880
881                let tls_dir = rpc_settings.tls_dir_path.unwrap_or(work_dir.join("tls"));
882
883                if !tls_dir.exists() {
884                    tracing::error!("TLS directory does not exist: {}", tls_dir.display());
885                    bail!("Cannot start RPC server: TLS directory does not exist");
886                }
887
888                mint_rpc.start(Some(tls_dir)).await?;
889
890                rpc_server = Some(mint_rpc);
891
892                rpc_enabled = true;
893            }
894        }
895    }
896
897    // Determine the desired QuoteTTL from config/env or fall back to defaults
898    let desired_quote_ttl: QuoteTTL = settings.info.quote_ttl.unwrap_or_default();
899
900    if rpc_enabled {
901        if mint.mint_info().await.is_err() {
902            tracing::info!("Mint info not set on mint, setting.");
903            // First boot with RPC enabled: seed from config
904            mint.set_mint_info(mint_builder_info).await?;
905            mint.set_quote_ttl(desired_quote_ttl).await?;
906        } else {
907            // If QuoteTTL has never been persisted, seed it now from config
908            if !mint.quote_ttl_is_persisted().await? {
909                mint.set_quote_ttl(desired_quote_ttl).await?;
910            }
911            // Add/refresh version information without altering stored mint_info fields
912            let mint_version = MintVersion::new(
913                "cdk-mintd".to_string(),
914                CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
915            );
916            let mut stored_mint_info = mint.mint_info().await?;
917            stored_mint_info.version = Some(mint_version);
918            mint.set_mint_info(stored_mint_info).await?;
919
920            tracing::info!("Mint info already set, not using config file settings.");
921        }
922    } else {
923        // RPC disabled: config is source of truth on every boot
924        tracing::info!("RPC not enabled, using mint info and quote TTL from config.");
925        let mut mint_builder_info = mint_builder_info;
926
927        if let Ok(mint_info) = mint.mint_info().await {
928            if mint_builder_info.pubkey.is_none() {
929                mint_builder_info.pubkey = mint_info.pubkey;
930            }
931        }
932
933        mint.set_mint_info(mint_builder_info).await?;
934        mint.set_quote_ttl(desired_quote_ttl).await?;
935    }
936
937    let mint_info = mint.mint_info().await?;
938    let nut04_methods = mint_info.nuts.nut04.supported_methods();
939    let nut05_methods = mint_info.nuts.nut05.supported_methods();
940
941    let bolt12_supported = nut04_methods.contains(&&PaymentMethod::Bolt12)
942        || nut05_methods.contains(&&PaymentMethod::Bolt12);
943
944    let v1_service =
945        cdk_axum::create_mint_router_with_custom_cache(Arc::clone(&mint), cache, bolt12_supported)
946            .await?;
947
948    let mut mint_service = Router::new()
949        .merge(v1_service)
950        .layer(
951            ServiceBuilder::new()
952                .layer(RequestDecompressionLayer::new())
953                .layer(CompressionLayer::new()),
954        )
955        .layer(TraceLayer::new_for_http());
956
957    for router in routers {
958        mint_service = mint_service.merge(router);
959    }
960
961    #[cfg(feature = "swagger")]
962    {
963        if settings.info.enable_swagger_ui.unwrap_or(false) {
964            mint_service = mint_service.merge(
965                utoipa_swagger_ui::SwaggerUi::new("/swagger-ui")
966                    .url("/api-docs/openapi.json", cdk_axum::ApiDoc::openapi()),
967            );
968        }
969    }
970    // Create a broadcast channel to share shutdown signal between services
971    let (shutdown_tx, _) = tokio::sync::broadcast::channel::<()>(1);
972
973    // Start Prometheus server if enabled
974    #[cfg(feature = "prometheus")]
975    let prometheus_handle = {
976        if let Some(prometheus_settings) = &settings.prometheus {
977            if prometheus_settings.enabled {
978                let addr = prometheus_settings
979                    .address
980                    .clone()
981                    .unwrap_or("127.0.0.1".to_string());
982                let port = prometheus_settings.port.unwrap_or(9000);
983
984                let address = format!("{}:{}", addr, port)
985                    .parse()
986                    .expect("Invalid prometheus address");
987
988                let server = cdk_prometheus::PrometheusBuilder::new()
989                    .bind_address(address)
990                    .build_with_cdk_metrics()?;
991
992                let mut shutdown_rx = shutdown_tx.subscribe();
993                let prometheus_shutdown = async move {
994                    let _ = shutdown_rx.recv().await;
995                };
996
997                Some(tokio::spawn(async move {
998                    if let Err(e) = server.start(prometheus_shutdown).await {
999                        tracing::error!("Failed to start prometheus server: {}", e);
1000                    }
1001                }))
1002            } else {
1003                None
1004            }
1005        } else {
1006            None
1007        }
1008    };
1009
1010    #[cfg(not(feature = "prometheus"))]
1011    let prometheus_handle: Option<tokio::task::JoinHandle<()>> = None;
1012
1013    mint.start().await?;
1014
1015    let socket_addr = SocketAddr::from_str(&format!("{listen_addr}:{listen_port}"))?;
1016
1017    let listener = tokio::net::TcpListener::bind(socket_addr).await?;
1018
1019    tracing::info!("listening on {}", listener.local_addr().unwrap());
1020
1021    // Create a task to wait for the shutdown signal and broadcast it
1022    let shutdown_broadcast_task = {
1023        let shutdown_tx = shutdown_tx.clone();
1024        tokio::spawn(async move {
1025            shutdown_signal.await;
1026            tracing::info!("Shutdown signal received, broadcasting to all services");
1027            let _ = shutdown_tx.send(());
1028        })
1029    };
1030
1031    // Create shutdown future for axum server
1032    let mut axum_shutdown_rx = shutdown_tx.subscribe();
1033    let axum_shutdown = async move {
1034        let _ = axum_shutdown_rx.recv().await;
1035    };
1036
1037    // Wait for axum server to complete with custom shutdown signal
1038    let axum_result = axum::serve(listener, mint_service).with_graceful_shutdown(axum_shutdown);
1039
1040    match axum_result.await {
1041        Ok(_) => {
1042            tracing::info!("Axum server stopped with okay status");
1043        }
1044        Err(err) => {
1045            tracing::warn!("Axum server stopped with error");
1046            tracing::error!("{}", err);
1047            bail!("Axum exited with error")
1048        }
1049    }
1050
1051    // Wait for the shutdown broadcast task to complete
1052    let _ = shutdown_broadcast_task.await;
1053
1054    // Wait for prometheus server to shutdown if it was started
1055    #[cfg(feature = "prometheus")]
1056    if let Some(handle) = prometheus_handle {
1057        if let Err(e) = handle.await {
1058            tracing::warn!("Prometheus server task failed: {}", e);
1059        }
1060    }
1061
1062    mint.stop().await?;
1063
1064    #[cfg(feature = "management-rpc")]
1065    {
1066        if let Some(rpc_server) = rpc_server {
1067            rpc_server.stop().await?;
1068        }
1069    }
1070
1071    Ok(())
1072}
1073
1074async fn shutdown_signal() {
1075    tokio::signal::ctrl_c()
1076        .await
1077        .expect("failed to install CTRL+C handler");
1078    tracing::info!("Shutdown signal received");
1079}
1080
1081fn work_dir() -> Result<PathBuf> {
1082    let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?;
1083    let dir = home_dir.join(".cdk-mintd");
1084
1085    std::fs::create_dir_all(&dir)?;
1086
1087    Ok(dir)
1088}
1089
1090/// The main entry point for the application when used as a library
1091pub async fn run_mintd(
1092    work_dir: &Path,
1093    settings: &config::Settings,
1094    db_password: Option<String>,
1095    enable_logging: bool,
1096    runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1097    routers: Vec<Router>,
1098) -> Result<()> {
1099    let _guard = if enable_logging {
1100        setup_tracing(work_dir, &settings.info.logging)?
1101    } else {
1102        None
1103    };
1104
1105    let result = run_mintd_with_shutdown(
1106        work_dir,
1107        settings,
1108        shutdown_signal(),
1109        db_password,
1110        runtime,
1111        routers,
1112    )
1113    .await;
1114
1115    // Explicitly drop the guard to ensure proper cleanup
1116    if let Some(guard) = _guard {
1117        tracing::info!("Shutting down logging worker thread");
1118        drop(guard);
1119        // Give the worker thread a moment to flush any remaining logs
1120        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
1121    }
1122
1123    tracing::info!("Mintd shutdown");
1124
1125    result
1126}
1127
1128/// Run mintd with a custom shutdown signal
1129pub async fn run_mintd_with_shutdown(
1130    work_dir: &Path,
1131    settings: &config::Settings,
1132    shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
1133    db_password: Option<String>,
1134    runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1135    routers: Vec<Router>,
1136) -> Result<()> {
1137    let (localstore, keystore, kv) = initial_setup(work_dir, settings, db_password.clone()).await?;
1138
1139    let mint_builder = MintBuilder::new(localstore);
1140
1141    // If RPC is enabled and DB contains mint_info already, initialize the builder from DB.
1142    // This ensures subsequent builder modifications (like version injection) can respect stored values.
1143    let maybe_mint_builder = {
1144        #[cfg(feature = "management-rpc")]
1145        {
1146            if let Some(rpc_settings) = settings.mint_management_rpc.clone() {
1147                if rpc_settings.enabled {
1148                    // Best-effort: pull DB state into builder if present
1149                    let mut tmp = mint_builder;
1150                    if let Err(e) = tmp.init_from_db_if_present().await {
1151                        tracing::warn!("Failed to init builder from DB: {}", e);
1152                    }
1153                    tmp
1154                } else {
1155                    mint_builder
1156                }
1157            } else {
1158                mint_builder
1159            }
1160        }
1161        #[cfg(not(feature = "management-rpc"))]
1162        {
1163            mint_builder
1164        }
1165    };
1166
1167    let mint_builder =
1168        configure_mint_builder(settings, maybe_mint_builder, runtime, work_dir, Some(kv)).await?;
1169    #[cfg(feature = "auth")]
1170    let mint_builder = setup_authentication(settings, work_dir, mint_builder, db_password).await?;
1171
1172    let config_mint_info = mint_builder.current_mint_info();
1173
1174    let mint = build_mint(settings, keystore, mint_builder).await?;
1175
1176    tracing::debug!("Mint built from builder.");
1177
1178    let mint = Arc::new(mint);
1179
1180    // Checks the status of all pending melt quotes
1181    // Pending melt quotes where the payment has gone through inputs are burnt
1182    // Pending melt quotes where the payment has **failed** inputs are reset to unspent
1183    mint.check_pending_melt_quotes().await?;
1184
1185    start_services_with_shutdown(
1186        mint.clone(),
1187        settings,
1188        work_dir,
1189        config_mint_info,
1190        shutdown_signal,
1191        routers,
1192    )
1193    .await
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198    use super::*;
1199
1200    #[test]
1201    fn test_postgres_auth_url_validation() {
1202        // Test that the auth database config requires explicit configuration
1203
1204        // Test empty URL
1205        let auth_config = config::PostgresAuthConfig {
1206            url: "".to_string(),
1207            ..Default::default()
1208        };
1209        assert!(auth_config.url.is_empty());
1210
1211        // Test non-empty URL
1212        let auth_config = config::PostgresAuthConfig {
1213            url: "postgresql://user:password@localhost:5432/auth_db".to_string(),
1214            ..Default::default()
1215        };
1216        assert!(!auth_config.url.is_empty());
1217    }
1218}