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