Skip to main content

cdk_mintd/
lib.rs

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