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, Context, Result};
14use axum::extract::DefaultBodyLimit;
15use axum::Router;
16use bip39::Mnemonic;
17use cdk::cdk_database::{self, KVStore, MintDatabase, MintKeysDatabase};
18use cdk::mint::{Mint, MintBuilder, MintMeltLimits};
19use cdk::nuts::nut00::KnownMethod;
20#[cfg(any(
21    feature = "cln",
22    feature = "lnbits",
23    feature = "lnd",
24    feature = "ldk-node",
25    feature = "fakewallet",
26    feature = "bdk",
27    feature = "grpc-processor"
28))]
29use cdk::nuts::nut17::SupportedMethods;
30use cdk::nuts::nut19::{CachedEndpoint, Method as NUT19Method, Path as NUT19Path};
31use cdk::nuts::{
32    AuthRequired, ContactInfo, Method, MintVersion, PaymentMethod, ProtectedEndpoint, RoutePath,
33};
34use cdk_axum::cache::HttpCache;
35use cdk_common::common::QuoteTTL;
36use cdk_common::database::DynMintDatabase;
37// internal crate modules
38#[cfg(feature = "prometheus")]
39use cdk_common::payment::MetricsMintPayment;
40use cdk_common::payment::MintPayment;
41#[cfg(feature = "postgres")]
42use cdk_postgres::{MintPgAuthDatabase, MintPgDatabase, PgConfig};
43#[cfg(feature = "sqlite")]
44use cdk_sqlite::mint::MintSqliteAuthDatabase;
45#[cfg(feature = "sqlite")]
46use cdk_sqlite::MintSqliteDatabase;
47use cli::CLIArgs;
48use config::{AuthType, DatabaseEngine, LnBackend};
49use env_vars::ENV_WORK_DIR;
50use setup::LnBackendSetup;
51use tower::ServiceBuilder;
52use tower_http::compression::CompressionLayer;
53use tower_http::decompression::RequestDecompressionLayer;
54use tower_http::trace::TraceLayer;
55use tracing_appender::{non_blocking, rolling};
56use tracing_subscriber::fmt::writer::MakeWriterExt;
57use tracing_subscriber::EnvFilter;
58
59pub mod cli;
60pub mod config;
61pub mod env_vars;
62pub mod setup;
63
64const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
65const DEFAULT_BATCH_MINT_SIZE: u64 = 100;
66const REQUEST_BODY_LIMIT_BYTES: usize = 1_048_576;
67
68fn extract_supported_payment_methods(mint_info: &cdk::nuts::MintInfo) -> Vec<String> {
69    let mut seen = HashSet::new();
70    mint_info
71        .nuts
72        .nut04
73        .methods
74        .iter()
75        .map(|method| method.method.to_string())
76        .filter(|method| seen.insert(method.clone()))
77        .collect()
78}
79
80#[cfg(feature = "cln")]
81fn expand_path(path: &str) -> Option<PathBuf> {
82    if path == "~" {
83        return home::home_dir();
84    }
85
86    if let Some(remainder) = path.strip_prefix("~/") {
87        return home::home_dir().map(|home_dir| home_dir.join(remainder));
88    }
89
90    Some(PathBuf::from(path))
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 KVStore<Err = cdk_database::Error> + Send + Sync>,
104)> {
105    tracing::info!("Initializing database...");
106    let (localstore, keystore, kv) = setup_database(settings, work_dir, db_password).await?;
107    tracing::info!("Database initialized successfully");
108    Ok((localstore, keystore, kv))
109}
110
111/// Sets up and initializes a tracing subscriber with custom log filtering.
112/// Logs can be configured to output to stdout only, file only, or both.
113/// Returns a guard that must be kept alive and properly dropped on shutdown.
114pub fn setup_tracing(
115    work_dir: &Path,
116    logging_config: &config::LoggingConfig,
117) -> Result<Option<tracing_appender::non_blocking::WorkerGuard>> {
118    let default_filter = "debug";
119    let hyper_filter = "hyper=warn,rustls=warn,reqwest=warn";
120    let h2_filter = "h2=warn";
121    let tower_filter = "tower=warn";
122    let tower_http = "tower_http=warn";
123    let rustls = "rustls=warn";
124    let tungstenite = "tungstenite=warn";
125    let tokio_postgres = "tokio_postgres=warn";
126
127    let env_filter = EnvFilter::new(format!(
128        "{default_filter},{hyper_filter},{h2_filter},{tower_filter},{tower_http},{rustls},{tungstenite},{tokio_postgres}"
129    ));
130
131    use config::LoggingOutput;
132    match logging_config.output {
133        LoggingOutput::Stderr => {
134            // Console output only (stderr)
135            let console_level = logging_config
136                .console_level
137                .as_deref()
138                .unwrap_or("info")
139                .parse::<tracing::Level>()
140                .unwrap_or(tracing::Level::INFO);
141
142            let stderr = std::io::stderr.with_max_level(console_level);
143
144            tracing_subscriber::fmt()
145                .with_env_filter(env_filter)
146                .with_ansi(false)
147                .with_writer(stderr)
148                .init();
149
150            tracing::info!("Logging initialized: console only ({}+)", console_level);
151            Ok(None)
152        }
153        LoggingOutput::File => {
154            // File output only
155            let file_level = logging_config
156                .file_level
157                .as_deref()
158                .unwrap_or("debug")
159                .parse::<tracing::Level>()
160                .unwrap_or(tracing::Level::DEBUG);
161
162            // Create logs directory in work_dir if it doesn't exist
163            let logs_dir = work_dir.join("logs");
164            std::fs::create_dir_all(&logs_dir)?;
165
166            // Set up file appender with daily rotation
167            let file_appender = rolling::daily(&logs_dir, "cdk-mintd.log");
168            let (non_blocking_appender, guard) = non_blocking(file_appender);
169
170            let file_writer = non_blocking_appender.with_max_level(file_level);
171
172            tracing_subscriber::fmt()
173                .with_env_filter(env_filter)
174                .with_ansi(false)
175                .with_writer(file_writer)
176                .init();
177
178            tracing::info!(
179                "Logging initialized: file only at {}/cdk-mintd.log ({}+)",
180                logs_dir.display(),
181                file_level
182            );
183            Ok(Some(guard))
184        }
185        LoggingOutput::Both => {
186            // Both console and file output (stderr + file)
187            let console_level = logging_config
188                .console_level
189                .as_deref()
190                .unwrap_or("info")
191                .parse::<tracing::Level>()
192                .unwrap_or(tracing::Level::INFO);
193            let file_level = logging_config
194                .file_level
195                .as_deref()
196                .unwrap_or("debug")
197                .parse::<tracing::Level>()
198                .unwrap_or(tracing::Level::DEBUG);
199
200            // Create logs directory in work_dir if it doesn't exist
201            let logs_dir = work_dir.join("logs");
202            std::fs::create_dir_all(&logs_dir)?;
203
204            // Set up file appender with daily rotation
205            let file_appender = rolling::daily(&logs_dir, "cdk-mintd.log");
206            let (non_blocking_appender, guard) = non_blocking(file_appender);
207
208            // Combine console output (stderr) and file output
209            let stderr = std::io::stderr.with_max_level(console_level);
210            let file_writer = non_blocking_appender.with_max_level(file_level);
211
212            tracing_subscriber::fmt()
213                .with_env_filter(env_filter)
214                .with_ansi(false)
215                .with_writer(stderr.and(file_writer))
216                .init();
217
218            tracing::info!(
219                "Logging initialized: console ({}+) and file at {}/cdk-mintd.log ({}+)",
220                console_level,
221                logs_dir.display(),
222                file_level
223            );
224            Ok(Some(guard))
225        }
226    }
227}
228
229/// Retrieves the work directory based on command-line arguments, environment variables, or system defaults.
230pub async fn get_work_directory(args: &CLIArgs) -> Result<PathBuf> {
231    let work_dir = if let Some(work_dir) = &args.work_dir {
232        tracing::info!("Using work dir from cmd arg");
233        work_dir.clone()
234    } else if let Ok(env_work_dir) = env::var(ENV_WORK_DIR) {
235        tracing::info!("Using work dir from env var");
236        env_work_dir.into()
237    } else {
238        work_dir()?
239    };
240    tracing::info!("Using work dir: {}", work_dir.display());
241    Ok(work_dir)
242}
243
244/// Loads the application settings based on a configuration file and environment variables.
245pub fn load_settings(work_dir: &Path, config_path: Option<PathBuf>) -> Result<config::Settings> {
246    // get config file name from args
247    let config_file_arg = match config_path {
248        Some(c) => c,
249        None => work_dir.join("config.toml"),
250    };
251
252    let mut settings = if config_file_arg.exists() {
253        config::Settings::try_new(Some(config_file_arg.clone()))
254            .with_context(|| format!("Failed to read config file {}", config_file_arg.display()))?
255    } else {
256        tracing::info!("Config file does not exist. Attempting to read env vars");
257        config::Settings::default()
258    };
259    // This check for any settings defined in ENV VARs
260    // ENV VARS will take **priority** over those in the config
261    settings.from_env()
262}
263
264/// Loads settings from command line arguments, environment variables, and optional seed file.
265pub fn load_settings_from_args(work_dir: &Path, args: &CLIArgs) -> Result<config::Settings> {
266    let mut settings = load_settings(work_dir, args.config.clone())?;
267
268    if let Some(seed_file) = args.seed_file.as_deref() {
269        apply_seed_file(&mut settings, seed_file)?;
270    }
271
272    Ok(settings)
273}
274
275/// Overrides the configured mint and active payment backend mnemonic with a seed file.
276pub fn apply_seed_file(settings: &mut config::Settings, seed_file: &Path) -> Result<()> {
277    let mnemonic = std::fs::read_to_string(seed_file)
278        .with_context(|| format!("Failed to read seed file {}", seed_file.display()))?;
279    let mnemonic = mnemonic.trim();
280
281    if mnemonic.is_empty() {
282        bail!("Seed file {} is empty", seed_file.display());
283    }
284
285    Mnemonic::parse(mnemonic)
286        .with_context(|| format!("Invalid seed phrase in seed file {}", seed_file.display()))?;
287
288    settings.info.seed = None;
289    settings.info.mnemonic = Some(mnemonic.to_owned());
290
291    #[cfg(feature = "bdk")]
292    if settings
293        .onchain
294        .as_ref()
295        .is_some_and(|onchain| onchain.onchain_backend == config::OnchainBackend::Bdk)
296    {
297        let mut bdk = settings.bdk.clone().unwrap_or_default();
298        bdk.mnemonic = Some(mnemonic.to_owned());
299        settings.bdk = Some(bdk);
300    }
301
302    #[cfg(feature = "ldk-node")]
303    if settings
304        .ln
305        .iter()
306        .any(|ln| ln.ln_backend == LnBackend::LdkNode)
307    {
308        let mut ldk_node = settings.ldk_node.clone().unwrap_or_default();
309        ldk_node.ldk_node_mnemonic = Some(mnemonic.to_owned());
310        settings.ldk_node = Some(ldk_node);
311    }
312
313    Ok(())
314}
315
316async fn setup_database(
317    settings: &config::Settings,
318    _work_dir: &Path,
319    _db_password: Option<String>,
320) -> Result<(
321    DynMintDatabase,
322    Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
323    Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync>,
324)> {
325    tracing::info!("Using database engine: {:?}", settings.database.engine);
326    match settings.database.engine {
327        #[cfg(feature = "sqlite")]
328        DatabaseEngine::Sqlite => {
329            let db = setup_sqlite_database(_work_dir, _db_password).await?;
330            let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> = db.clone();
331            let kv: Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync> = db.clone();
332            let keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync> = db;
333            Ok((localstore, keystore, kv))
334        }
335        #[cfg(feature = "postgres")]
336        DatabaseEngine::Postgres => {
337            // Get the PostgreSQL configuration, ensuring it exists
338            let pg_config = settings.database.postgres.as_ref().ok_or_else(|| {
339                anyhow!("PostgreSQL configuration is required when using PostgreSQL engine")
340            })?;
341
342            if pg_config.url.is_empty() {
343                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");
344            }
345
346            #[cfg(feature = "postgres")]
347            let db_config = PgConfig::new(
348                pg_config.url.as_str(),
349                pg_config.tls_mode.as_deref(),
350                pg_config.max_connections,
351                pg_config.connection_timeout_seconds,
352            );
353            #[cfg(feature = "postgres")]
354            let pg_db = Arc::new(MintPgDatabase::new(db_config).await?);
355            tracing::info!("PostgreSQL database connection established");
356            #[cfg(feature = "postgres")]
357            let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> =
358                pg_db.clone();
359            #[cfg(feature = "postgres")]
360            let kv: Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync> = pg_db.clone();
361            #[cfg(feature = "postgres")]
362            let keystore: Arc<
363                dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync,
364            > = pg_db;
365            #[cfg(feature = "postgres")]
366            return Ok((localstore, keystore, kv));
367
368            #[cfg(not(feature = "postgres"))]
369            bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
370        }
371        #[cfg(not(feature = "sqlite"))]
372        DatabaseEngine::Sqlite => {
373            bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
374        }
375        #[cfg(not(feature = "postgres"))]
376        DatabaseEngine::Postgres => {
377            bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
378        }
379    }
380}
381
382#[cfg(feature = "sqlite")]
383async fn setup_sqlite_database(
384    work_dir: &Path,
385    _password: Option<String>,
386) -> Result<Arc<MintSqliteDatabase>> {
387    let sql_db_path = work_dir.join("cdk-mintd.sqlite");
388    tracing::info!("SQLite database path: {}", sql_db_path.display());
389
390    #[cfg(not(feature = "sqlcipher"))]
391    let db = MintSqliteDatabase::new(&sql_db_path).await?;
392    #[cfg(feature = "sqlcipher")]
393    let db = {
394        // Get password from command line arguments for sqlcipher
395        let password = _password
396            .ok_or_else(|| anyhow!("Password required when sqlcipher feature is enabled"))?;
397        tracing::info!("Using SQLCipher encryption for SQLite database");
398        MintSqliteDatabase::new((sql_db_path, password)).await?
399    };
400
401    tracing::info!("SQLite database initialized successfully");
402    Ok(Arc::new(db))
403}
404
405/**
406 * Configures a `MintBuilder` instance with provided settings and initializes
407 * routers for Lightning Network backends.
408 */
409async fn configure_mint_builder(
410    settings: &config::Settings,
411    mint_builder: MintBuilder,
412    runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
413    work_dir: &Path,
414    kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
415) -> Result<MintBuilder> {
416    settings
417        .validate_backend_pairing()
418        .map_err(anyhow::Error::msg)?;
419
420    // Configure basic mint information
421    let mint_builder = configure_basic_info(settings, mint_builder);
422
423    // Check that fake wallet is not used on mainnet
424    #[cfg(feature = "fakewallet")]
425    if settings
426        .ln
427        .iter()
428        .any(|ln| ln.ln_backend == LnBackend::FakeWallet)
429    {
430        if let Some(_onchain) = &settings.onchain {
431            #[cfg(feature = "bdk")]
432            if _onchain.onchain_backend == config::OnchainBackend::Bdk {
433                if let Some(bdk) = &settings.bdk {
434                    if let Some(network) = &bdk.network {
435                        let network = network.to_lowercase();
436                        if network == "mainnet" || network == "bitcoin" {
437                            bail!("Fake wallet cannot be used for Lightning when On-chain is configured for Mainnet");
438                        }
439                    }
440                }
441            }
442        }
443    }
444
445    // Configure lightning backend
446    let mint_builder = configure_lightning_backend(
447        settings,
448        mint_builder,
449        runtime.clone(),
450        work_dir,
451        kv_store.clone(),
452    )
453    .await?;
454
455    // Configure onchain backend
456    let mint_builder =
457        configure_onchain_backend(settings, mint_builder, runtime, work_dir, kv_store).await?;
458
459    // Extract configured payment methods from mint_builder
460    let mint_info = mint_builder.current_mint_info();
461    let payment_methods = extract_supported_payment_methods(&mint_info);
462
463    // Enable batch minting by default for all supported methods
464    let mint_builder = mint_builder
465        .with_batch_minting(Some(DEFAULT_BATCH_MINT_SIZE), Some(payment_methods.clone()));
466
467    // Configure caching with payment methods
468    let mint_builder = configure_cache(settings, mint_builder, &payment_methods).await?;
469
470    // Configure transaction limits
471    let mint_builder =
472        mint_builder.with_limits(settings.limits.max_inputs, settings.limits.max_outputs);
473
474    // Verify at least one payment processor is configured
475    if mint_builder
476        .current_mint_info()
477        .nuts
478        .nut04
479        .methods
480        .is_empty()
481    {
482        bail!("At least one payment backend (Lightning or On-chain) must be configured");
483    }
484
485    Ok(mint_builder)
486}
487
488/// Configures basic mint information (name, contact info, descriptions, etc.)
489fn configure_basic_info(settings: &config::Settings, mint_builder: MintBuilder) -> MintBuilder {
490    // Add contact information
491    let mut contacts = Vec::new();
492    if let Some(nostr_key) = &settings.mint_info.contact_nostr_public_key {
493        if !nostr_key.is_empty() {
494            contacts.push(ContactInfo::new("nostr".to_string(), nostr_key.to_string()));
495        }
496    }
497    if let Some(email) = &settings.mint_info.contact_email {
498        if !email.is_empty() {
499            contacts.push(ContactInfo::new("email".to_string(), email.to_string()));
500        }
501    }
502
503    // Add version information
504    let mint_version = MintVersion::new(
505        "cdk-mintd".to_string(),
506        CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
507    );
508
509    // Configure mint builder with basic info
510    let mut builder = mint_builder.with_version(mint_version);
511
512    // Only set name if it's not empty
513    if !settings.mint_info.name.is_empty() {
514        builder = builder.with_name(settings.mint_info.name.clone());
515    }
516
517    // Only set description if it's not empty
518    if !settings.mint_info.description.is_empty() {
519        builder = builder.with_description(settings.mint_info.description.clone());
520    }
521
522    // Add optional information
523    if let Some(long_description) = &settings.mint_info.description_long {
524        if !long_description.is_empty() {
525            builder = builder.with_long_description(long_description.to_string());
526        }
527    }
528
529    for contact in contacts {
530        builder = builder.with_contact_info(contact);
531    }
532
533    if let Some(pubkey) = settings.mint_info.pubkey {
534        builder = builder.with_pubkey(pubkey);
535    }
536
537    if let Some(icon_url) = &settings.mint_info.icon_url {
538        if !icon_url.is_empty() {
539            builder = builder.with_icon_url(icon_url.to_string());
540        }
541    }
542
543    if let Some(motd) = &settings.mint_info.motd {
544        if !motd.is_empty() {
545            builder = builder.with_motd(motd.to_string());
546        }
547    }
548
549    if let Some(tos_url) = &settings.mint_info.tos_url {
550        if !tos_url.is_empty() {
551            builder = builder.with_tos_url(tos_url.to_string());
552        }
553    }
554
555    builder = builder.with_keyset_v2(settings.info.use_keyset_v2);
556
557    builder
558}
559/// Configures Lightning Network backend based on the specified backend type
560async fn configure_lightning_backend(
561    settings: &config::Settings,
562    mut mint_builder: MintBuilder,
563    _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
564    work_dir: &Path,
565    _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
566) -> Result<MintBuilder> {
567    if settings.ln.is_empty() {
568        tracing::info!("No Lightning backend configured");
569        return Ok(mint_builder);
570    }
571
572    #[cfg(feature = "fakewallet")]
573    let mut configure_fake_wallet_keyset_rotations = false;
574
575    for ln_entry in &settings.ln {
576        let mint_melt_limits = MintMeltLimits {
577            mint_min: ln_entry.min_mint,
578            mint_max: ln_entry.max_mint,
579            melt_min: ln_entry.min_melt,
580            melt_max: ln_entry.max_melt,
581        };
582
583        tracing::debug!(
584            "Ln backend: {:?} (unit: {:?})",
585            ln_entry.ln_backend,
586            ln_entry.unit
587        );
588
589        match ln_entry.ln_backend {
590            #[cfg(feature = "cln")]
591            LnBackend::Cln => {
592                let cln_settings = settings.cln.clone().ok_or_else(|| {
593                    anyhow!("CLN backend selected but [cln] config section is missing")
594                })?;
595                let cln = cln_settings
596                    .setup(
597                        settings,
598                        cdk::nuts::CurrencyUnit::Msat,
599                        None,
600                        work_dir,
601                        _kv_store.clone(),
602                    )
603                    .await?;
604                #[cfg(feature = "prometheus")]
605                let cln = MetricsMintPayment::new(cln);
606
607                mint_builder = configure_backend_for_unit(
608                    settings,
609                    mint_builder,
610                    ln_entry.unit.clone(),
611                    mint_melt_limits,
612                    Arc::new(cln),
613                )
614                .await?;
615            }
616            #[cfg(feature = "lnbits")]
617            LnBackend::LNbits => {
618                let lnbits_settings = settings.lnbits.clone().ok_or_else(|| {
619                    anyhow!("LNbits backend selected but [lnbits] config section is missing")
620                })?;
621                let lnbits = lnbits_settings
622                    .setup(settings, ln_entry.unit.clone(), None, work_dir, None)
623                    .await?;
624                #[cfg(feature = "prometheus")]
625                let lnbits = MetricsMintPayment::new(lnbits);
626
627                mint_builder = configure_backend_for_unit(
628                    settings,
629                    mint_builder,
630                    ln_entry.unit.clone(),
631                    mint_melt_limits,
632                    Arc::new(lnbits),
633                )
634                .await?;
635            }
636            #[cfg(feature = "lnd")]
637            LnBackend::Lnd => {
638                let lnd_settings = settings.lnd.clone().ok_or_else(|| {
639                    anyhow!("LND backend selected but [lnd] config section is missing")
640                })?;
641                let lnd = lnd_settings
642                    .setup(
643                        settings,
644                        cdk::nuts::CurrencyUnit::Msat,
645                        None,
646                        work_dir,
647                        _kv_store.clone(),
648                    )
649                    .await?;
650                #[cfg(feature = "prometheus")]
651                let lnd = MetricsMintPayment::new(lnd);
652
653                mint_builder = configure_backend_for_unit(
654                    settings,
655                    mint_builder,
656                    ln_entry.unit.clone(),
657                    mint_melt_limits,
658                    Arc::new(lnd),
659                )
660                .await?;
661            }
662            #[cfg(feature = "fakewallet")]
663            LnBackend::FakeWallet => {
664                let fake_wallet = settings.fake_wallet.clone().ok_or_else(|| {
665                    anyhow!(
666                        "Fake wallet backend selected but [fake_wallet] config section is missing"
667                    )
668                })?;
669                tracing::info!("Using fake wallet: {:?}", fake_wallet);
670
671                let fake = fake_wallet
672                    .setup(
673                        settings,
674                        ln_entry.unit.clone(),
675                        None,
676                        work_dir,
677                        _kv_store.clone(),
678                    )
679                    .await?;
680                #[cfg(feature = "prometheus")]
681                let fake = MetricsMintPayment::new(fake);
682
683                mint_builder = configure_backend_for_unit(
684                    settings,
685                    mint_builder,
686                    ln_entry.unit.clone(),
687                    mint_melt_limits,
688                    Arc::new(fake),
689                )
690                .await?;
691
692                configure_fake_wallet_keyset_rotations = true;
693            }
694            #[cfg(feature = "grpc-processor")]
695            LnBackend::GrpcProcessor => {
696                let grpc_processor = settings.grpc_processor.clone().ok_or_else(|| {
697                    anyhow!(
698                        "gRPC payment processor backend selected but [grpc_processor] config section is missing"
699                    )
700                })?;
701
702                tracing::info!(
703                    "Attempting to start with gRPC payment processor at {}:{}.",
704                    grpc_processor.addr,
705                    grpc_processor.port
706                );
707
708                let processor = grpc_processor
709                    .setup(settings, ln_entry.unit.clone(), None, work_dir, None)
710                    .await?;
711                #[cfg(feature = "prometheus")]
712                let processor = MetricsMintPayment::new(processor);
713
714                mint_builder = configure_backend_for_unit(
715                    settings,
716                    mint_builder,
717                    ln_entry.unit.clone(),
718                    mint_melt_limits,
719                    Arc::new(processor),
720                )
721                .await?;
722            }
723            #[cfg(feature = "ldk-node")]
724            LnBackend::LdkNode => {
725                let ldk_node_settings = settings.ldk_node.clone().ok_or_else(|| {
726                    anyhow!("LDK Node backend selected but [ldk_node] config section is missing")
727                })?;
728                tracing::info!("Using LDK Node backend: {:?}", ldk_node_settings);
729
730                let ldk_node = ldk_node_settings
731                    .setup(
732                        settings,
733                        ln_entry.unit.clone(),
734                        _runtime.clone(),
735                        work_dir,
736                        None,
737                    )
738                    .await?;
739
740                mint_builder = configure_backend_for_unit(
741                    settings,
742                    mint_builder,
743                    ln_entry.unit.clone(),
744                    mint_melt_limits,
745                    Arc::new(ldk_node),
746                )
747                .await?;
748            }
749            LnBackend::None => {
750                tracing::info!(
751                    "No Lightning backend configured for unit {:?}",
752                    ln_entry.unit
753                );
754            }
755        };
756    }
757
758    #[cfg(feature = "fakewallet")]
759    if configure_fake_wallet_keyset_rotations {
760        let fake_wallet = settings.fake_wallet.as_ref().ok_or_else(|| {
761            anyhow!("Fake wallet backend selected but [fake_wallet] config section is missing")
762        })?;
763        mint_builder = configure_fake_wallet_keyset_rotations_once(mint_builder, fake_wallet);
764    }
765
766    Ok(mint_builder)
767}
768
769#[cfg(feature = "fakewallet")]
770fn configure_fake_wallet_keyset_rotations_once(
771    mut mint_builder: MintBuilder,
772    fake_wallet: &config::FakeWallet,
773) -> MintBuilder {
774    for rotation_cfg in &fake_wallet.keyset_rotations {
775        use cdk::mint::KeysetRotation;
776
777        let amounts = cdk::mint::UnitConfig::default().amounts;
778        let final_expiry = if rotation_cfg.expired {
779            Some(cdk::util::unix_time().saturating_sub(3600))
780        } else {
781            None
782        };
783
784        mint_builder = mint_builder.with_keyset_rotation(KeysetRotation {
785            unit: rotation_cfg.unit.clone(),
786            amounts,
787            input_fee_ppk: rotation_cfg.input_fee_ppk,
788            use_keyset_v2: rotation_cfg.version == "v2",
789            final_expiry,
790        });
791    }
792
793    mint_builder
794}
795
796/// Configures Onchain backend based on the specified backend type
797async fn configure_onchain_backend(
798    settings: &config::Settings,
799    #[cfg_attr(not(feature = "bdk"), allow(unused_mut))] mut mint_builder: MintBuilder,
800    _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
801    _work_dir: &Path,
802    _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
803) -> Result<MintBuilder> {
804    use config::OnchainBackend;
805    #[cfg(feature = "bdk")]
806    use setup::OnchainBackendSetup;
807
808    if let Some(onchain_settings) = &settings.onchain {
809        match onchain_settings.onchain_backend {
810            #[cfg(feature = "bdk")]
811            OnchainBackend::Bdk => {
812                let mint_melt_limits = MintMeltLimits {
813                    mint_min: onchain_settings.min_mint,
814                    mint_max: onchain_settings.max_mint,
815                    melt_min: onchain_settings.min_melt,
816                    melt_max: onchain_settings.max_melt,
817                };
818
819                let bdk_settings = settings.bdk.clone().ok_or_else(|| {
820                    anyhow!("BDK onchain backend selected but [bdk] config section is missing")
821                })?;
822                let bdk = bdk_settings
823                    .setup(
824                        settings,
825                        cdk::nuts::CurrencyUnit::Sat,
826                        None,
827                        _work_dir,
828                        _kv_store,
829                    )
830                    .await?;
831                let bdk = Arc::new(bdk);
832
833                mint_builder = configure_backend_for_unit(
834                    settings,
835                    mint_builder,
836                    cdk::nuts::CurrencyUnit::Sat,
837                    mint_melt_limits,
838                    bdk,
839                )
840                .await?;
841            }
842            OnchainBackend::None => {}
843            #[cfg(feature = "fakewallet")]
844            OnchainBackend::FakeWallet => {
845                let has_lightning_backend = settings
846                    .ln
847                    .iter()
848                    .any(|ln| ln.ln_backend != LnBackend::None);
849                let has_real_ln_backend = settings
850                    .ln
851                    .iter()
852                    .any(|ln| !matches!(ln.ln_backend, LnBackend::None | LnBackend::FakeWallet));
853
854                if !has_lightning_backend {
855                    let mint_melt_limits = MintMeltLimits {
856                        mint_min: onchain_settings.min_mint,
857                        mint_max: onchain_settings.max_mint,
858                        melt_min: onchain_settings.min_melt,
859                        melt_max: onchain_settings.max_melt,
860                    };
861                    let fake_wallet = settings
862                        .fake_wallet
863                        .clone()
864                        .ok_or_else(|| anyhow!("Fake wallet config section is missing"))?;
865
866                    for unit in fake_wallet.clone().supported_units {
867                        let fake = fake_wallet
868                            .setup(settings, unit.clone(), None, _work_dir, _kv_store.clone())
869                            .await?;
870                        #[cfg(feature = "prometheus")]
871                        let fake = MetricsMintPayment::new(fake);
872
873                        mint_builder = configure_backend_for_methods(
874                            settings,
875                            mint_builder,
876                            unit,
877                            mint_melt_limits,
878                            Arc::new(fake),
879                            vec![PaymentMethod::Known(KnownMethod::Onchain)],
880                        )
881                        .await?;
882                    }
883                } else if has_real_ln_backend {
884                    bail!(
885                        "onchain_backend = \"fakewallet\" cannot be combined with a real Lightning backend"
886                    );
887                }
888            }
889        }
890    }
891
892    Ok(mint_builder)
893}
894
895/// Helper function to configure a mint builder with a lightning backend for a specific currency unit
896async fn configure_backend_for_unit(
897    settings: &config::Settings,
898    mint_builder: MintBuilder,
899    unit: cdk::nuts::CurrencyUnit,
900    mint_melt_limits: MintMeltLimits,
901    backend: Arc<dyn MintPayment<Err = cdk_common::payment::Error> + Send + Sync>,
902) -> Result<MintBuilder> {
903    let payment_settings = backend.get_settings().await?;
904    validate_backend_unit(&unit, &payment_settings.unit)?;
905
906    let mut methods = Vec::new();
907
908    // Add bolt11 if supported by payment processor
909    if payment_settings.bolt11.is_some() {
910        methods.push(PaymentMethod::Known(KnownMethod::Bolt11));
911    }
912
913    // Add bolt12 if supported by payment processor
914    if payment_settings.bolt12.is_some() {
915        methods.push(PaymentMethod::Known(KnownMethod::Bolt12));
916    }
917
918    // Add onchain if supported by payment processor
919    if payment_settings.onchain.is_some() {
920        methods.push(PaymentMethod::Known(KnownMethod::Onchain));
921    }
922
923    // Add custom methods from payment settings
924    for method_name in payment_settings.custom.keys() {
925        methods.push(PaymentMethod::from(method_name.as_str()));
926    }
927
928    configure_backend_for_methods(
929        settings,
930        mint_builder,
931        unit,
932        mint_melt_limits,
933        backend,
934        methods,
935    )
936    .await
937}
938
939async fn configure_backend_for_methods(
940    settings: &config::Settings,
941    mut mint_builder: MintBuilder,
942    unit: cdk::nuts::CurrencyUnit,
943    mint_melt_limits: MintMeltLimits,
944    backend: Arc<dyn MintPayment<Err = cdk_common::payment::Error> + Send + Sync>,
945    methods: Vec<PaymentMethod>,
946) -> Result<MintBuilder> {
947    // Add all supported payment methods to the mint builder
948    for method in &methods {
949        mint_builder
950            .add_payment_processor(
951                unit.clone(),
952                method.clone(),
953                mint_melt_limits,
954                backend.clone(),
955            )
956            .await?;
957    }
958
959    // Configure NUT17 (WebSocket support) for all payment methods
960    for method in &methods {
961        let method_str = method.to_string();
962        let nut17_supported = match method_str.as_str() {
963            "bolt11" => SupportedMethods::default_bolt11(unit.clone()),
964            "bolt12" => SupportedMethods::default_bolt12(unit.clone()),
965            _ => SupportedMethods::default_custom(method.clone(), unit.clone()),
966        };
967        mint_builder = mint_builder.with_supported_websockets(nut17_supported);
968    }
969
970    if let Some(input_fee) = settings.info.input_fee_ppk {
971        mint_builder.set_unit_fee(&unit, input_fee)?;
972    }
973
974    Ok(mint_builder)
975}
976
977fn validate_backend_unit(
978    configured_unit: &cdk::nuts::CurrencyUnit,
979    backend_unit: &str,
980) -> Result<()> {
981    let backend_unit = cdk::nuts::CurrencyUnit::from_str(backend_unit)
982        .with_context(|| format!("Payment backend returned invalid unit `{backend_unit}`"))?;
983
984    if units_are_compatible(&backend_unit, configured_unit) {
985        return Ok(());
986    }
987
988    bail!(
989        "Payment backend reports unit {} but config registers unit {}; only matching units or sat/msat conversions are supported",
990        backend_unit,
991        configured_unit
992    )
993}
994
995fn units_are_compatible(
996    backend_unit: &cdk::nuts::CurrencyUnit,
997    configured_unit: &cdk::nuts::CurrencyUnit,
998) -> bool {
999    backend_unit == configured_unit
1000        || matches!(
1001            (backend_unit, configured_unit),
1002            (cdk::nuts::CurrencyUnit::Sat, cdk::nuts::CurrencyUnit::Msat)
1003                | (cdk::nuts::CurrencyUnit::Msat, cdk::nuts::CurrencyUnit::Sat)
1004        )
1005}
1006
1007/// Configures cache settings with support for custom payment methods
1008async fn configure_cache(
1009    settings: &config::Settings,
1010    mint_builder: MintBuilder,
1011    payment_methods: &[String],
1012) -> Result<MintBuilder> {
1013    let mut cached_endpoints = vec![
1014        // Always include swap endpoint
1015        CachedEndpoint::new(NUT19Method::Post, NUT19Path::Swap),
1016    ];
1017
1018    // Add cache endpoints for each configured payment method
1019    for method in payment_methods {
1020        // All payment methods (including bolt11, bolt12) use custom paths now
1021        cached_endpoints.push(CachedEndpoint::new(
1022            NUT19Method::Post,
1023            NUT19Path::custom_mint(method),
1024        ));
1025        cached_endpoints.push(CachedEndpoint::new(
1026            NUT19Method::Post,
1027            NUT19Path::custom_melt(method),
1028        ));
1029    }
1030
1031    let cache: HttpCache = HttpCache::from_config(settings.info.http_cache.clone()).await?;
1032    Ok(mint_builder.with_cache(Some(cache.ttl.as_secs()), cached_endpoints))
1033}
1034
1035async fn setup_authentication(
1036    settings: &config::Settings,
1037    _work_dir: &Path,
1038    mut mint_builder: MintBuilder,
1039    _password: Option<String>,
1040) -> Result<(
1041    MintBuilder,
1042    Option<cdk_common::database::DynMintAuthDatabase>,
1043)> {
1044    if let Some(auth_settings) = settings.auth.clone() {
1045        use cdk_common::database::DynMintAuthDatabase;
1046
1047        tracing::info!("Auth settings are defined. {:?}", auth_settings);
1048        let auth_localstore: DynMintAuthDatabase = match settings.database.engine {
1049            #[cfg(feature = "sqlite")]
1050            DatabaseEngine::Sqlite => {
1051                #[cfg(feature = "sqlite")]
1052                {
1053                    let sql_db_path = _work_dir.join("cdk-mintd-auth.sqlite");
1054                    #[cfg(not(feature = "sqlcipher"))]
1055                    let sqlite_db = MintSqliteAuthDatabase::new(&sql_db_path).await?;
1056                    #[cfg(feature = "sqlcipher")]
1057                    let sqlite_db = {
1058                        // Get password from command line arguments for sqlcipher
1059                        let password = _password.clone().ok_or_else(|| {
1060                            anyhow!("Password required when sqlcipher feature is enabled")
1061                        })?;
1062                        MintSqliteAuthDatabase::new((sql_db_path, password)).await?
1063                    };
1064
1065                    Arc::new(sqlite_db)
1066                }
1067                #[cfg(not(feature = "sqlite"))]
1068                {
1069                    bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
1070                }
1071            }
1072            #[cfg(feature = "postgres")]
1073            DatabaseEngine::Postgres => {
1074                #[cfg(feature = "postgres")]
1075                {
1076                    // Require dedicated auth database configuration - no fallback to main database
1077                    let auth_db_config = settings.auth_database.as_ref().ok_or_else(|| {
1078                        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")
1079                    })?;
1080
1081                    let auth_pg_config = auth_db_config.postgres.as_ref().ok_or_else(|| {
1082                        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")
1083                    })?;
1084
1085                    if auth_pg_config.url.is_empty() {
1086                        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");
1087                    }
1088
1089                    let auth_db_config = PgConfig::new(
1090                        auth_pg_config.url.as_str(),
1091                        auth_pg_config.tls_mode.as_deref(),
1092                        auth_pg_config.max_connections,
1093                        auth_pg_config.connection_timeout_seconds,
1094                    );
1095                    Arc::new(MintPgAuthDatabase::new(auth_db_config).await?)
1096                }
1097                #[cfg(not(feature = "postgres"))]
1098                {
1099                    bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
1100                }
1101            }
1102            #[cfg(not(feature = "sqlite"))]
1103            DatabaseEngine::Sqlite => {
1104                bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
1105            }
1106            #[cfg(not(feature = "postgres"))]
1107            DatabaseEngine::Postgres => {
1108                bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
1109            }
1110        };
1111
1112        let mut protected_endpoints = HashMap::new();
1113        let mut blind_auth_endpoints = vec![];
1114        let mut clear_auth_endpoints = vec![];
1115        let mut unprotected_endpoints = vec![];
1116
1117        let mint_blind_auth_endpoint =
1118            ProtectedEndpoint::new(Method::Post, RoutePath::MintBlindAuth);
1119
1120        protected_endpoints.insert(mint_blind_auth_endpoint.clone(), AuthRequired::Clear);
1121
1122        clear_auth_endpoints.push(mint_blind_auth_endpoint);
1123
1124        // Helper function to add endpoint based on auth type
1125        let mut add_endpoint = |endpoint: ProtectedEndpoint, auth_type: &AuthType| {
1126            match auth_type {
1127                AuthType::Blind => {
1128                    protected_endpoints.insert(endpoint.clone(), AuthRequired::Blind);
1129                    blind_auth_endpoints.push(endpoint);
1130                }
1131                AuthType::Clear => {
1132                    protected_endpoints.insert(endpoint.clone(), AuthRequired::Clear);
1133                    clear_auth_endpoints.push(endpoint);
1134                }
1135                AuthType::None => {
1136                    unprotected_endpoints.push(endpoint);
1137                }
1138            };
1139        };
1140
1141        // Payment method endpoints (bolt11, bolt12, custom) will be added dynamically
1142        // after the mint is built and we can query the payment processors for their
1143        // supported methods. See the start_services_with_shutdown function where we
1144        // add auth endpoints for all configured payment methods.
1145
1146        // Swap endpoint
1147        {
1148            let swap_protected_endpoint = ProtectedEndpoint::new(Method::Post, RoutePath::Swap);
1149            add_endpoint(swap_protected_endpoint, &auth_settings.swap);
1150        }
1151
1152        // Restore endpoint
1153        {
1154            let restore_protected_endpoint =
1155                ProtectedEndpoint::new(Method::Post, RoutePath::Restore);
1156            add_endpoint(restore_protected_endpoint, &auth_settings.restore);
1157        }
1158
1159        // Check proof state endpoint
1160        {
1161            let state_protected_endpoint =
1162                ProtectedEndpoint::new(Method::Post, RoutePath::Checkstate);
1163            add_endpoint(state_protected_endpoint, &auth_settings.check_proof_state);
1164        }
1165
1166        // Ws endpoint
1167        {
1168            let ws_protected_endpoint = ProtectedEndpoint::new(Method::Get, RoutePath::Ws);
1169            add_endpoint(ws_protected_endpoint, &auth_settings.websocket_auth);
1170        }
1171
1172        // Custom protected_endpoints will be added dynamically after the mint is built
1173        // and we can query the payment processors for their supported methods.
1174        // For now, we don't add any custom endpoints here - they'll be added in the
1175        // start_services_with_shutdown function after we have access to the mint instance.
1176
1177        mint_builder = mint_builder.with_auth(
1178            auth_localstore.clone(),
1179            auth_settings.openid_discovery,
1180            auth_settings.openid_client_id,
1181            clear_auth_endpoints,
1182        );
1183        mint_builder =
1184            mint_builder.with_blind_auth(auth_settings.mint_max_bat, blind_auth_endpoints);
1185
1186        let mut tx = auth_localstore.begin_transaction().await?;
1187
1188        if !unprotected_endpoints.is_empty() {
1189            tx.remove_protected_endpoints(unprotected_endpoints).await?;
1190        }
1191        if !protected_endpoints.is_empty() {
1192            tx.add_protected_endpoints(protected_endpoints).await?;
1193        }
1194        tx.commit().await?;
1195
1196        Ok((mint_builder, Some(auth_localstore)))
1197    } else {
1198        Ok((mint_builder, None))
1199    }
1200}
1201
1202/// Build mints with the configured the signing method (remote signatory or local seed)
1203async fn build_mint(
1204    settings: &config::Settings,
1205    keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
1206    mint_builder: MintBuilder,
1207) -> Result<Mint> {
1208    if let Some(signatory_url) = settings.info.signatory_url.clone() {
1209        tracing::info!(
1210            "Connecting to remote signatory to {} with certs {:?}",
1211            signatory_url,
1212            settings.info.signatory_certs.clone()
1213        );
1214
1215        Ok(mint_builder
1216            .build_with_signatory(Arc::new(
1217                cdk_signatory::SignatoryRpcClient::new(
1218                    signatory_url,
1219                    settings.info.signatory_certs.clone(),
1220                )
1221                .await?,
1222            ))
1223            .await?)
1224    } else if let Some(seed) = settings.info.seed.clone() {
1225        let seed_bytes: Vec<u8> = seed.into();
1226        Ok(mint_builder.build_with_seed(keystore, &seed_bytes).await?)
1227    } else if let Some(mnemonic) = settings
1228        .info
1229        .mnemonic
1230        .clone()
1231        .map(|s| Mnemonic::from_str(&s))
1232        .transpose()?
1233    {
1234        Ok(mint_builder
1235            .build_with_seed(keystore, &mnemonic.to_seed_normalized(""))
1236            .await?)
1237    } else {
1238        bail!("No seed nor remote signatory set");
1239    }
1240}
1241
1242async fn start_services_with_shutdown(
1243    mint: Arc<cdk::mint::Mint>,
1244    settings: &config::Settings,
1245    _work_dir: &Path,
1246    mint_builder_info: cdk::nuts::MintInfo,
1247    shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
1248    routers: Vec<Router>,
1249    auth_localstore: Option<cdk_common::database::DynMintAuthDatabase>,
1250) -> Result<()> {
1251    let listen_addr = settings.info.listen_host.clone();
1252    let listen_port = settings.info.listen_port;
1253    let cache: HttpCache = HttpCache::from_config(settings.info.http_cache.clone()).await?;
1254
1255    #[cfg(feature = "management-rpc")]
1256    let mut rpc_enabled = false;
1257    #[cfg(not(feature = "management-rpc"))]
1258    let rpc_enabled = false;
1259
1260    #[cfg(feature = "management-rpc")]
1261    let mut rpc_server: Option<cdk_mint_rpc::MintRPCServer> = None;
1262
1263    #[cfg(feature = "management-rpc")]
1264    {
1265        if let Some(rpc_settings) = settings.mint_management_rpc.clone() {
1266            if rpc_settings.enabled {
1267                let addr = rpc_settings.address.unwrap_or("127.0.0.1".to_string());
1268                let port = rpc_settings.port.unwrap_or(8086);
1269                let mut mint_rpc = cdk_mint_rpc::MintRPCServer::new(&addr, port, mint.clone())?;
1270
1271                let tls_dir = rpc_settings.tls_dir_path.unwrap_or(_work_dir.join("tls"));
1272
1273                let tls_dir = if tls_dir.exists() {
1274                    Some(tls_dir)
1275                } else {
1276                    tracing::warn!(
1277                        "TLS directory does not exist: {}. Starting RPC server in INSECURE mode without TLS encryption",
1278                        tls_dir.display()
1279                    );
1280                    None
1281                };
1282
1283                mint_rpc.start(tls_dir).await?;
1284
1285                rpc_server = Some(mint_rpc);
1286
1287                rpc_enabled = true;
1288            }
1289        }
1290    }
1291
1292    // Determine the desired QuoteTTL from config/env or fall back to defaults
1293    let desired_quote_ttl: QuoteTTL = settings.info.quote_ttl.unwrap_or_default();
1294
1295    if rpc_enabled {
1296        if mint.mint_info().await.is_err() {
1297            tracing::info!("Mint info not set on mint, setting.");
1298            // First boot with RPC enabled: seed from config
1299            mint.set_mint_info(mint_builder_info).await?;
1300            mint.set_quote_ttl(desired_quote_ttl).await?;
1301        } else {
1302            // If QuoteTTL has never been persisted, seed it now from config
1303            if !mint.quote_ttl_is_persisted().await? {
1304                mint.set_quote_ttl(desired_quote_ttl).await?;
1305            }
1306            // Add/refresh version information without altering stored mint_info fields
1307            let mint_version = MintVersion::new(
1308                "cdk-mintd".to_string(),
1309                CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
1310            );
1311            let mut stored_mint_info = mint.mint_info().await?;
1312            stored_mint_info.version = Some(mint_version);
1313            mint.set_mint_info(stored_mint_info).await?;
1314
1315            tracing::info!("Mint info already set, not using config file settings.");
1316        }
1317    } else {
1318        // RPC disabled: config is source of truth on every boot
1319        tracing::info!("RPC not enabled, using mint info and quote TTL from config.");
1320        let mut mint_builder_info = mint_builder_info;
1321
1322        if let Ok(mint_info) = mint.mint_info().await {
1323            if mint_builder_info.pubkey.is_none() {
1324                mint_builder_info.pubkey = mint_info.pubkey;
1325            }
1326        }
1327
1328        mint.set_mint_info(mint_builder_info).await?;
1329        mint.set_quote_ttl(desired_quote_ttl).await?;
1330    }
1331
1332    let mint_info = mint.mint_info().await?;
1333    let nut04_methods = mint_info.nuts.nut04.supported_methods();
1334    let nut05_methods = mint_info.nuts.nut05.supported_methods();
1335
1336    // Get custom payment methods from payment processors
1337    let mut custom_methods = mint.get_custom_payment_methods().await?;
1338
1339    // Add bolt11 if it's supported by any payment processor
1340    let bolt11_method = PaymentMethod::Known(KnownMethod::Bolt11);
1341    let bolt11_supported =
1342        nut04_methods.contains(&&bolt11_method) || nut05_methods.contains(&&bolt11_method);
1343    // Add bolt12 if it's supported by any payment processor
1344    let bolt12_method = PaymentMethod::Known(KnownMethod::Bolt12);
1345    let bolt12_supported =
1346        nut04_methods.contains(&&bolt12_method) || nut05_methods.contains(&&bolt12_method);
1347
1348    // Add onchain if it's supported by any payment processor
1349    let onchain_method = PaymentMethod::Known(KnownMethod::Onchain);
1350    let onchain_supported =
1351        nut04_methods.contains(&&onchain_method) || nut05_methods.contains(&&onchain_method);
1352
1353    if bolt11_supported
1354        && !custom_methods.contains(&PaymentMethod::Known(KnownMethod::Bolt11).to_string())
1355    {
1356        custom_methods.push(PaymentMethod::Known(KnownMethod::Bolt11).to_string());
1357    }
1358    if bolt12_supported
1359        && !custom_methods.contains(&PaymentMethod::Known(KnownMethod::Bolt12).to_string())
1360    {
1361        custom_methods.push(PaymentMethod::Known(KnownMethod::Bolt12).to_string());
1362    }
1363    if onchain_supported
1364        && !custom_methods.contains(&PaymentMethod::Known(KnownMethod::Onchain).to_string())
1365    {
1366        custom_methods.push(PaymentMethod::Known(KnownMethod::Onchain).to_string());
1367    }
1368
1369    tracing::info!("Payment methods: {:?}", custom_methods);
1370
1371    // Configure auth for custom payment methods if auth is enabled
1372    if let (Some(ref auth_settings), Some(auth_db)) = (&settings.auth, &auth_localstore) {
1373        if auth_settings.auth_enabled {
1374            use std::collections::HashMap;
1375
1376            use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
1377            use cdk::nuts::AuthRequired;
1378
1379            use crate::config::AuthType;
1380
1381            // First, remove all existing payment-method-related endpoints from the database
1382            // to ensure old payment methods don't persist when configuration changes
1383            let existing_endpoints = auth_db.get_auth_for_endpoints().await?;
1384            let payment_method_endpoints_to_remove: Vec<ProtectedEndpoint> = existing_endpoints
1385                .keys()
1386                .filter(|endpoint| {
1387                    matches!(
1388                        endpoint.path,
1389                        RoutePath::MintQuote(_)
1390                            | RoutePath::Mint(_)
1391                            | RoutePath::MeltQuote(_)
1392                            | RoutePath::Melt(_)
1393                    )
1394                })
1395                .cloned()
1396                .collect();
1397
1398            if !payment_method_endpoints_to_remove.is_empty() {
1399                tracing::debug!(
1400                    "Removing {} old payment method endpoints from database",
1401                    payment_method_endpoints_to_remove.len()
1402                );
1403                let mut tx = auth_db.begin_transaction().await?;
1404                tx.remove_protected_endpoints(payment_method_endpoints_to_remove)
1405                    .await?;
1406                tx.commit().await?;
1407            }
1408
1409            // Now add endpoints for current payment methods
1410            if !custom_methods.is_empty() {
1411                let mut protected_endpoints = HashMap::new();
1412
1413                for method_name in &custom_methods {
1414                    tracing::debug!("Adding auth endpoints for payment method: {}", method_name);
1415
1416                    // Determine auth type based on settings
1417                    let mint_quote_auth = match auth_settings.get_mint_quote {
1418                        AuthType::Clear => Some(AuthRequired::Clear),
1419                        AuthType::Blind => Some(AuthRequired::Blind),
1420                        AuthType::None => None,
1421                    };
1422
1423                    let check_mint_quote_auth = match auth_settings.check_mint_quote {
1424                        AuthType::Clear => Some(AuthRequired::Clear),
1425                        AuthType::Blind => Some(AuthRequired::Blind),
1426                        AuthType::None => None,
1427                    };
1428
1429                    let mint_auth = match auth_settings.mint {
1430                        AuthType::Clear => Some(AuthRequired::Clear),
1431                        AuthType::Blind => Some(AuthRequired::Blind),
1432                        AuthType::None => None,
1433                    };
1434
1435                    let melt_quote_auth = match auth_settings.get_melt_quote {
1436                        AuthType::Clear => Some(AuthRequired::Clear),
1437                        AuthType::Blind => Some(AuthRequired::Blind),
1438                        AuthType::None => None,
1439                    };
1440
1441                    let check_melt_quote_auth = match auth_settings.check_melt_quote {
1442                        AuthType::Clear => Some(AuthRequired::Clear),
1443                        AuthType::Blind => Some(AuthRequired::Blind),
1444                        AuthType::None => None,
1445                    };
1446
1447                    let melt_auth = match auth_settings.melt {
1448                        AuthType::Clear => Some(AuthRequired::Clear),
1449                        AuthType::Blind => Some(AuthRequired::Blind),
1450                        AuthType::None => None,
1451                    };
1452
1453                    // Create endpoints for each payment method operation
1454                    if let Some(auth) = mint_quote_auth {
1455                        protected_endpoints.insert(
1456                            ProtectedEndpoint::new(
1457                                Method::Post,
1458                                RoutePath::MintQuote(method_name.clone()),
1459                            ),
1460                            auth,
1461                        );
1462                    }
1463                    if let Some(auth) = check_mint_quote_auth {
1464                        protected_endpoints.insert(
1465                            ProtectedEndpoint::new(
1466                                Method::Get,
1467                                RoutePath::MintQuote(method_name.clone()),
1468                            ),
1469                            auth,
1470                        );
1471                    }
1472                    if let Some(auth) = mint_auth {
1473                        protected_endpoints.insert(
1474                            ProtectedEndpoint::new(
1475                                Method::Post,
1476                                RoutePath::Mint(method_name.clone()),
1477                            ),
1478                            auth,
1479                        );
1480                    }
1481                    if let Some(auth) = melt_quote_auth {
1482                        protected_endpoints.insert(
1483                            ProtectedEndpoint::new(
1484                                Method::Post,
1485                                RoutePath::MeltQuote(method_name.clone()),
1486                            ),
1487                            auth,
1488                        );
1489                    }
1490                    if let Some(auth) = check_melt_quote_auth {
1491                        protected_endpoints.insert(
1492                            ProtectedEndpoint::new(
1493                                Method::Get,
1494                                RoutePath::MeltQuote(method_name.clone()),
1495                            ),
1496                            auth,
1497                        );
1498                    }
1499                    if let Some(auth) = melt_auth {
1500                        protected_endpoints.insert(
1501                            ProtectedEndpoint::new(
1502                                Method::Post,
1503                                RoutePath::Melt(method_name.clone()),
1504                            ),
1505                            auth,
1506                        );
1507                    }
1508                }
1509
1510                // Add all custom endpoints in one transaction
1511                if !protected_endpoints.is_empty() {
1512                    let mut tx = auth_db.begin_transaction().await?;
1513                    tx.add_protected_endpoints(protected_endpoints).await?;
1514                    tx.commit().await?;
1515                }
1516            }
1517        }
1518    }
1519
1520    let v1_service = cdk_axum::create_mint_router_with_custom_cache(
1521        Arc::clone(&mint),
1522        cache,
1523        custom_methods,
1524        settings.info.enable_info_page.unwrap_or(true),
1525    )
1526    .await?;
1527
1528    let mut mint_service = Router::new()
1529        .merge(v1_service)
1530        .layer(DefaultBodyLimit::max(REQUEST_BODY_LIMIT_BYTES))
1531        .layer(
1532            ServiceBuilder::new()
1533                .layer(RequestDecompressionLayer::new())
1534                .layer(CompressionLayer::new()),
1535        )
1536        .layer(TraceLayer::new_for_http());
1537
1538    for router in routers {
1539        mint_service = mint_service.merge(router);
1540    }
1541
1542    // Create a broadcast channel to share shutdown signal between services
1543    let (shutdown_tx, _) = tokio::sync::broadcast::channel::<()>(1);
1544
1545    // Start Prometheus server if enabled
1546    #[cfg(feature = "prometheus")]
1547    let prometheus_handle = {
1548        if let Some(prometheus_settings) = &settings.prometheus {
1549            if prometheus_settings.enabled {
1550                let addr = prometheus_settings
1551                    .address
1552                    .clone()
1553                    .unwrap_or("127.0.0.1".to_string());
1554                let port = prometheus_settings.port.unwrap_or(9000);
1555
1556                let address = format!("{}:{}", addr, port)
1557                    .parse()
1558                    .expect("Invalid prometheus address");
1559
1560                let server = cdk_prometheus::PrometheusBuilder::new()
1561                    .bind_address(address)
1562                    .build_with_cdk_metrics()?;
1563
1564                let mut shutdown_rx = shutdown_tx.subscribe();
1565                let prometheus_shutdown = async move {
1566                    let _ = shutdown_rx.recv().await;
1567                };
1568
1569                Some(tokio::spawn(async move {
1570                    if let Err(e) = server.start(prometheus_shutdown).await {
1571                        tracing::error!("Failed to start prometheus server: {}", e);
1572                    }
1573                }))
1574            } else {
1575                None
1576            }
1577        } else {
1578            None
1579        }
1580    };
1581
1582    mint.start().await?;
1583
1584    let socket_addr = SocketAddr::from_str(&format!("{listen_addr}:{listen_port}"))?;
1585
1586    let listener = tokio::net::TcpListener::bind(socket_addr).await?;
1587
1588    tracing::info!("listening on {}", listener.local_addr()?);
1589
1590    // Create a task to wait for the shutdown signal and broadcast it
1591    let shutdown_broadcast_task = {
1592        let shutdown_tx = shutdown_tx.clone();
1593        tokio::spawn(async move {
1594            shutdown_signal.await;
1595            tracing::info!("Shutdown signal received, broadcasting to all services");
1596            let _ = shutdown_tx.send(());
1597        })
1598    };
1599
1600    // Create shutdown future for axum server
1601    let mut axum_shutdown_rx = shutdown_tx.subscribe();
1602    let axum_shutdown = async move {
1603        let _ = axum_shutdown_rx.recv().await;
1604    };
1605
1606    // Wait for axum server to complete with custom shutdown signal
1607    let axum_result = axum::serve(listener, mint_service).with_graceful_shutdown(axum_shutdown);
1608
1609    match axum_result.await {
1610        Ok(_) => {
1611            tracing::info!("Axum server stopped with okay status");
1612        }
1613        Err(err) => {
1614            tracing::warn!("Axum server stopped with error");
1615            tracing::error!("{}", err);
1616            bail!("Axum exited with error")
1617        }
1618    }
1619
1620    // Wait for the shutdown broadcast task to complete
1621    let _ = shutdown_broadcast_task.await;
1622
1623    // Wait for prometheus server to shutdown if it was started
1624    #[cfg(feature = "prometheus")]
1625    if let Some(handle) = prometheus_handle {
1626        if let Err(e) = handle.await {
1627            tracing::warn!("Prometheus server task failed: {}", e);
1628        }
1629    }
1630
1631    mint.stop().await?;
1632
1633    #[cfg(feature = "management-rpc")]
1634    {
1635        if let Some(rpc_server) = rpc_server {
1636            rpc_server.stop().await?;
1637        }
1638    }
1639
1640    Ok(())
1641}
1642
1643async fn shutdown_signal() {
1644    tokio::signal::ctrl_c()
1645        .await
1646        .expect("failed to install CTRL+C handler");
1647    tracing::info!("Shutdown signal received");
1648}
1649
1650fn work_dir() -> Result<PathBuf> {
1651    let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?;
1652    let dir = home_dir.join(".cdk-mintd");
1653
1654    std::fs::create_dir_all(&dir)?;
1655
1656    Ok(dir)
1657}
1658
1659/// The main entry point for the application when used as a library
1660pub async fn run_mintd(
1661    work_dir: &Path,
1662    settings: &config::Settings,
1663    db_password: Option<String>,
1664    enable_logging: bool,
1665    runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1666    routers: Vec<Router>,
1667) -> Result<()> {
1668    let _guard = if enable_logging {
1669        setup_tracing(work_dir, &settings.info.logging)?
1670    } else {
1671        None
1672    };
1673
1674    let result = run_mintd_with_shutdown(
1675        work_dir,
1676        settings,
1677        shutdown_signal(),
1678        db_password,
1679        runtime,
1680        routers,
1681    )
1682    .await;
1683
1684    // Explicitly drop the guard to ensure proper cleanup
1685    if let Some(guard) = _guard {
1686        tracing::info!("Shutting down logging worker thread");
1687        drop(guard);
1688        // Give the worker thread a moment to flush any remaining logs
1689        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
1690    }
1691
1692    tracing::info!("Mintd shutdown");
1693
1694    result
1695}
1696
1697/// Run mintd with a custom shutdown signal
1698pub async fn run_mintd_with_shutdown(
1699    work_dir: &Path,
1700    settings: &config::Settings,
1701    shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
1702    db_password: Option<String>,
1703    runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1704    routers: Vec<Router>,
1705) -> Result<()> {
1706    let (localstore, keystore, kv) = initial_setup(work_dir, settings, db_password.clone()).await?;
1707
1708    let mint_builder = MintBuilder::new(localstore);
1709
1710    // If RPC is enabled and DB contains mint_info already, initialize the builder from DB.
1711    // This ensures subsequent builder modifications (like version injection) can respect stored values.
1712    let maybe_mint_builder = {
1713        #[cfg(feature = "management-rpc")]
1714        {
1715            if let Some(rpc_settings) = settings.mint_management_rpc.clone() {
1716                if rpc_settings.enabled {
1717                    // Best-effort: pull DB state into builder if present
1718                    let mut tmp = mint_builder;
1719                    if let Err(e) = tmp.init_from_db_if_present().await {
1720                        tracing::warn!("Failed to init builder from DB: {}", e);
1721                    }
1722                    tmp
1723                } else {
1724                    mint_builder
1725                }
1726            } else {
1727                mint_builder
1728            }
1729        }
1730        #[cfg(not(feature = "management-rpc"))]
1731        {
1732            mint_builder
1733        }
1734    };
1735
1736    let mint_builder =
1737        configure_mint_builder(settings, maybe_mint_builder, runtime, work_dir, Some(kv)).await?;
1738    let (mint_builder, auth_localstore) =
1739        setup_authentication(settings, work_dir, mint_builder, db_password).await?;
1740
1741    let config_mint_info = mint_builder.current_mint_info();
1742
1743    let mint = build_mint(settings, keystore, mint_builder).await?;
1744
1745    tracing::debug!("Mint built from builder.");
1746
1747    let mint = Arc::new(mint);
1748
1749    start_services_with_shutdown(
1750        mint.clone(),
1751        settings,
1752        work_dir,
1753        config_mint_info,
1754        shutdown_signal,
1755        routers,
1756        auth_localstore,
1757    )
1758    .await
1759}
1760
1761#[cfg(test)]
1762mod tests {
1763    use std::fs;
1764
1765    use cdk::nuts::{CurrencyUnit, MintMethodSettings, PaymentMethod};
1766
1767    use super::*;
1768
1769    const TEST_MNEMONIC: &str =
1770        "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
1771
1772    fn temp_seed_file(name: &str) -> PathBuf {
1773        std::env::temp_dir().join(format!("cdk_mintd_{name}_{}", std::process::id()))
1774    }
1775
1776    #[test]
1777    fn apply_seed_file_sets_mint_mnemonic_from_trimmed_file_contents() {
1778        let seed_file = temp_seed_file("seed_file_sets_seed");
1779        fs::write(&seed_file, format!("  {TEST_MNEMONIC}\n")).expect("seed file should be written");
1780        let mut settings = config::Settings {
1781            info: config::Info {
1782                seed: Some("raw seed from config".to_string()),
1783                mnemonic: Some("mnemonic from config".to_string()),
1784                signatory_url: Some("http://127.0.0.1:50051".to_string()),
1785                signatory_certs: Some("/tmp/certs".to_string()),
1786                ..Default::default()
1787            },
1788            ..Default::default()
1789        };
1790
1791        apply_seed_file(&mut settings, &seed_file).expect("seed file should be applied");
1792
1793        assert_eq!(settings.info.seed, None);
1794        assert_eq!(settings.info.mnemonic, Some(TEST_MNEMONIC.to_string()));
1795        assert_eq!(
1796            settings.info.signatory_url,
1797            Some("http://127.0.0.1:50051".to_string())
1798        );
1799        assert_eq!(
1800            settings.info.signatory_certs,
1801            Some("/tmp/certs".to_string())
1802        );
1803
1804        let _ = fs::remove_file(&seed_file);
1805    }
1806
1807    #[cfg(feature = "bdk")]
1808    #[test]
1809    fn apply_seed_file_sets_active_bdk_mnemonic() {
1810        use crate::config::{Bdk, Onchain, OnchainBackend};
1811
1812        let seed_file = temp_seed_file("seed_file_sets_bdk_seed");
1813        fs::write(&seed_file, TEST_MNEMONIC).expect("seed file should be written");
1814        let mut settings = config::Settings {
1815            onchain: Some(Onchain {
1816                onchain_backend: OnchainBackend::Bdk,
1817                ..Default::default()
1818            }),
1819            bdk: Some(Bdk {
1820                mnemonic: Some("old bdk mnemonic".to_string()),
1821                ..Default::default()
1822            }),
1823            ..Default::default()
1824        };
1825
1826        apply_seed_file(&mut settings, &seed_file).expect("seed file should be applied");
1827
1828        assert_eq!(
1829            settings
1830                .bdk
1831                .expect("bdk settings should be present")
1832                .mnemonic,
1833            Some(TEST_MNEMONIC.to_string())
1834        );
1835
1836        let _ = fs::remove_file(&seed_file);
1837    }
1838
1839    #[cfg(feature = "ldk-node")]
1840    #[test]
1841    fn apply_seed_file_sets_active_ldk_node_mnemonic() {
1842        use crate::config::{LdkNode, Ln, LnBackend};
1843
1844        let seed_file = temp_seed_file("seed_file_sets_ldk_seed");
1845        fs::write(&seed_file, TEST_MNEMONIC).expect("seed file should be written");
1846        let mut settings = config::Settings {
1847            ln: vec![Ln {
1848                ln_backend: LnBackend::LdkNode,
1849                ..Default::default()
1850            }],
1851            ldk_node: Some(LdkNode {
1852                ldk_node_mnemonic: Some("old ldk mnemonic".to_string()),
1853                ..Default::default()
1854            }),
1855            ..Default::default()
1856        };
1857
1858        apply_seed_file(&mut settings, &seed_file).expect("seed file should be applied");
1859
1860        assert_eq!(
1861            settings
1862                .ldk_node
1863                .expect("ldk node settings should be present")
1864                .ldk_node_mnemonic,
1865            Some(TEST_MNEMONIC.to_string())
1866        );
1867
1868        let _ = fs::remove_file(&seed_file);
1869    }
1870
1871    #[test]
1872    fn apply_seed_file_rejects_empty_seed_file() {
1873        let seed_file = temp_seed_file("empty_seed_file");
1874        fs::write(&seed_file, "\n\t ").expect("seed file should be written");
1875        let mut settings = config::Settings::default();
1876
1877        let err = apply_seed_file(&mut settings, &seed_file)
1878            .expect_err("empty seed file should be rejected");
1879
1880        assert!(err.to_string().contains("is empty"));
1881        assert_eq!(settings.info.seed, None);
1882
1883        let _ = fs::remove_file(&seed_file);
1884    }
1885
1886    #[test]
1887    fn apply_seed_file_rejects_invalid_seed_phrase() {
1888        let seed_file = temp_seed_file("invalid_seed_file");
1889        fs::write(&seed_file, "not a valid seed phrase").expect("seed file should be written");
1890        let mut settings = config::Settings::default();
1891
1892        let err = apply_seed_file(&mut settings, &seed_file)
1893            .expect_err("invalid seed phrase should be rejected");
1894
1895        assert!(err.to_string().contains("Invalid seed phrase"));
1896        assert_eq!(settings.info.mnemonic, None);
1897
1898        let _ = fs::remove_file(&seed_file);
1899    }
1900
1901    #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
1902    #[tokio::test]
1903    async fn fakewallet_dispatcher_uses_ln_entry_unit() {
1904        use cdk::mint::MintBuilder;
1905        use cdk_sqlite::mint::memory;
1906
1907        use crate::config::{FakeWallet, Ln, LnBackend};
1908
1909        let settings = config::Settings {
1910            ln: vec![Ln {
1911                ln_backend: LnBackend::FakeWallet,
1912                unit: CurrencyUnit::Eur,
1913                ..Default::default()
1914            }],
1915            fake_wallet: Some(FakeWallet::default()),
1916            ..Default::default()
1917        };
1918
1919        let localstore = Arc::new(memory::empty().await.unwrap());
1920        let builder = MintBuilder::new(localstore);
1921        let builder =
1922            configure_lightning_backend(&settings, builder, None, &std::env::temp_dir(), None)
1923                .await
1924                .expect("dispatcher should succeed");
1925
1926        let mint_info = builder.current_mint_info();
1927        let units: Vec<_> = mint_info
1928            .nuts
1929            .nut04
1930            .methods
1931            .iter()
1932            .map(|m| m.unit.clone())
1933            .collect();
1934        assert!(
1935            units.contains(&CurrencyUnit::Eur),
1936            "expected Eur, got {units:?}"
1937        );
1938        assert!(
1939            !units.contains(&CurrencyUnit::Sat),
1940            "Sat would only appear if supported_units leaked through; got {units:?}"
1941        );
1942    }
1943
1944    #[test]
1945    fn backend_unit_validation_allows_matching_units() {
1946        validate_backend_unit(&CurrencyUnit::Eur, "EUR").expect("matching units should pass");
1947    }
1948
1949    #[test]
1950    fn backend_unit_validation_allows_sat_msat_pair() {
1951        validate_backend_unit(&CurrencyUnit::Sat, "MSAT")
1952            .expect("sat/msat compatible units should pass");
1953        validate_backend_unit(&CurrencyUnit::Msat, "SAT")
1954            .expect("msat/sat compatible units should pass");
1955    }
1956
1957    #[test]
1958    fn backend_unit_validation_rejects_unsupported_conversion() {
1959        let err = validate_backend_unit(&CurrencyUnit::Eur, "SAT")
1960            .expect_err("sat backend should not advertise eur");
1961
1962        assert!(
1963            err.to_string().contains("only matching units"),
1964            "error should explain the supported conversions: {err}"
1965        );
1966    }
1967
1968    #[cfg(feature = "cln")]
1969    #[test]
1970    fn expand_path_expands_bare_tilde_without_panic() {
1971        let expanded = expand_path("~");
1972
1973        assert_eq!(expanded, home::home_dir());
1974    }
1975
1976    #[cfg(feature = "cln")]
1977    #[test]
1978    fn expand_path_keeps_named_tilde_paths_literal() {
1979        let expanded = expand_path("~foo").expect("path should be returned");
1980
1981        assert_eq!(expanded, PathBuf::from("~foo"));
1982    }
1983
1984    #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
1985    #[tokio::test]
1986    async fn duplicate_ln_unit_method_pair_is_rejected() {
1987        use cdk::mint::MintBuilder;
1988        use cdk_sqlite::mint::memory;
1989
1990        use crate::config::{FakeWallet, Ln, LnBackend};
1991
1992        let settings = config::Settings {
1993            ln: vec![
1994                Ln {
1995                    ln_backend: LnBackend::FakeWallet,
1996                    unit: CurrencyUnit::Sat,
1997                    ..Default::default()
1998                },
1999                Ln {
2000                    ln_backend: LnBackend::FakeWallet,
2001                    unit: CurrencyUnit::Sat,
2002                    ..Default::default()
2003                },
2004            ],
2005            fake_wallet: Some(FakeWallet::default()),
2006            ..Default::default()
2007        };
2008
2009        let localstore = Arc::new(memory::empty().await.unwrap());
2010        let builder = MintBuilder::new(localstore);
2011        let err =
2012            configure_lightning_backend(&settings, builder, None, &std::env::temp_dir(), None)
2013                .await
2014                .expect_err("duplicate unit/method pair should be rejected");
2015
2016        assert!(err.to_string().contains("Duplicate payment processor"));
2017    }
2018
2019    #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
2020    #[tokio::test]
2021    async fn empty_ln_vec_returns_unchanged_builder() {
2022        use cdk::mint::MintBuilder;
2023        use cdk_sqlite::mint::memory;
2024
2025        let settings = config::Settings {
2026            ln: vec![],
2027            ..Default::default()
2028        };
2029
2030        let localstore = Arc::new(memory::empty().await.unwrap());
2031        let builder = MintBuilder::new(localstore);
2032        let builder =
2033            configure_lightning_backend(&settings, builder, None, &std::env::temp_dir(), None)
2034                .await
2035                .expect("empty ln should succeed");
2036
2037        let mint_info = builder.current_mint_info();
2038        assert!(
2039            mint_info.nuts.nut04.methods.is_empty(),
2040            "no backends should be registered"
2041        );
2042    }
2043
2044    #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
2045    #[tokio::test]
2046    async fn ln_backend_none_logs_and_continues() {
2047        use cdk::mint::MintBuilder;
2048        use cdk_sqlite::mint::memory;
2049
2050        use crate::config::{Ln, LnBackend};
2051
2052        let settings = config::Settings {
2053            ln: vec![Ln {
2054                ln_backend: LnBackend::None,
2055                unit: CurrencyUnit::Sat,
2056                ..Default::default()
2057            }],
2058            ..Default::default()
2059        };
2060
2061        let localstore = Arc::new(memory::empty().await.unwrap());
2062        let builder = MintBuilder::new(localstore);
2063        let builder =
2064            configure_lightning_backend(&settings, builder, None, &std::env::temp_dir(), None)
2065                .await
2066                .expect("LnBackend::None should succeed");
2067
2068        let mint_info = builder.current_mint_info();
2069        assert!(
2070            mint_info.nuts.nut04.methods.is_empty(),
2071            "LnBackend::None should not register any methods"
2072        );
2073    }
2074
2075    #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
2076    #[tokio::test]
2077    async fn onchain_backend_none_returns_unchanged() {
2078        use cdk::mint::MintBuilder;
2079        use cdk_sqlite::mint::memory;
2080
2081        use crate::config::{Onchain, OnchainBackend};
2082
2083        let settings = config::Settings {
2084            onchain: Some(Onchain {
2085                onchain_backend: OnchainBackend::None,
2086                ..Default::default()
2087            }),
2088            ..Default::default()
2089        };
2090
2091        let localstore = Arc::new(memory::empty().await.unwrap());
2092        let builder = MintBuilder::new(localstore);
2093        let builder =
2094            configure_onchain_backend(&settings, builder, None, &std::env::temp_dir(), None)
2095                .await
2096                .expect("OnchainBackend::None should succeed");
2097
2098        let mint_info = builder.current_mint_info();
2099        assert!(
2100            mint_info.nuts.nut04.methods.is_empty(),
2101            "OnchainBackend::None should not register any methods"
2102        );
2103    }
2104
2105    #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
2106    #[tokio::test]
2107    async fn fakewallet_onchain_no_lightning_configures_onchain_methods() {
2108        use cdk::mint::MintBuilder;
2109        use cdk_sqlite::mint::memory;
2110
2111        use crate::config::{FakeWallet, Ln, LnBackend, Onchain, OnchainBackend};
2112
2113        let settings = config::Settings {
2114            ln: vec![Ln {
2115                ln_backend: LnBackend::None,
2116                ..Default::default()
2117            }],
2118            onchain: Some(Onchain {
2119                onchain_backend: OnchainBackend::FakeWallet,
2120                ..Default::default()
2121            }),
2122            fake_wallet: Some(FakeWallet::default()),
2123            ..Default::default()
2124        };
2125
2126        let localstore = Arc::new(memory::empty().await.unwrap());
2127        let builder = MintBuilder::new(localstore);
2128        let builder =
2129            configure_onchain_backend(&settings, builder, None, &std::env::temp_dir(), None)
2130                .await
2131                .expect("fakewallet onchain should succeed");
2132
2133        let mint_info = builder.current_mint_info();
2134        let methods: Vec<_> = mint_info
2135            .nuts
2136            .nut04
2137            .methods
2138            .iter()
2139            .map(|m| m.method.clone())
2140            .collect();
2141        assert!(
2142            methods.contains(&PaymentMethod::Known(KnownMethod::Onchain)),
2143            "expected onchain method, got {methods:?}"
2144        );
2145    }
2146
2147    #[cfg(all(feature = "fakewallet", feature = "cln", feature = "sqlite"))]
2148    #[tokio::test]
2149    async fn fakewallet_onchain_with_real_ln_bails() {
2150        use cdk::mint::MintBuilder;
2151        use cdk_sqlite::mint::memory;
2152
2153        use crate::config::{Ln, LnBackend, Onchain, OnchainBackend};
2154
2155        let settings = config::Settings {
2156            ln: vec![Ln {
2157                ln_backend: LnBackend::Cln,
2158                unit: CurrencyUnit::Sat,
2159                ..Default::default()
2160            }],
2161            onchain: Some(Onchain {
2162                onchain_backend: OnchainBackend::FakeWallet,
2163                ..Default::default()
2164            }),
2165            ..Default::default()
2166        };
2167
2168        let localstore = Arc::new(memory::empty().await.unwrap());
2169        let builder = MintBuilder::new(localstore);
2170        let err = configure_onchain_backend(&settings, builder, None, &std::env::temp_dir(), None)
2171            .await
2172            .expect_err("fakewallet onchain with real LN should bail");
2173
2174        assert!(
2175            err.to_string().contains("fakewallet"),
2176            "error should mention fakewallet: {err}"
2177        );
2178    }
2179
2180    #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
2181    #[tokio::test]
2182    async fn configure_mint_builder_no_backends_bails() {
2183        use cdk::mint::MintBuilder;
2184        use cdk_sqlite::mint::memory;
2185
2186        use crate::config::{Ln, LnBackend};
2187
2188        let settings = config::Settings {
2189            ln: vec![Ln {
2190                ln_backend: LnBackend::None,
2191                ..Default::default()
2192            }],
2193            ..Default::default()
2194        };
2195
2196        let localstore = Arc::new(memory::empty().await.unwrap());
2197        let builder = MintBuilder::new(localstore);
2198        let err = configure_mint_builder(&settings, builder, None, &std::env::temp_dir(), None)
2199            .await
2200            .expect_err("no payment backends should bail");
2201
2202        assert!(
2203            err.to_string().contains("At least one payment backend"),
2204            "error should mention missing backends: {err}"
2205        );
2206    }
2207
2208    #[cfg(all(feature = "fakewallet", feature = "sqlite", feature = "bdk"))]
2209    #[tokio::test]
2210    async fn configure_mint_builder_fake_wallet_with_bdk_onchain_bails() {
2211        use cdk::mint::MintBuilder;
2212        use cdk_sqlite::mint::memory;
2213
2214        use crate::config::{Bdk, FakeWallet, Ln, LnBackend, Onchain, OnchainBackend};
2215
2216        let settings = config::Settings {
2217            ln: vec![Ln {
2218                ln_backend: LnBackend::FakeWallet,
2219                ..Default::default()
2220            }],
2221            onchain: Some(Onchain {
2222                onchain_backend: OnchainBackend::Bdk,
2223                ..Default::default()
2224            }),
2225            fake_wallet: Some(FakeWallet::default()),
2226            bdk: Some(Bdk {
2227                network: Some("mainnet".to_string()),
2228                ..Default::default()
2229            }),
2230            ..Default::default()
2231        };
2232
2233        let localstore = Arc::new(memory::empty().await.unwrap());
2234        let builder = MintBuilder::new(localstore);
2235        let err = configure_mint_builder(&settings, builder, None, &std::env::temp_dir(), None)
2236            .await
2237            .expect_err("fake wallet with BDK onchain should bail");
2238
2239        assert!(
2240            err.to_string().contains("fakewallet") && err.to_string().contains("bdk"),
2241            "error should mention backend pairing validation: {err}"
2242        );
2243    }
2244
2245    #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
2246    #[tokio::test]
2247    async fn configure_backend_for_methods_registers_websockets_and_fee() {
2248        use cdk::mint::MintBuilder;
2249        use cdk_sqlite::mint::memory;
2250
2251        use crate::config::{FakeWallet, Ln, LnBackend};
2252
2253        let settings = config::Settings {
2254            ln: vec![Ln {
2255                ln_backend: LnBackend::FakeWallet,
2256                unit: CurrencyUnit::Sat,
2257                ..Default::default()
2258            }],
2259            fake_wallet: Some(FakeWallet::default()),
2260            info: config::Info {
2261                input_fee_ppk: Some(100),
2262                ..Default::default()
2263            },
2264            ..Default::default()
2265        };
2266
2267        let localstore = Arc::new(memory::empty().await.unwrap());
2268        let builder = MintBuilder::new(localstore);
2269
2270        let fake_wallet = settings.fake_wallet.clone().expect("fake wallet config");
2271        let fake = fake_wallet
2272            .setup(
2273                &settings,
2274                CurrencyUnit::Sat,
2275                None,
2276                &std::env::temp_dir(),
2277                None,
2278            )
2279            .await
2280            .expect("fake wallet setup");
2281
2282        let mint_melt_limits = cdk::mint::MintMeltLimits {
2283            mint_min: 1.into(),
2284            mint_max: 500_000.into(),
2285            melt_min: 1.into(),
2286            melt_max: 500_000.into(),
2287        };
2288
2289        let builder = configure_backend_for_methods(
2290            &settings,
2291            builder,
2292            CurrencyUnit::Sat,
2293            mint_melt_limits,
2294            Arc::new(fake),
2295            vec![PaymentMethod::Known(KnownMethod::Bolt11)],
2296        )
2297        .await
2298        .expect("configure_backend_for_methods should succeed");
2299
2300        let mint_info = builder.current_mint_info();
2301        assert!(
2302            !mint_info.nuts.nut04.methods.is_empty(),
2303            "bolt11 method should be registered"
2304        );
2305        assert!(
2306            !mint_info.nuts.nut17.supported.is_empty(),
2307            "websocket support should be configured"
2308        );
2309    }
2310
2311    #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
2312    #[tokio::test]
2313    async fn fakewallet_onchain_with_fake_ln_does_not_duplicate() {
2314        use cdk::mint::MintBuilder;
2315        use cdk_sqlite::mint::memory;
2316
2317        use crate::config::{FakeWallet, Ln, LnBackend, Onchain, OnchainBackend};
2318
2319        let settings = config::Settings {
2320            ln: vec![Ln {
2321                ln_backend: LnBackend::FakeWallet,
2322                unit: CurrencyUnit::Sat,
2323                ..Default::default()
2324            }],
2325            onchain: Some(Onchain {
2326                onchain_backend: OnchainBackend::FakeWallet,
2327                ..Default::default()
2328            }),
2329            fake_wallet: Some(FakeWallet::default()),
2330            ..Default::default()
2331        };
2332
2333        let localstore = Arc::new(memory::empty().await.unwrap());
2334        let builder = MintBuilder::new(localstore);
2335        let builder =
2336            configure_onchain_backend(&settings, builder, None, &std::env::temp_dir(), None)
2337                .await
2338                .expect("fakewallet onchain with fake LN should succeed without duplicating");
2339
2340        let mint_info = builder.current_mint_info();
2341        assert!(
2342            mint_info.nuts.nut04.methods.is_empty(),
2343            "when has_lightning_backend is true and no real LN, fakewallet onchain should skip; got {:?}",
2344            mint_info.nuts.nut04.methods
2345        );
2346    }
2347
2348    #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
2349    #[tokio::test]
2350    async fn fakewallet_onchain_missing_fake_wallet_config_bails() {
2351        use cdk::mint::MintBuilder;
2352        use cdk_sqlite::mint::memory;
2353
2354        use crate::config::{Ln, LnBackend, Onchain, OnchainBackend};
2355
2356        let settings = config::Settings {
2357            ln: vec![Ln {
2358                ln_backend: LnBackend::None,
2359                ..Default::default()
2360            }],
2361            onchain: Some(Onchain {
2362                onchain_backend: OnchainBackend::FakeWallet,
2363                ..Default::default()
2364            }),
2365            fake_wallet: None,
2366            ..Default::default()
2367        };
2368
2369        let localstore = Arc::new(memory::empty().await.unwrap());
2370        let builder = MintBuilder::new(localstore);
2371        let err = configure_onchain_backend(&settings, builder, None, &std::env::temp_dir(), None)
2372            .await
2373            .expect_err("missing fake_wallet config should bail");
2374
2375        assert!(
2376            err.to_string().contains("Fake wallet config"),
2377            "error should mention missing config: {err}"
2378        );
2379    }
2380
2381    #[test]
2382    fn test_postgres_auth_url_validation() {
2383        // Test that the auth database config requires explicit configuration
2384
2385        // Test empty URL
2386        let auth_config = config::PostgresAuthConfig {
2387            url: "".to_string(),
2388            ..Default::default()
2389        };
2390        assert!(auth_config.url.is_empty());
2391
2392        // Test non-empty URL
2393        let auth_config = config::PostgresAuthConfig {
2394            url: "postgresql://user:password@localhost:5432/auth_db".to_string(),
2395            ..Default::default()
2396        };
2397        assert!(!auth_config.url.is_empty());
2398    }
2399
2400    #[test]
2401    fn test_extract_supported_payment_methods_unique_ordered() {
2402        let mut mint_info = cdk::nuts::MintInfo::default();
2403        mint_info.nuts.nut04.methods = vec![
2404            MintMethodSettings {
2405                method: PaymentMethod::Known(KnownMethod::Bolt11),
2406                unit: CurrencyUnit::Sat,
2407                min_amount: None,
2408                max_amount: None,
2409                options: None,
2410            },
2411            MintMethodSettings {
2412                method: PaymentMethod::Known(KnownMethod::Bolt12),
2413                unit: CurrencyUnit::Sat,
2414                min_amount: None,
2415                max_amount: None,
2416                options: None,
2417            },
2418            MintMethodSettings {
2419                method: PaymentMethod::Known(KnownMethod::Bolt11),
2420                unit: CurrencyUnit::Msat,
2421                min_amount: None,
2422                max_amount: None,
2423                options: None,
2424            },
2425            MintMethodSettings {
2426                method: PaymentMethod::Custom("paypal".to_string()),
2427                unit: CurrencyUnit::Usd,
2428                min_amount: None,
2429                max_amount: None,
2430                options: None,
2431            },
2432            MintMethodSettings {
2433                method: PaymentMethod::Custom("paypal".to_string()),
2434                unit: CurrencyUnit::Eur,
2435                min_amount: None,
2436                max_amount: None,
2437                options: None,
2438            },
2439        ];
2440
2441        let methods = extract_supported_payment_methods(&mint_info);
2442
2443        assert_eq!(methods, vec!["bolt11", "bolt12", "paypal"]);
2444    }
2445}