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