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