1#![allow(missing_docs)]
2use 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
12use anyhow::{anyhow, bail, Context, Result};
14use axum::extract::DefaultBodyLimit;
15use axum::Router;
16use bip39::Mnemonic;
17use cdk::cdk_database::{self, KVStore, KVStoreCompareAndSwap, MintDatabase, MintKeysDatabase};
18use cdk::mint::{Mint, MintBuilder, MintMeltLimits};
19use cdk::nuts::nut00::KnownMethod;
20#[cfg(any(
21 feature = "cln",
22 feature = "lnd",
23 feature = "ldk-node",
24 feature = "fakewallet",
25 feature = "bdk",
26 feature = "grpc-processor"
27))]
28use cdk::nuts::nut17::SupportedMethods;
29use cdk::nuts::nut19::{CachedEndpoint, Method as NUT19Method, Path as NUT19Path};
30use cdk::nuts::{
31 AuthRequired, ContactInfo, Method, MintVersion, PaymentMethod, ProtectedEndpoint, RoutePath,
32};
33use cdk_axum::cache::HttpCache;
34use cdk_common::common::QuoteTTL;
35use cdk_common::database::DynMintDatabase;
36#[cfg(feature = "prometheus")]
38use cdk_common::payment::MetricsMintPayment;
39use cdk_common::payment::MintPayment;
40#[cfg(feature = "postgres")]
41use cdk_postgres::{MintPgAuthDatabase, MintPgDatabase, PgConfig};
42#[cfg(feature = "sqlite")]
43use cdk_sqlite::mint::MintSqliteAuthDatabase;
44#[cfg(feature = "sqlite")]
45use cdk_sqlite::MintSqliteDatabase;
46use cli::CLIArgs;
47use config::{AuthType, DatabaseEngine, PaymentBackendType};
48use env_vars::ENV_WORK_DIR;
49use setup::PaymentBackendSetup;
50use tower::ServiceBuilder;
51use tower_http::compression::CompressionLayer;
52use tower_http::decompression::RequestDecompressionLayer;
53use tower_http::trace::TraceLayer;
54use tracing_appender::{non_blocking, rolling};
55use tracing_subscriber::fmt::writer::MakeWriterExt;
56use tracing_subscriber::EnvFilter;
57
58pub mod cli;
59pub mod config;
60mod config_migration;
61mod config_service;
62mod config_store;
63pub mod env_vars;
64mod secret;
65pub mod setup;
66
67pub use config_migration::{migrate_legacy_configuration, MigrationOutcome};
68pub use config_service::{ApplyOutcome, RollbackOutcome};
69
70#[cfg(test)]
71pub(crate) mod test_utils {
72 use std::path::PathBuf;
73 use std::sync::atomic::{AtomicUsize, Ordering};
74 use std::sync::{Mutex, MutexGuard, OnceLock};
75
76 pub(crate) fn env_lock() -> MutexGuard<'static, ()> {
77 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
78 LOCK.get_or_init(|| Mutex::new(()))
79 .lock()
80 .unwrap_or_else(|e| e.into_inner())
81 }
82
83 pub(crate) fn unique_temp_path(name: &str) -> PathBuf {
84 static COUNTER: AtomicUsize = AtomicUsize::new(0);
85 std::env::temp_dir().join(format!(
86 "{name}_{}_{}",
87 std::process::id(),
88 COUNTER.fetch_add(1, Ordering::Relaxed)
89 ))
90 }
91}
92
93const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
94const DEFAULT_BATCH_MINT_SIZE: u64 = 100;
95const REQUEST_BODY_LIMIT_BYTES: usize = 1_048_576;
96
97type DynSignatory = Arc<dyn cdk_signatory::signatory::Signatory + Send + Sync>;
98
99#[derive(Clone)]
100struct ValidatedSigningSource {
101 expected_pubkey: cdk::nuts::PublicKey,
102 remote_signatory: Option<DynSignatory>,
103}
104
105impl std::fmt::Debug for ValidatedSigningSource {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 f.debug_struct("ValidatedSigningSource")
108 .field("expected_pubkey", &self.expected_pubkey)
109 .field("remote_signatory", &self.remote_signatory.is_some())
110 .finish()
111 }
112}
113
114#[derive(Debug, Clone)]
123struct ConfigurationActivation {
124 service: config_service::ConfigurationService,
125 phase: ActivationPhase,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130enum ActivationPhase {
131 Pending {
134 revision: u64,
136 },
137 Applied,
139}
140
141impl ConfigurationActivation {
142 fn new(
143 service: config_service::ConfigurationService,
144 state: config_store::DocumentState,
145 revision: u64,
146 ) -> Self {
147 let phase = match state {
148 config_store::DocumentState::Pending => ActivationPhase::Pending { revision },
149 config_store::DocumentState::Applied => ActivationPhase::Applied,
150 };
151 Self { service, phase }
152 }
153
154 fn forces_configuration(&self) -> bool {
157 matches!(self.phase, ActivationPhase::Pending { .. })
158 }
159
160 fn preserves_database_values(&self, rpc_enabled: bool) -> bool {
162 rpc_enabled && !self.forces_configuration()
163 }
164
165 async fn mark_applied(&self) -> Result<()> {
171 if let ActivationPhase::Pending { revision } = self.phase {
172 if !self.service.mark_applied(revision).await? {
173 tracing::info!(
174 "A newer configuration was stored during startup and remains unapplied for the next restart."
175 );
176 }
177 }
178 Ok(())
179 }
180}
181
182#[cfg(feature = "management-rpc")]
183#[derive(Debug, Clone)]
184struct ConfigurationMutationGuard {
185 service: config_service::ConfigurationService,
186}
187
188#[cfg(feature = "management-rpc")]
189#[async_trait::async_trait]
190impl cdk_mint_rpc::MintMutationGuard for ConfigurationMutationGuard {
191 async fn check(&self) -> Result<(), cdk_mint_rpc::MintMutationGuardError> {
192 match self.service.has_pending_configuration().await {
193 Ok(true) => Err(cdk_mint_rpc::MintMutationGuardError::FailedPrecondition(
194 "A configuration apply is pending; restart cdk-mintd before making management RPC changes"
195 .to_owned(),
196 )),
197 Ok(false) => Ok(()),
198 Err(error) => Err(cdk_mint_rpc::MintMutationGuardError::Internal(format!(
199 "Could not inspect the stored configuration state: {error}"
200 ))),
201 }
202 }
203}
204
205#[cfg(all(feature = "management-rpc", feature = "bdk"))]
206type ConfiguredWalletInfoProvider = Option<cdk_mint_rpc::DynWalletInfoProvider>;
207#[cfg(not(all(feature = "management-rpc", feature = "bdk")))]
208type ConfiguredWalletInfoProvider = ();
209
210#[cfg(all(feature = "management-rpc", feature = "bdk"))]
211#[derive(Clone)]
212struct BdkWalletInfoProvider {
213 bdk: Arc<cdk_bdk::CdkBdk>,
214}
215
216#[cfg(all(feature = "management-rpc", feature = "bdk"))]
217#[async_trait::async_trait]
218impl cdk_mint_rpc::WalletInfoProvider for BdkWalletInfoProvider {
219 async fn create_deposit_address(
220 &self,
221 ) -> std::result::Result<String, cdk_mint_rpc::WalletInfoError> {
222 self.bdk
223 .create_operator_deposit_address()
224 .await
225 .map_err(|err| cdk_mint_rpc::WalletInfoError::new(err.to_string()))
226 }
227
228 async fn get_balance(
229 &self,
230 ) -> std::result::Result<cdk_mint_rpc::wallet::GetBalanceResponse, cdk_mint_rpc::WalletInfoError>
231 {
232 let balance = self.bdk.wallet_balance().await;
233
234 Ok(cdk_mint_rpc::wallet::GetBalanceResponse {
235 confirmed_sat: balance.confirmed_sat,
236 trusted_pending_sat: balance.trusted_pending_sat,
237 untrusted_pending_sat: balance.untrusted_pending_sat,
238 immature_sat: balance.immature_sat,
239 trusted_spendable_sat: balance.trusted_spendable_sat,
240 total_sat: balance.total_sat,
241 network: balance.network,
242 synced_height: balance.synced_height,
243 })
244 }
245
246 async fn list_transactions(
247 &self,
248 offset: usize,
249 limit: usize,
250 ) -> std::result::Result<cdk_mint_rpc::WalletTransactionPage, cdk_mint_rpc::WalletInfoError>
251 {
252 let page = self
253 .bdk
254 .wallet_transactions(offset, limit)
255 .await
256 .map_err(|err| cdk_mint_rpc::WalletInfoError::new(err.to_string()))?;
257
258 Ok(cdk_mint_rpc::WalletTransactionPage {
259 transactions: page
260 .items
261 .into_iter()
262 .map(|transaction| cdk_mint_rpc::wallet::WalletTransaction {
263 txid: transaction.txid,
264 inputs: transaction
265 .inputs
266 .into_iter()
267 .map(|input| cdk_mint_rpc::wallet::WalletTransactionInput {
268 txid: input.txid,
269 vout: input.vout,
270 amount_sat: input.amount_sat,
271 address: input.address,
272 })
273 .collect(),
274 outputs: transaction
275 .outputs
276 .into_iter()
277 .map(|output| cdk_mint_rpc::wallet::WalletTransactionOutput {
278 vout: output.vout,
279 address: output.address,
280 amount_sat: output.amount_sat,
281 quote_id: output.quote_id,
282 })
283 .collect(),
284 received_sat: transaction.received_sat,
285 sent_sat: transaction.sent_sat,
286 fee_sat: transaction.fee_sat,
287 balance_delta_sat: transaction.balance_delta_sat,
288 confirmation_height: transaction.confirmation_height,
289 confirmation_time: transaction.confirmation_time,
290 first_seen: transaction.first_seen,
291 })
292 .collect(),
293 total: page.total,
294 })
295 }
296
297 async fn list_addresses(
298 &self,
299 offset: usize,
300 limit: usize,
301 ) -> std::result::Result<cdk_mint_rpc::WalletAddressPage, cdk_mint_rpc::WalletInfoError> {
302 let page = self
303 .bdk
304 .wallet_addresses(offset, limit)
305 .await
306 .map_err(|err| cdk_mint_rpc::WalletInfoError::new(err.to_string()))?;
307
308 Ok(cdk_mint_rpc::WalletAddressPage {
309 addresses: page
310 .items
311 .into_iter()
312 .map(|address| cdk_mint_rpc::wallet::WalletAddress {
313 address: address.address,
314 keychain: match address.keychain {
315 cdk_bdk::WalletKeychain::External => {
316 cdk_mint_rpc::wallet::KeychainKind::External.into()
317 }
318 cdk_bdk::WalletKeychain::Internal => {
319 cdk_mint_rpc::wallet::KeychainKind::Internal.into()
320 }
321 },
322 derivation_index: address.derivation_index,
323 used: address.used,
324 balance_sat: address.balance_sat,
325 confirmed_balance_sat: address.confirmed_balance_sat,
326 })
327 .collect(),
328 total: page.total,
329 })
330 }
331}
332
333#[cfg(all(feature = "management-rpc", feature = "bdk"))]
334fn no_wallet_info_provider() -> ConfiguredWalletInfoProvider {
335 None
336}
337
338#[cfg(not(all(feature = "management-rpc", feature = "bdk")))]
339fn no_wallet_info_provider() -> ConfiguredWalletInfoProvider {}
340
341fn extract_supported_payment_methods(mint_info: &cdk::nuts::MintInfo) -> Vec<String> {
342 let mut seen = HashSet::new();
343 mint_info
344 .nuts
345 .nut04
346 .methods
347 .iter()
348 .map(|method| method.method.to_string())
349 .filter(|method| seen.insert(method.clone()))
350 .collect()
351}
352
353#[cfg(feature = "cln")]
354fn expand_path(path: &str) -> Option<PathBuf> {
355 if path == "~" {
356 return home::home_dir();
357 }
358
359 if let Some(remainder) = path.strip_prefix("~/") {
360 return home::home_dir().map(|home_dir| home_dir.join(remainder));
361 }
362
363 Some(PathBuf::from(path))
364}
365
366async fn initial_setup(
370 work_dir: &Path,
371 settings: &config::Settings,
372 db_password: Option<String>,
373) -> Result<(
374 DynMintDatabase,
375 Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
376 Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync>,
377 Arc<dyn KVStoreCompareAndSwap<Err = cdk_database::Error> + Send + Sync>,
378)> {
379 tracing::info!("Initializing database...");
380 let (localstore, keystore, kv, configuration_store) =
381 setup_database(settings, work_dir, db_password).await?;
382 tracing::info!("Database initialized successfully");
383 Ok((localstore, keystore, kv, configuration_store))
384}
385
386#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
388pub enum MintInitializationMode {
389 New,
391 Existing,
393}
394
395#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
397pub enum BdkWalletPolicy {
398 RequireExisting,
400 AllowNew,
402}
403
404pub fn setup_tracing(
408 work_dir: &Path,
409 logging_config: &config::LoggingConfig,
410) -> Result<Option<tracing_appender::non_blocking::WorkerGuard>> {
411 let default_filter = "debug";
412 let hyper_filter = "hyper=warn,rustls=warn,reqwest=warn";
413 let h2_filter = "h2=warn";
414 let tower_filter = "tower=warn";
415 let tower_http = "tower_http=warn";
416 let rustls = "rustls=warn";
417 let tungstenite = "tungstenite=warn";
418 let tokio_postgres = "tokio_postgres=warn";
419
420 let env_filter = EnvFilter::new(format!(
421 "{default_filter},{hyper_filter},{h2_filter},{tower_filter},{tower_http},{rustls},{tungstenite},{tokio_postgres}"
422 ));
423
424 use config::LoggingOutput;
425 match logging_config.output {
426 LoggingOutput::Stderr => {
427 let console_level = logging_config
429 .console_level
430 .as_deref()
431 .unwrap_or("info")
432 .parse::<tracing::Level>()
433 .unwrap_or(tracing::Level::INFO);
434
435 let stderr = std::io::stderr.with_max_level(console_level);
436
437 tracing_subscriber::fmt()
438 .with_env_filter(env_filter)
439 .with_ansi(false)
440 .with_writer(stderr)
441 .init();
442
443 tracing::info!("Logging initialized: console only ({}+)", console_level);
444 Ok(None)
445 }
446 LoggingOutput::File => {
447 let file_level = logging_config
449 .file_level
450 .as_deref()
451 .unwrap_or("debug")
452 .parse::<tracing::Level>()
453 .unwrap_or(tracing::Level::DEBUG);
454
455 let logs_dir = work_dir.join("logs");
457 std::fs::create_dir_all(&logs_dir)?;
458
459 let file_appender = rolling::daily(&logs_dir, "cdk-mintd.log");
461 let (non_blocking_appender, guard) = non_blocking(file_appender);
462
463 let file_writer = non_blocking_appender.with_max_level(file_level);
464
465 tracing_subscriber::fmt()
466 .with_env_filter(env_filter)
467 .with_ansi(false)
468 .with_writer(file_writer)
469 .init();
470
471 tracing::info!(
472 "Logging initialized: file only at {}/cdk-mintd.log ({}+)",
473 logs_dir.display(),
474 file_level
475 );
476 Ok(Some(guard))
477 }
478 LoggingOutput::Both => {
479 let console_level = logging_config
481 .console_level
482 .as_deref()
483 .unwrap_or("info")
484 .parse::<tracing::Level>()
485 .unwrap_or(tracing::Level::INFO);
486 let file_level = logging_config
487 .file_level
488 .as_deref()
489 .unwrap_or("debug")
490 .parse::<tracing::Level>()
491 .unwrap_or(tracing::Level::DEBUG);
492
493 let logs_dir = work_dir.join("logs");
495 std::fs::create_dir_all(&logs_dir)?;
496
497 let file_appender = rolling::daily(&logs_dir, "cdk-mintd.log");
499 let (non_blocking_appender, guard) = non_blocking(file_appender);
500
501 let stderr = std::io::stderr.with_max_level(console_level);
503 let file_writer = non_blocking_appender.with_max_level(file_level);
504
505 tracing_subscriber::fmt()
506 .with_env_filter(env_filter)
507 .with_ansi(false)
508 .with_writer(stderr.and(file_writer))
509 .init();
510
511 tracing::info!(
512 "Logging initialized: console ({}+) and file at {}/cdk-mintd.log ({}+)",
513 console_level,
514 logs_dir.display(),
515 file_level
516 );
517 Ok(Some(guard))
518 }
519 }
520}
521
522pub async fn get_work_directory(args: &CLIArgs) -> Result<PathBuf> {
524 let work_dir = if let Some(work_dir) = &args.work_dir {
525 tracing::info!("Using work dir from cmd arg");
526 work_dir.clone()
527 } else if let Ok(env_work_dir) = env::var(ENV_WORK_DIR) {
528 tracing::info!("Using work dir from env var");
529 env_work_dir.into()
530 } else {
531 work_dir()?
532 };
533 tracing::info!("Using work dir: {}", work_dir.display());
534 Ok(work_dir)
535}
536
537pub fn load_settings(work_dir: &Path, config_path: Option<PathBuf>) -> Result<config::Settings> {
539 let settings = load_settings_from_sources(work_dir, config_path)?;
540 validate_settings(&settings)?;
541
542 Ok(settings)
543}
544
545fn load_settings_from_sources(
546 work_dir: &Path,
547 config_path: Option<PathBuf>,
548) -> Result<config::Settings> {
549 let config_file_arg = match config_path {
551 Some(c) => c,
552 None => work_dir.join("config.toml"),
553 };
554
555 let mut settings = if config_file_arg.exists() {
556 config::Settings::try_new(Some(config_file_arg.clone()))
557 .with_context(|| format!("Failed to read config file {}", config_file_arg.display()))?
558 } else {
559 tracing::info!("Config file does not exist. Attempting to read env vars");
560 config::Settings::default()
561 };
562 settings.from_env()
565}
566
567pub(crate) fn validate_settings(settings: &config::Settings) -> Result<()> {
568 validate_payment_backends(settings)?;
569 settings
570 .validate_backend_pairing()
571 .map_err(anyhow::Error::msg)?;
572 validate_listen_config(settings)?;
573 validate_signing_config(settings)?;
574 validate_payment_backend_config(settings)?;
575 validate_onchain_config(settings)?;
576 validate_database_config(settings)?;
577 validate_auth_config(settings)?;
578 validate_management_rpc_config(settings)?;
579 validate_prometheus_config(settings)?;
580
581 Ok(())
582}
583
584fn validate_payment_backends(settings: &config::Settings) -> Result<()> {
585 let has_payment_backend = settings
586 .payment_backend
587 .iter()
588 .any(|backend| backend.backend != PaymentBackendType::None);
589 let has_onchain_backend = settings
590 .onchain
591 .as_ref()
592 .is_some_and(|onchain| onchain.onchain_backend != config::OnchainBackend::None);
593
594 if !has_payment_backend && !has_onchain_backend {
595 bail!("At least one payment backend must be configured");
596 }
597
598 Ok(())
599}
600
601fn validate_database_config(settings: &config::Settings) -> Result<()> {
602 if settings.database.engine == DatabaseEngine::Postgres {
603 let pg_config = settings.database.postgres.as_ref().ok_or_else(|| {
604 anyhow!("PostgreSQL configuration is required when using PostgreSQL engine")
605 })?;
606
607 if pg_config.url.is_empty() {
608 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");
609 }
610 }
611
612 Ok(())
613}
614
615fn validate_listen_config(settings: &config::Settings) -> Result<()> {
616 format!(
617 "{}:{}",
618 settings.info.listen_host, settings.info.listen_port
619 )
620 .parse::<SocketAddr>()
621 .map_err(|err| {
622 anyhow!(
623 "Invalid mint listen address [info].listen_host/[info].listen_port ({}:{}): {err}",
624 settings.info.listen_host,
625 settings.info.listen_port
626 )
627 })?;
628
629 Ok(())
630}
631
632fn validate_signing_config(settings: &config::Settings) -> Result<()> {
633 const MIN_SEED_BYTES: usize = 32;
634
635 if let Some(signatory) = settings.enabled_signatory() {
636 let has_local_seed = settings
637 .info
638 .seed
639 .as_ref()
640 .is_some_and(|seed| !seed.is_empty());
641 let has_local_mnemonic = settings
642 .info
643 .mnemonic
644 .as_ref()
645 .is_some_and(|mnemonic| !mnemonic.is_empty());
646 if has_local_seed || has_local_mnemonic {
647 bail!(
648 "Remote signatory configuration cannot include [info].seed or [info].mnemonic; \
649 keep private signing material on the signatory host"
650 );
651 }
652
653 if signatory.tls_dir.is_none() && !signatory.allow_insecure {
654 bail!(
655 "gRPC signatory TLS is not configured. Set [signatory].tls_dir or \
656 [signatory].allow_insecure = true to connect without TLS"
657 );
658 }
659
660 return Ok(());
661 }
662
663 let seed = settings.info.seed.as_ref();
664 let mnemonic = settings
665 .info
666 .mnemonic
667 .as_ref()
668 .filter(|value| !value.is_empty());
669
670 if let Some(seed) = seed {
671 if seed.is_empty() {
672 bail!("Seed in [info].seed must not be empty");
673 }
674 if seed.len() < MIN_SEED_BYTES {
675 bail!(
676 "Seed in [info].seed is too short ({} bytes); require at least {MIN_SEED_BYTES} bytes",
677 seed.len()
678 );
679 }
680 return Ok(());
681 }
682
683 if let Some(mnemonic) = mnemonic {
684 Mnemonic::from_str(mnemonic)
685 .map_err(|err| anyhow!("Invalid mnemonic in [info].mnemonic: {err}"))?;
686 return Ok(());
687 }
688
689 bail!("No signing source configured. Set [info].mnemonic or [info].seed to an env:/file: secret reference, or enable [signatory]");
690}
691
692fn validate_payment_backend_config(settings: &config::Settings) -> Result<()> {
693 for payment_backend in &settings.payment_backend {
696 if payment_backend.min_mint > payment_backend.max_mint {
697 bail!("Payment backend min_mint cannot be greater than max_mint");
698 }
699 if payment_backend.min_melt > payment_backend.max_melt {
700 bail!("Payment backend min_melt cannot be greater than max_melt");
701 }
702
703 match payment_backend.backend {
704 PaymentBackendType::None => {}
705 #[cfg(feature = "cln")]
706 PaymentBackendType::Cln => {
707 let cln = settings.cln.as_ref().ok_or_else(|| {
708 anyhow!("CLN backend selected but [cln] config section is missing")
709 })?;
710 if cln.rpc_path.as_os_str().is_empty() {
711 bail!("CLN rpc_path must be set in [cln].rpc_path");
712 }
713 }
714 #[cfg(feature = "lnd")]
715 PaymentBackendType::Lnd => {
716 let lnd = settings.lnd.as_ref().ok_or_else(|| {
717 anyhow!("LND backend selected but [lnd] config section is missing")
718 })?;
719 if lnd.address.is_empty() {
720 bail!("LND address must be set in [lnd].address");
721 }
722 if lnd.cert_file.as_os_str().is_empty() {
723 bail!("LND cert_file must be set in [lnd].cert_file");
724 }
725 if lnd.macaroon_file.as_os_str().is_empty() {
726 bail!("LND macaroon_file must be set in [lnd].macaroon_file");
727 }
728 }
729 #[cfg(feature = "fakewallet")]
730 PaymentBackendType::FakeWallet => {
731 let fake_wallet = settings.fake_wallet.as_ref().ok_or_else(|| {
732 anyhow!(
733 "Fake wallet backend selected but [fake_wallet] config section is missing"
734 )
735 })?;
736 if fake_wallet.supported_units.is_empty() {
737 bail!("Fake wallet supported_units must contain at least one unit in [fake_wallet].supported_units");
738 }
739 if fake_wallet.min_delay_time > fake_wallet.max_delay_time {
740 bail!("Fake wallet min_delay_time cannot be greater than max_delay_time");
741 }
742 }
743 #[cfg(feature = "grpc-processor")]
744 PaymentBackendType::GrpcProcessor => {
745 let grpc_processor = settings.grpc_processor.as_ref().ok_or_else(|| {
746 anyhow!(
747 "gRPC payment processor backend selected but [grpc_processor] config section is missing"
748 )
749 })?;
750 if grpc_processor.supported_units.is_empty() {
751 bail!("gRPC payment processor supported_units must contain at least one unit in [grpc_processor].supported_units");
752 }
753 if grpc_processor.address.is_empty() {
754 bail!("gRPC payment processor address must be set in [grpc_processor].address");
755 }
756 }
757 #[cfg(feature = "ldk-node")]
758 PaymentBackendType::LdkNode => {
759 if settings.ldk_node.is_none() {
760 bail!("LDK Node backend selected but [ldk_node] config section is missing");
761 }
762 }
763 }
764 }
765
766 Ok(())
767}
768
769fn validate_onchain_config(settings: &config::Settings) -> Result<()> {
770 let Some(onchain) = settings.onchain.as_ref() else {
771 return Ok(());
772 };
773
774 if onchain.min_mint > onchain.max_mint {
775 bail!("On-chain min_mint cannot be greater than max_mint");
776 }
777 if onchain.min_melt > onchain.max_melt {
778 bail!("On-chain min_melt cannot be greater than max_melt");
779 }
780
781 match onchain.onchain_backend {
782 config::OnchainBackend::None => {}
783 #[cfg(feature = "bdk")]
784 config::OnchainBackend::Bdk => {
785 let bdk = settings.bdk.as_ref().ok_or_else(|| {
786 anyhow!("BDK onchain backend selected but [bdk] config section is missing")
787 })?;
788 bdk.validate().map_err(anyhow::Error::msg)?;
789 }
790 #[cfg(feature = "fakewallet")]
791 config::OnchainBackend::FakeWallet => {
792 if settings.fake_wallet.is_none() {
793 bail!(
794 "Fake wallet onchain backend selected but [fake_wallet] config section is missing"
795 );
796 }
797 }
798 }
799
800 Ok(())
801}
802
803fn validate_auth_config(settings: &config::Settings) -> Result<()> {
804 let Some(auth) = settings.auth.as_ref() else {
805 return Ok(());
806 };
807
808 if auth.openid_discovery.is_empty() {
809 bail!("Auth openid_discovery must be set in [auth].openid_discovery");
810 }
811 if auth.openid_client_id.is_empty() {
812 bail!("Auth openid_client_id must be set in [auth].openid_client_id");
813 }
814
815 if settings.database.engine == DatabaseEngine::Postgres {
816 let auth_db_config = settings.auth_database.as_ref().ok_or_else(|| {
817 anyhow!("Auth database configuration is required when using PostgreSQL with authentication. Set [auth_database]")
818 })?;
819 let auth_pg_config = auth_db_config.postgres.as_ref().ok_or_else(|| {
820 anyhow!("PostgreSQL auth database configuration is required when using PostgreSQL with authentication. Set [auth_database.postgres]")
821 })?;
822 if auth_pg_config.url.is_empty() {
823 bail!("Auth database PostgreSQL URL is required. Set [auth_database.postgres].url to an env: or file: secret reference");
824 }
825 }
826
827 Ok(())
828}
829
830fn validate_management_rpc_config(settings: &config::Settings) -> Result<()> {
831 #[cfg(not(feature = "management-rpc"))]
832 let _ = settings;
833
834 #[cfg(feature = "management-rpc")]
835 if let Some(rpc_settings) = settings.mint_management_rpc.as_ref() {
836 if rpc_settings.enabled {
837 let address = rpc_settings.address.as_deref().unwrap_or("127.0.0.1");
838 let port = rpc_settings.port.unwrap_or(8086);
839 format!("{address}:{port}")
840 .parse::<SocketAddr>()
841 .map_err(|err| {
842 anyhow!(
843 "Invalid mint management RPC address [mint_management_rpc].address/[mint_management_rpc].port ({address}:{port}): {err}"
844 )
845 })?;
846 }
847 }
848
849 Ok(())
850}
851
852fn validate_prometheus_config(settings: &config::Settings) -> Result<()> {
853 #[cfg(not(feature = "prometheus"))]
854 let _ = settings;
855
856 #[cfg(feature = "prometheus")]
857 if let Some(prometheus_settings) = settings.prometheus.as_ref() {
858 if prometheus_settings.enabled {
859 let address = prometheus_settings
860 .address
861 .as_deref()
862 .unwrap_or("127.0.0.1");
863 let port = prometheus_settings.port.unwrap_or(9000);
864 format!("{address}:{port}")
865 .parse::<SocketAddr>()
866 .map_err(|err| {
867 anyhow!(
868 "Invalid Prometheus address [prometheus].address/[prometheus].port ({address}:{port}): {err}"
869 )
870 })?;
871 }
872 }
873
874 Ok(())
875}
876
877pub fn load_settings_from_args(work_dir: &Path, args: &CLIArgs) -> Result<config::Settings> {
879 let mut settings = load_settings_from_sources(work_dir, args.config.clone())?;
880
881 if let Some(seed_file) = args.seed_file.as_deref() {
882 apply_seed_file(&mut settings, seed_file)?;
883 }
884
885 validate_settings(&settings)?;
886
887 Ok(settings)
888}
889
890pub fn apply_seed_file(settings: &mut config::Settings, seed_file: &Path) -> Result<()> {
892 let mnemonic = std::fs::read_to_string(seed_file)
893 .with_context(|| format!("Failed to read seed file {}", seed_file.display()))?;
894 let mnemonic = mnemonic.trim();
895
896 if mnemonic.is_empty() {
897 bail!("Seed file {} is empty", seed_file.display());
898 }
899
900 Mnemonic::parse(mnemonic)
901 .with_context(|| format!("Invalid seed phrase in seed file {}", seed_file.display()))?;
902
903 settings.info.seed = None;
904 settings.info.mnemonic = Some(mnemonic.to_owned());
905
906 #[cfg(feature = "bdk")]
907 if settings
908 .onchain
909 .as_ref()
910 .is_some_and(|onchain| onchain.onchain_backend == config::OnchainBackend::Bdk)
911 {
912 let mut bdk = settings.bdk.clone().unwrap_or_default();
913 bdk.mnemonic = Some(mnemonic.to_owned());
914 settings.bdk = Some(bdk);
915 }
916
917 #[cfg(feature = "ldk-node")]
918 if settings
919 .payment_backend
920 .iter()
921 .any(|backend| backend.backend == PaymentBackendType::LdkNode)
922 {
923 let mut ldk_node = settings.ldk_node.clone().unwrap_or_default();
924 ldk_node.ldk_node_mnemonic = Some(mnemonic.to_owned());
925 settings.ldk_node = Some(ldk_node);
926 }
927
928 Ok(())
929}
930
931async fn setup_database(
932 settings: &config::Settings,
933 _work_dir: &Path,
934 _db_password: Option<String>,
935) -> Result<(
936 DynMintDatabase,
937 Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
938 Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync>,
939 Arc<dyn KVStoreCompareAndSwap<Err = cdk_database::Error> + Send + Sync>,
940)> {
941 tracing::info!("Using database engine: {:?}", settings.database.engine);
942 match settings.database.engine {
943 #[cfg(feature = "sqlite")]
944 DatabaseEngine::Sqlite => {
945 let db = setup_sqlite_database(_work_dir, _db_password).await?;
946 let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> = db.clone();
947 let kv: Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync> = db.clone();
948 let configuration_store: Arc<
949 dyn KVStoreCompareAndSwap<Err = cdk_database::Error> + Send + Sync,
950 > = db.clone();
951 let keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync> = db;
952 Ok((localstore, keystore, kv, configuration_store))
953 }
954 #[cfg(feature = "postgres")]
955 DatabaseEngine::Postgres => {
956 let pg_config = settings.database.postgres.as_ref().ok_or_else(|| {
958 anyhow!("PostgreSQL configuration is required when using PostgreSQL engine")
959 })?;
960
961 if pg_config.url.is_empty() {
962 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");
963 }
964
965 #[cfg(feature = "postgres")]
966 let db_config = PgConfig::new(
967 pg_config.url.as_str(),
968 pg_config.tls_mode.as_deref(),
969 pg_config.max_connections,
970 pg_config.connection_timeout_seconds,
971 );
972 #[cfg(feature = "postgres")]
973 let pg_db = Arc::new(MintPgDatabase::new(db_config).await?);
974 tracing::info!("PostgreSQL database connection established");
975 #[cfg(feature = "postgres")]
976 let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> =
977 pg_db.clone();
978 #[cfg(feature = "postgres")]
979 let kv: Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync> = pg_db.clone();
980 #[cfg(feature = "postgres")]
981 let configuration_store: Arc<
982 dyn KVStoreCompareAndSwap<Err = cdk_database::Error> + Send + Sync,
983 > = pg_db.clone();
984 #[cfg(feature = "postgres")]
985 let keystore: Arc<
986 dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync,
987 > = pg_db;
988 #[cfg(feature = "postgres")]
989 return Ok((localstore, keystore, kv, configuration_store));
990
991 #[cfg(not(feature = "postgres"))]
992 bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
993 }
994 #[cfg(not(feature = "sqlite"))]
995 DatabaseEngine::Sqlite => {
996 bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
997 }
998 #[cfg(not(feature = "postgres"))]
999 DatabaseEngine::Postgres => {
1000 bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
1001 }
1002 }
1003}
1004
1005#[cfg(feature = "sqlite")]
1006async fn setup_sqlite_database(
1007 work_dir: &Path,
1008 _password: Option<String>,
1009) -> Result<Arc<MintSqliteDatabase>> {
1010 let sql_db_path = work_dir.join("cdk-mintd.sqlite");
1011 tracing::info!("SQLite database path: {}", sql_db_path.display());
1012
1013 #[cfg(not(feature = "sqlcipher"))]
1014 let db = MintSqliteDatabase::new(&sql_db_path).await?;
1015 #[cfg(feature = "sqlcipher")]
1016 let db = {
1017 let password = _password
1019 .ok_or_else(|| anyhow!("Password required when sqlcipher feature is enabled"))?;
1020 tracing::info!("Using SQLCipher encryption for SQLite database");
1021 MintSqliteDatabase::new((sql_db_path, password)).await?
1022 };
1023
1024 tracing::info!("SQLite database initialized successfully");
1025 Ok(Arc::new(db))
1026}
1027
1028async fn configure_mint_builder_with_wallet_info(
1033 settings: &config::Settings,
1034 mint_builder: MintBuilder,
1035 runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1036 work_dir: &Path,
1037 kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
1038) -> Result<(MintBuilder, ConfiguredWalletInfoProvider)> {
1039 settings
1040 .validate_backend_pairing()
1041 .map_err(anyhow::Error::msg)?;
1042
1043 let mint_builder = configure_basic_info(settings, mint_builder);
1045
1046 #[cfg(feature = "fakewallet")]
1048 if settings
1049 .payment_backend
1050 .iter()
1051 .any(|backend| backend.backend == PaymentBackendType::FakeWallet)
1052 {
1053 if let Some(_onchain) = &settings.onchain {
1054 #[cfg(feature = "bdk")]
1055 if _onchain.onchain_backend == config::OnchainBackend::Bdk {
1056 if let Some(bdk) = &settings.bdk {
1057 if let Some(network) = &bdk.network {
1058 let network = network.to_lowercase();
1059 if network == "mainnet" || network == "bitcoin" {
1060 bail!("Fake wallet cannot be used as a payment backend when On-chain is configured for Mainnet");
1061 }
1062 }
1063 }
1064 }
1065 }
1066 }
1067
1068 let mint_builder = configure_payment_backends(
1070 settings,
1071 mint_builder,
1072 runtime.clone(),
1073 work_dir,
1074 kv_store.clone(),
1075 )
1076 .await?;
1077
1078 let (mint_builder, wallet_info_provider) = configure_onchain_backend_with_wallet_info(
1080 settings,
1081 mint_builder,
1082 runtime,
1083 work_dir,
1084 kv_store,
1085 )
1086 .await?;
1087
1088 let mint_info = mint_builder.current_mint_info();
1090 let payment_methods = extract_supported_payment_methods(&mint_info);
1091
1092 let mint_builder = mint_builder
1094 .with_batch_minting(Some(DEFAULT_BATCH_MINT_SIZE), Some(payment_methods.clone()));
1095
1096 let mint_builder = configure_cache(settings, mint_builder, &payment_methods).await?;
1098
1099 let mint_builder =
1101 mint_builder.with_limits(settings.limits.max_inputs, settings.limits.max_outputs);
1102
1103 if mint_builder
1105 .current_mint_info()
1106 .nuts
1107 .nut04
1108 .methods
1109 .is_empty()
1110 {
1111 bail!("At least one payment backend must be configured");
1112 }
1113
1114 Ok((mint_builder, wallet_info_provider))
1115}
1116
1117#[cfg(test)]
1118async fn configure_mint_builder(
1119 settings: &config::Settings,
1120 mint_builder: MintBuilder,
1121 runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1122 work_dir: &Path,
1123 kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
1124) -> Result<MintBuilder> {
1125 Ok(
1126 configure_mint_builder_with_wallet_info(
1127 settings,
1128 mint_builder,
1129 runtime,
1130 work_dir,
1131 kv_store,
1132 )
1133 .await?
1134 .0,
1135 )
1136}
1137
1138fn configure_basic_info(settings: &config::Settings, mint_builder: MintBuilder) -> MintBuilder {
1140 let mut contacts = Vec::new();
1142 if let Some(nostr_key) = &settings.mint_info.contact_nostr_public_key {
1143 if !nostr_key.is_empty() {
1144 contacts.push(ContactInfo::new("nostr".to_string(), nostr_key.to_string()));
1145 }
1146 }
1147 if let Some(email) = &settings.mint_info.contact_email {
1148 if !email.is_empty() {
1149 contacts.push(ContactInfo::new("email".to_string(), email.to_string()));
1150 }
1151 }
1152
1153 let mint_version = MintVersion::new(
1155 "cdk-mintd".to_string(),
1156 CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
1157 );
1158
1159 let mut builder = mint_builder.with_version(mint_version);
1161
1162 if !settings.mint_info.name.is_empty() {
1164 builder = builder.with_name(settings.mint_info.name.clone());
1165 }
1166
1167 if !settings.mint_info.description.is_empty() {
1169 builder = builder.with_description(settings.mint_info.description.clone());
1170 }
1171
1172 if let Some(long_description) = &settings.mint_info.description_long {
1174 if !long_description.is_empty() {
1175 builder = builder.with_long_description(long_description.to_string());
1176 }
1177 }
1178
1179 for contact in contacts {
1180 builder = builder.with_contact_info(contact);
1181 }
1182
1183 if let Some(pubkey) = settings.mint_info.pubkey {
1184 builder = builder.with_pubkey(pubkey);
1185 }
1186
1187 if let Some(icon_url) = &settings.mint_info.icon_url {
1188 if !icon_url.is_empty() {
1189 builder = builder.with_icon_url(icon_url.to_string());
1190 }
1191 }
1192
1193 if let Some(motd) = &settings.mint_info.motd {
1194 if !motd.is_empty() {
1195 builder = builder.with_motd(motd.to_string());
1196 }
1197 }
1198
1199 if let Some(tos_url) = &settings.mint_info.tos_url {
1200 if !tos_url.is_empty() {
1201 builder = builder.with_tos_url(tos_url.to_string());
1202 }
1203 }
1204
1205 builder = builder.with_keyset_v2(settings.info.use_keyset_v2);
1206
1207 builder
1208}
1209async fn configure_payment_backends(
1211 settings: &config::Settings,
1212 mut mint_builder: MintBuilder,
1213 _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1214 work_dir: &Path,
1215 _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
1216) -> Result<MintBuilder> {
1217 if settings.payment_backend.is_empty() {
1218 tracing::info!("No payment backend configured");
1219 return Ok(mint_builder);
1220 }
1221
1222 #[cfg(feature = "fakewallet")]
1223 let mut configure_fake_wallet_keyset_rotations = false;
1224
1225 for backend_entry in &settings.payment_backend {
1226 let mint_melt_limits = MintMeltLimits {
1227 mint_min: backend_entry.min_mint,
1228 mint_max: backend_entry.max_mint,
1229 melt_min: backend_entry.min_melt,
1230 melt_max: backend_entry.max_melt,
1231 };
1232
1233 tracing::debug!(
1234 "Payment backend: {:?} (unit: {:?})",
1235 backend_entry.backend,
1236 backend_entry.unit
1237 );
1238
1239 match backend_entry.backend {
1240 #[cfg(feature = "cln")]
1241 PaymentBackendType::Cln => {
1242 let cln_settings = settings.cln.clone().ok_or_else(|| {
1243 anyhow!("CLN backend selected but [cln] config section is missing")
1244 })?;
1245 let cln = cln_settings
1246 .setup(
1247 settings,
1248 cdk::nuts::CurrencyUnit::Msat,
1249 None,
1250 work_dir,
1251 _kv_store.clone(),
1252 )
1253 .await?;
1254 #[cfg(feature = "prometheus")]
1255 let cln = MetricsMintPayment::new(cln);
1256
1257 mint_builder = configure_backend_for_unit(
1258 settings,
1259 mint_builder,
1260 backend_entry.unit.clone(),
1261 mint_melt_limits,
1262 Arc::new(cln),
1263 )
1264 .await?;
1265 }
1266 #[cfg(feature = "lnd")]
1267 PaymentBackendType::Lnd => {
1268 let lnd_settings = settings.lnd.clone().ok_or_else(|| {
1269 anyhow!("LND backend selected but [lnd] config section is missing")
1270 })?;
1271 let lnd = lnd_settings
1272 .setup(
1273 settings,
1274 cdk::nuts::CurrencyUnit::Msat,
1275 None,
1276 work_dir,
1277 _kv_store.clone(),
1278 )
1279 .await?;
1280 #[cfg(feature = "prometheus")]
1281 let lnd = MetricsMintPayment::new(lnd);
1282
1283 mint_builder = configure_backend_for_unit(
1284 settings,
1285 mint_builder,
1286 backend_entry.unit.clone(),
1287 mint_melt_limits,
1288 Arc::new(lnd),
1289 )
1290 .await?;
1291 }
1292 #[cfg(feature = "fakewallet")]
1293 PaymentBackendType::FakeWallet => {
1294 let fake_wallet = settings.fake_wallet.clone().ok_or_else(|| {
1295 anyhow!(
1296 "Fake wallet backend selected but [fake_wallet] config section is missing"
1297 )
1298 })?;
1299 tracing::info!("Using fake wallet: {:?}", fake_wallet);
1300
1301 let fake = fake_wallet
1302 .setup(
1303 settings,
1304 backend_entry.unit.clone(),
1305 None,
1306 work_dir,
1307 _kv_store.clone(),
1308 )
1309 .await?;
1310 #[cfg(feature = "prometheus")]
1311 let fake = MetricsMintPayment::new(fake);
1312
1313 mint_builder = configure_backend_for_unit(
1314 settings,
1315 mint_builder,
1316 backend_entry.unit.clone(),
1317 mint_melt_limits,
1318 Arc::new(fake),
1319 )
1320 .await?;
1321
1322 configure_fake_wallet_keyset_rotations = true;
1323 }
1324 #[cfg(feature = "grpc-processor")]
1325 PaymentBackendType::GrpcProcessor => {
1326 let grpc_processor = settings.grpc_processor.clone().ok_or_else(|| {
1327 anyhow!(
1328 "gRPC payment processor backend selected but [grpc_processor] config section is missing"
1329 )
1330 })?;
1331
1332 tracing::info!(
1333 "Attempting to start with gRPC payment processor at {}:{}.",
1334 grpc_processor.address,
1335 grpc_processor.port
1336 );
1337
1338 let processor = grpc_processor
1339 .setup(settings, backend_entry.unit.clone(), None, work_dir, None)
1340 .await?;
1341 #[cfg(feature = "prometheus")]
1342 let processor = MetricsMintPayment::new(processor);
1343
1344 mint_builder = configure_backend_for_unit(
1345 settings,
1346 mint_builder,
1347 backend_entry.unit.clone(),
1348 mint_melt_limits,
1349 Arc::new(processor),
1350 )
1351 .await?;
1352 }
1353 #[cfg(feature = "ldk-node")]
1354 PaymentBackendType::LdkNode => {
1355 let ldk_node_settings = settings.ldk_node.clone().ok_or_else(|| {
1356 anyhow!("LDK Node backend selected but [ldk_node] config section is missing")
1357 })?;
1358 tracing::info!("Using LDK Node backend: {:?}", ldk_node_settings);
1359
1360 let ldk_node = ldk_node_settings
1361 .setup(
1362 settings,
1363 backend_entry.unit.clone(),
1364 _runtime.clone(),
1365 work_dir,
1366 _kv_store.clone(),
1367 )
1368 .await?;
1369
1370 mint_builder = configure_backend_for_unit(
1371 settings,
1372 mint_builder,
1373 backend_entry.unit.clone(),
1374 mint_melt_limits,
1375 Arc::new(ldk_node),
1376 )
1377 .await?;
1378 }
1379 PaymentBackendType::None => {
1380 tracing::info!(
1381 "No payment backend configured for unit {:?}",
1382 backend_entry.unit
1383 );
1384 }
1385 };
1386 }
1387
1388 #[cfg(feature = "fakewallet")]
1389 if configure_fake_wallet_keyset_rotations {
1390 let fake_wallet = settings.fake_wallet.as_ref().ok_or_else(|| {
1391 anyhow!("Fake wallet backend selected but [fake_wallet] config section is missing")
1392 })?;
1393 mint_builder = configure_fake_wallet_keyset_rotations_once(mint_builder, fake_wallet);
1394 }
1395
1396 Ok(mint_builder)
1397}
1398
1399#[cfg(feature = "fakewallet")]
1400fn configure_fake_wallet_keyset_rotations_once(
1401 mut mint_builder: MintBuilder,
1402 fake_wallet: &config::FakeWallet,
1403) -> MintBuilder {
1404 for rotation_cfg in &fake_wallet.keyset_rotations {
1405 use cdk::mint::KeysetRotation;
1406
1407 let amounts = cdk::mint::UnitConfig::default().amounts;
1408 let final_expiry = if rotation_cfg.expired {
1409 Some(cdk::util::unix_time().saturating_sub(3600))
1410 } else {
1411 None
1412 };
1413
1414 mint_builder = mint_builder.with_keyset_rotation(KeysetRotation {
1415 unit: rotation_cfg.unit.clone(),
1416 amounts,
1417 input_fee_ppk: rotation_cfg.input_fee_ppk,
1418 use_keyset_v2: rotation_cfg.version == "v2",
1419 final_expiry,
1420 });
1421 }
1422
1423 mint_builder
1424}
1425
1426async fn configure_onchain_backend_with_wallet_info(
1428 settings: &config::Settings,
1429 #[cfg_attr(not(feature = "bdk"), allow(unused_mut))] mut mint_builder: MintBuilder,
1430 _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1431 _work_dir: &Path,
1432 _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
1433) -> Result<(MintBuilder, ConfiguredWalletInfoProvider)> {
1434 use config::OnchainBackend;
1435 #[cfg(feature = "bdk")]
1436 use setup::OnchainBackendSetup;
1437
1438 #[cfg(all(feature = "management-rpc", feature = "bdk"))]
1439 let mut wallet_info_provider = no_wallet_info_provider();
1440
1441 if let Some(onchain_settings) = &settings.onchain {
1442 match onchain_settings.onchain_backend {
1443 #[cfg(feature = "bdk")]
1444 OnchainBackend::Bdk => {
1445 let mint_melt_limits = MintMeltLimits {
1446 mint_min: onchain_settings.min_mint,
1447 mint_max: onchain_settings.max_mint,
1448 melt_min: onchain_settings.min_melt,
1449 melt_max: onchain_settings.max_melt,
1450 };
1451
1452 let bdk_settings = settings.bdk.clone().ok_or_else(|| {
1453 anyhow!("BDK onchain backend selected but [bdk] config section is missing")
1454 })?;
1455 let bdk = bdk_settings
1456 .setup(
1457 settings,
1458 cdk::nuts::CurrencyUnit::Sat,
1459 None,
1460 _work_dir,
1461 _kv_store,
1462 )
1463 .await?;
1464 let bdk = Arc::new(bdk);
1465
1466 #[cfg(feature = "management-rpc")]
1467 {
1468 wallet_info_provider = Some(Arc::new(BdkWalletInfoProvider {
1469 bdk: Arc::clone(&bdk),
1470 }));
1471 }
1472
1473 mint_builder = configure_backend_for_unit(
1474 settings,
1475 mint_builder,
1476 cdk::nuts::CurrencyUnit::Sat,
1477 mint_melt_limits,
1478 bdk,
1479 )
1480 .await?;
1481 }
1482 OnchainBackend::None => {}
1483 #[cfg(feature = "fakewallet")]
1484 OnchainBackend::FakeWallet => {
1485 let has_payment_backend = settings
1486 .payment_backend
1487 .iter()
1488 .any(|backend| backend.backend != PaymentBackendType::None);
1489 let has_real_payment_backend = settings.payment_backend.iter().any(|backend| {
1490 !matches!(
1491 backend.backend,
1492 PaymentBackendType::None | PaymentBackendType::FakeWallet
1493 )
1494 });
1495
1496 if !has_payment_backend {
1497 let mint_melt_limits = MintMeltLimits {
1498 mint_min: onchain_settings.min_mint,
1499 mint_max: onchain_settings.max_mint,
1500 melt_min: onchain_settings.min_melt,
1501 melt_max: onchain_settings.max_melt,
1502 };
1503 let fake_wallet = settings
1504 .fake_wallet
1505 .clone()
1506 .ok_or_else(|| anyhow!("Fake wallet config section is missing"))?;
1507
1508 for unit in fake_wallet.clone().supported_units {
1509 let fake = fake_wallet
1510 .setup(settings, unit.clone(), None, _work_dir, _kv_store.clone())
1511 .await?;
1512 #[cfg(feature = "prometheus")]
1513 let fake = MetricsMintPayment::new(fake);
1514
1515 mint_builder = configure_backend_for_methods(
1516 settings,
1517 mint_builder,
1518 unit,
1519 mint_melt_limits,
1520 Arc::new(fake),
1521 vec![PaymentMethod::Known(KnownMethod::Onchain)],
1522 )
1523 .await?;
1524 }
1525 } else if has_real_payment_backend {
1526 bail!(
1527 "onchain_backend = \"fakewallet\" cannot be combined with a real payment backend"
1528 );
1529 }
1530 }
1531 }
1532 }
1533
1534 #[cfg(all(feature = "management-rpc", feature = "bdk"))]
1535 {
1536 Ok((mint_builder, wallet_info_provider))
1537 }
1538 #[cfg(not(all(feature = "management-rpc", feature = "bdk")))]
1539 {
1540 Ok((mint_builder, no_wallet_info_provider()))
1541 }
1542}
1543
1544#[cfg(test)]
1545async fn configure_onchain_backend(
1546 settings: &config::Settings,
1547 mint_builder: MintBuilder,
1548 runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1549 work_dir: &Path,
1550 kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
1551) -> Result<MintBuilder> {
1552 Ok(configure_onchain_backend_with_wallet_info(
1553 settings,
1554 mint_builder,
1555 runtime,
1556 work_dir,
1557 kv_store,
1558 )
1559 .await?
1560 .0)
1561}
1562
1563async fn configure_backend_for_unit(
1565 settings: &config::Settings,
1566 mint_builder: MintBuilder,
1567 unit: cdk::nuts::CurrencyUnit,
1568 mint_melt_limits: MintMeltLimits,
1569 backend: Arc<dyn MintPayment<Err = cdk_common::payment::Error> + Send + Sync>,
1570) -> Result<MintBuilder> {
1571 let payment_settings = backend.get_settings().await?;
1572 validate_backend_unit(&unit, &payment_settings.unit)?;
1573
1574 let mut methods = Vec::new();
1575
1576 if payment_settings.bolt11.is_some() {
1578 methods.push(PaymentMethod::Known(KnownMethod::Bolt11));
1579 }
1580
1581 if payment_settings.bolt12.is_some() {
1583 methods.push(PaymentMethod::Known(KnownMethod::Bolt12));
1584 }
1585
1586 if payment_settings.onchain.is_some() {
1588 methods.push(PaymentMethod::Known(KnownMethod::Onchain));
1589 }
1590
1591 for method_name in payment_settings.custom.keys() {
1593 methods.push(PaymentMethod::from(method_name.as_str()));
1594 }
1595
1596 configure_backend_for_methods(
1597 settings,
1598 mint_builder,
1599 unit,
1600 mint_melt_limits,
1601 backend,
1602 methods,
1603 )
1604 .await
1605}
1606
1607async fn configure_backend_for_methods(
1608 settings: &config::Settings,
1609 mut mint_builder: MintBuilder,
1610 unit: cdk::nuts::CurrencyUnit,
1611 mint_melt_limits: MintMeltLimits,
1612 backend: Arc<dyn MintPayment<Err = cdk_common::payment::Error> + Send + Sync>,
1613 methods: Vec<PaymentMethod>,
1614) -> Result<MintBuilder> {
1615 for method in &methods {
1617 mint_builder
1618 .add_payment_processor(
1619 unit.clone(),
1620 method.clone(),
1621 mint_melt_limits,
1622 backend.clone(),
1623 )
1624 .await?;
1625 }
1626
1627 for method in &methods {
1629 let method_str = method.to_string();
1630 let nut17_supported = match method_str.as_str() {
1631 "bolt11" => SupportedMethods::default_bolt11(unit.clone()),
1632 "bolt12" => SupportedMethods::default_bolt12(unit.clone()),
1633 _ => SupportedMethods::default_custom(method.clone(), unit.clone()),
1634 };
1635 mint_builder = mint_builder.with_supported_websockets(nut17_supported);
1636 }
1637
1638 if let Some(input_fee) = settings.info.input_fee_ppk {
1639 mint_builder.set_unit_fee(&unit, input_fee)?;
1640 }
1641
1642 Ok(mint_builder)
1643}
1644
1645fn validate_backend_unit(
1646 configured_unit: &cdk::nuts::CurrencyUnit,
1647 backend_unit: &str,
1648) -> Result<()> {
1649 let backend_unit = cdk::nuts::CurrencyUnit::from_str(backend_unit)
1650 .with_context(|| format!("Payment backend returned invalid unit `{backend_unit}`"))?;
1651
1652 if units_are_compatible(&backend_unit, configured_unit) {
1653 return Ok(());
1654 }
1655
1656 bail!(
1657 "Payment backend reports unit {} but config registers unit {}; only matching units or sat/msat conversions are supported",
1658 backend_unit,
1659 configured_unit
1660 )
1661}
1662
1663fn units_are_compatible(
1664 backend_unit: &cdk::nuts::CurrencyUnit,
1665 configured_unit: &cdk::nuts::CurrencyUnit,
1666) -> bool {
1667 backend_unit == configured_unit
1668 || matches!(
1669 (backend_unit, configured_unit),
1670 (cdk::nuts::CurrencyUnit::Sat, cdk::nuts::CurrencyUnit::Msat)
1671 | (cdk::nuts::CurrencyUnit::Msat, cdk::nuts::CurrencyUnit::Sat)
1672 )
1673}
1674
1675async fn configure_cache(
1677 settings: &config::Settings,
1678 mint_builder: MintBuilder,
1679 payment_methods: &[String],
1680) -> Result<MintBuilder> {
1681 let mut cached_endpoints = vec![
1682 CachedEndpoint::new(NUT19Method::Post, NUT19Path::Swap),
1684 ];
1685
1686 for method in payment_methods {
1688 cached_endpoints.push(CachedEndpoint::new(
1690 NUT19Method::Post,
1691 NUT19Path::custom_mint(method),
1692 ));
1693 cached_endpoints.push(CachedEndpoint::new(
1694 NUT19Method::Post,
1695 NUT19Path::custom_melt(method),
1696 ));
1697 }
1698
1699 let cache: HttpCache = HttpCache::from_config(settings.info.http_cache.clone()).await?;
1700 Ok(mint_builder.with_cache(Some(cache.ttl.as_secs()), cached_endpoints))
1701}
1702
1703async fn setup_authentication(
1704 settings: &config::Settings,
1705 _work_dir: &Path,
1706 mut mint_builder: MintBuilder,
1707 _password: Option<String>,
1708) -> Result<(
1709 MintBuilder,
1710 Option<cdk_common::database::DynMintAuthDatabase>,
1711)> {
1712 if let Some(auth_settings) = settings.auth.clone() {
1713 use cdk_common::database::DynMintAuthDatabase;
1714
1715 tracing::info!("Auth settings are defined. {:?}", auth_settings);
1716 let auth_localstore: DynMintAuthDatabase = match settings.database.engine {
1717 #[cfg(feature = "sqlite")]
1718 DatabaseEngine::Sqlite => {
1719 #[cfg(feature = "sqlite")]
1720 {
1721 let sql_db_path = _work_dir.join("cdk-mintd-auth.sqlite");
1722 #[cfg(not(feature = "sqlcipher"))]
1723 let sqlite_db = MintSqliteAuthDatabase::new(&sql_db_path).await?;
1724 #[cfg(feature = "sqlcipher")]
1725 let sqlite_db = {
1726 let password = _password.clone().ok_or_else(|| {
1728 anyhow!("Password required when sqlcipher feature is enabled")
1729 })?;
1730 MintSqliteAuthDatabase::new((sql_db_path, password)).await?
1731 };
1732
1733 Arc::new(sqlite_db)
1734 }
1735 #[cfg(not(feature = "sqlite"))]
1736 {
1737 bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
1738 }
1739 }
1740 #[cfg(feature = "postgres")]
1741 DatabaseEngine::Postgres => {
1742 #[cfg(feature = "postgres")]
1743 {
1744 let auth_db_config = settings.auth_database.as_ref().ok_or_else(|| {
1746 anyhow!("Auth database configuration is required when using PostgreSQL with authentication. Set [auth_database]")
1747 })?;
1748
1749 let auth_pg_config = auth_db_config.postgres.as_ref().ok_or_else(|| {
1750 anyhow!("PostgreSQL auth database configuration is required when using PostgreSQL with authentication. Set [auth_database.postgres]")
1751 })?;
1752
1753 if auth_pg_config.url.is_empty() {
1754 bail!("Auth database PostgreSQL URL is required and cannot be empty. Set [auth_database.postgres].url to an env: or file: secret reference");
1755 }
1756
1757 let auth_db_config = PgConfig::new(
1758 auth_pg_config.url.as_str(),
1759 auth_pg_config.tls_mode.as_deref(),
1760 auth_pg_config.max_connections,
1761 auth_pg_config.connection_timeout_seconds,
1762 );
1763 Arc::new(MintPgAuthDatabase::new(auth_db_config).await?)
1764 }
1765 #[cfg(not(feature = "postgres"))]
1766 {
1767 bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
1768 }
1769 }
1770 #[cfg(not(feature = "sqlite"))]
1771 DatabaseEngine::Sqlite => {
1772 bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
1773 }
1774 #[cfg(not(feature = "postgres"))]
1775 DatabaseEngine::Postgres => {
1776 bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
1777 }
1778 };
1779
1780 let mut protected_endpoints = HashMap::new();
1781 let mut blind_auth_endpoints = vec![];
1782 let mut clear_auth_endpoints = vec![];
1783 let mut unprotected_endpoints = vec![];
1784
1785 let mint_blind_auth_endpoint =
1786 ProtectedEndpoint::new(Method::Post, RoutePath::MintBlindAuth);
1787
1788 protected_endpoints.insert(mint_blind_auth_endpoint.clone(), AuthRequired::Clear);
1789
1790 clear_auth_endpoints.push(mint_blind_auth_endpoint);
1791
1792 let mut add_endpoint = |endpoint: ProtectedEndpoint, auth_type: &AuthType| {
1794 match auth_type {
1795 AuthType::Blind => {
1796 protected_endpoints.insert(endpoint.clone(), AuthRequired::Blind);
1797 blind_auth_endpoints.push(endpoint);
1798 }
1799 AuthType::Clear => {
1800 protected_endpoints.insert(endpoint.clone(), AuthRequired::Clear);
1801 clear_auth_endpoints.push(endpoint);
1802 }
1803 AuthType::None => {
1804 unprotected_endpoints.push(endpoint);
1805 }
1806 };
1807 };
1808
1809 {
1816 let swap_protected_endpoint = ProtectedEndpoint::new(Method::Post, RoutePath::Swap);
1817 add_endpoint(swap_protected_endpoint, &auth_settings.swap);
1818 }
1819
1820 {
1822 let restore_protected_endpoint =
1823 ProtectedEndpoint::new(Method::Post, RoutePath::Restore);
1824 add_endpoint(restore_protected_endpoint, &auth_settings.restore);
1825 }
1826
1827 {
1829 let state_protected_endpoint =
1830 ProtectedEndpoint::new(Method::Post, RoutePath::Checkstate);
1831 add_endpoint(state_protected_endpoint, &auth_settings.check_proof_state);
1832 }
1833
1834 {
1836 let ws_protected_endpoint = ProtectedEndpoint::new(Method::Get, RoutePath::Ws);
1837 add_endpoint(ws_protected_endpoint, &auth_settings.websocket_auth);
1838 }
1839
1840 mint_builder = mint_builder.with_auth(
1846 auth_localstore.clone(),
1847 auth_settings.openid_discovery,
1848 auth_settings.openid_client_id,
1849 clear_auth_endpoints,
1850 );
1851 mint_builder =
1852 mint_builder.with_blind_auth(auth_settings.mint_max_bat, blind_auth_endpoints);
1853
1854 let mut tx = auth_localstore.begin_transaction().await?;
1855
1856 if !unprotected_endpoints.is_empty() {
1857 tx.remove_protected_endpoints(unprotected_endpoints).await?;
1858 }
1859 if !protected_endpoints.is_empty() {
1860 tx.add_protected_endpoints(protected_endpoints).await?;
1861 }
1862 tx.commit().await?;
1863
1864 Ok((mint_builder, Some(auth_localstore)))
1865 } else {
1866 Ok((mint_builder, None))
1867 }
1868}
1869
1870async fn build_mint(
1872 settings: &config::Settings,
1873 keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
1874 mint_builder: MintBuilder,
1875 validated_signing_source: Option<&ValidatedSigningSource>,
1876) -> Result<Mint> {
1877 if let Some(signatory) = settings.enabled_signatory() {
1878 let tls_dir = signatory.tls_dir.clone();
1879
1880 if tls_dir.is_none() {
1881 if !signatory.allow_insecure {
1882 bail!(
1883 "gRPC signatory TLS is not configured. Set [signatory].tls_dir or \
1884 [signatory].allow_insecure = true to connect without TLS"
1885 );
1886 }
1887
1888 tracing::warn!(
1889 "No gRPC signatory TLS directory configured; connecting without TLS because \
1890 allow_insecure is true"
1891 );
1892 }
1893
1894 let remote_signatory = match validated_signing_source
1895 .and_then(|validated| validated.remote_signatory.clone())
1896 {
1897 Some(remote_signatory) => {
1898 tracing::info!(
1899 "Using the remote signatory connection validated during configuration startup"
1900 );
1901 remote_signatory
1902 }
1903 None => {
1904 tracing::info!(
1905 "Connecting to remote signatory at {}:{} with TLS directory {:?}",
1906 signatory.address,
1907 signatory.port,
1908 tls_dir
1909 );
1910 Arc::new(
1911 cdk_signatory::SignatoryRpcClient::new(
1912 &signatory.address,
1913 signatory.port,
1914 tls_dir,
1915 )
1916 .await?,
1917 )
1918 }
1919 };
1920 if let Some(validated) = validated_signing_source {
1921 ensure_signatory_identity(&remote_signatory, validated.expected_pubkey).await?;
1922 }
1923
1924 Ok(mint_builder.build_with_signatory(remote_signatory).await?)
1925 } else if let Some(seed) = settings.info.seed.clone().filter(|seed| !seed.is_empty()) {
1926 if validated_signing_source.is_some_and(|validated| validated.remote_signatory.is_some()) {
1927 bail!("Validated remote signatory provided for local signing configuration");
1928 }
1929 let seed_bytes: Vec<u8> = seed.into();
1930 Ok(mint_builder.build_with_seed(keystore, &seed_bytes).await?)
1931 } else if let Some(mnemonic) = settings
1932 .info
1933 .mnemonic
1934 .clone()
1935 .map(|s| Mnemonic::from_str(&s))
1936 .transpose()?
1937 {
1938 Ok(mint_builder
1939 .build_with_seed(keystore, &mnemonic.to_seed_normalized(""))
1940 .await?)
1941 } else {
1942 bail!("No seed nor remote signatory set");
1943 }
1944}
1945
1946async fn ensure_signatory_identity(
1947 signatory: &DynSignatory,
1948 expected_pubkey: cdk::nuts::PublicKey,
1949) -> Result<()> {
1950 let actual_pubkey = signatory.keysets().await?.pubkey;
1951 if actual_pubkey != expected_pubkey {
1952 return Err(config_service::ConfigurationServiceError::SigningIdentityChange.into());
1953 }
1954 Ok(())
1955}
1956
1957async fn reconcile_canonical_configuration(
1958 mint: &Mint,
1959 mut configured_mint_info: cdk::nuts::MintInfo,
1960 configured_quote_ttl: QuoteTTL,
1961 preserve_database_values: bool,
1962) -> Result<()> {
1963 if !preserve_database_values {
1964 tracing::info!(
1965 "Applying mint info and quote TTL from the database-backed configuration document."
1966 );
1967 if let Ok(stored_mint_info) = mint.mint_info().await {
1968 if configured_mint_info.pubkey.is_none() {
1969 configured_mint_info.pubkey = stored_mint_info.pubkey;
1970 }
1971 }
1972 mint.set_mint_info_and_quote_ttl(configured_mint_info, configured_quote_ttl)
1973 .await?;
1974 return Ok(());
1975 }
1976
1977 if mint.mint_info().await.is_err() {
1978 tracing::info!("Mint info not set on mint, setting.");
1979 mint.set_mint_info_and_quote_ttl(configured_mint_info, configured_quote_ttl)
1980 .await?;
1981 return Ok(());
1982 }
1983
1984 if !mint.quote_ttl_is_persisted().await? {
1985 mint.set_quote_ttl(configured_quote_ttl).await?;
1986 }
1987 let mint_version = MintVersion::new(
1988 "cdk-mintd".to_string(),
1989 CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
1990 );
1991 let mut stored_mint_info = mint.mint_info().await?;
1992 stored_mint_info.version = Some(mint_version);
1993 mint.set_mint_info(stored_mint_info).await?;
1994 tracing::info!("Preserving RPC-managed mint info from the database.");
1995 Ok(())
1996}
1997
1998struct PreparedMintd {
2001 mint: Arc<cdk::mint::Mint>,
2002 #[cfg(feature = "prometheus")]
2003 prometheus: Option<config::Prometheus>,
2004 mint_service: Router,
2005 listen_addr: String,
2006 listen_port: u16,
2007 activation: Option<ConfigurationActivation>,
2008 shutdown_tx: tokio::sync::broadcast::Sender<()>,
2009 #[cfg(feature = "management-rpc")]
2011 rpc_to_start: Option<(cdk_mint_rpc::MintRPCServer, Option<PathBuf>)>,
2012}
2013
2014struct RunningMintd {
2017 mint: Arc<cdk::mint::Mint>,
2018 mint_service: Router,
2019 listener: tokio::net::TcpListener,
2020 shutdown_tx: tokio::sync::broadcast::Sender<()>,
2021 #[cfg(feature = "prometheus")]
2022 prometheus_handle: Option<tokio::task::JoinHandle<()>>,
2023 #[cfg(feature = "management-rpc")]
2024 rpc_server: Option<cdk_mint_rpc::MintRPCServer>,
2025}
2026
2027impl PreparedMintd {
2028 #[allow(clippy::too_many_arguments)]
2031 async fn prepare(
2032 mint: Arc<cdk::mint::Mint>,
2033 settings: &config::Settings,
2034 _work_dir: &Path,
2035 _wallet_info_provider: ConfiguredWalletInfoProvider,
2036 mint_builder_info: cdk::nuts::MintInfo,
2037 routers: Vec<Router>,
2038 auth_localstore: Option<cdk_common::database::DynMintAuthDatabase>,
2039 activation: Option<ConfigurationActivation>,
2040 ) -> Result<Self> {
2041 let listen_addr = settings.info.listen_host.clone();
2042 let listen_port = settings.info.listen_port;
2043 let cache: HttpCache = HttpCache::from_config(settings.info.http_cache.clone()).await?;
2044
2045 #[cfg(feature = "management-rpc")]
2046 let mut rpc_enabled = false;
2047 #[cfg(not(feature = "management-rpc"))]
2048 let rpc_enabled = false;
2049
2050 #[cfg(feature = "management-rpc")]
2051 let mut rpc_to_start = None;
2052
2053 #[cfg(feature = "management-rpc")]
2054 {
2055 if let Some(rpc_settings) = settings.mint_management_rpc.clone() {
2056 if rpc_settings.enabled {
2057 let addr = rpc_settings.address.unwrap_or("127.0.0.1".to_string());
2058 let port = rpc_settings.port.unwrap_or(8086);
2059 let mut mint_rpc = cdk_mint_rpc::MintRPCServer::new(&addr, port, mint.clone())?
2060 .with_mint_quote_payment_override(
2061 rpc_settings.allow_mint_quote_payment_override,
2062 );
2063 if let Some(activation) = activation.as_ref() {
2064 mint_rpc =
2065 mint_rpc.with_mutation_guard(Arc::new(ConfigurationMutationGuard {
2066 service: activation.service.clone(),
2067 }));
2068 }
2069 #[cfg(feature = "bdk")]
2070 if let Some(provider) = _wallet_info_provider.clone() {
2071 mint_rpc = mint_rpc.with_wallet_info_provider(provider);
2072 }
2073
2074 let tls_dir = rpc_settings.tls_dir.unwrap_or(_work_dir.join("tls"));
2075
2076 let tls_dir = if tls_dir.exists() {
2077 Some(tls_dir)
2078 } else if rpc_settings.allow_insecure {
2079 tracing::warn!(
2080 "TLS directory does not exist: {}. Starting RPC server in INSECURE mode without TLS encryption because allow_insecure is true",
2081 tls_dir.display()
2082 );
2083 None
2084 } else {
2085 bail!(
2086 "Management RPC TLS directory does not exist: {}. Set \
2087 [mint_management_rpc].tls_dir or \
2088 [mint_management_rpc].allow_insecure = true to start without \
2089 TLS",
2090 tls_dir.display()
2091 );
2092 };
2093
2094 rpc_to_start = Some((mint_rpc, tls_dir));
2095 rpc_enabled = true;
2096 }
2097 }
2098 }
2099
2100 let desired_quote_ttl: QuoteTTL = settings.info.quote_ttl.unwrap_or_default();
2102
2103 let preserve_database_values = match activation.as_ref() {
2104 Some(activation) => activation.preserves_database_values(rpc_enabled),
2105 None => rpc_enabled,
2109 };
2110
2111 reconcile_canonical_configuration(
2112 mint.as_ref(),
2113 mint_builder_info,
2114 desired_quote_ttl,
2115 preserve_database_values,
2116 )
2117 .await?;
2118
2119 let mint_info = mint.mint_info().await?;
2120 let nut04_methods = mint_info.nuts.nut04.supported_methods();
2121 let nut05_methods = mint_info.nuts.nut05.supported_methods();
2122
2123 let mut custom_methods = mint.get_custom_payment_methods().await?;
2125
2126 let bolt11_method = PaymentMethod::Known(KnownMethod::Bolt11);
2128 let bolt11_supported =
2129 nut04_methods.contains(&&bolt11_method) || nut05_methods.contains(&&bolt11_method);
2130 let bolt12_method = PaymentMethod::Known(KnownMethod::Bolt12);
2132 let bolt12_supported =
2133 nut04_methods.contains(&&bolt12_method) || nut05_methods.contains(&&bolt12_method);
2134
2135 let onchain_method = PaymentMethod::Known(KnownMethod::Onchain);
2137 let onchain_supported =
2138 nut04_methods.contains(&&onchain_method) || nut05_methods.contains(&&onchain_method);
2139
2140 if bolt11_supported
2141 && !custom_methods.contains(&PaymentMethod::Known(KnownMethod::Bolt11).to_string())
2142 {
2143 custom_methods.push(PaymentMethod::Known(KnownMethod::Bolt11).to_string());
2144 }
2145 if bolt12_supported
2146 && !custom_methods.contains(&PaymentMethod::Known(KnownMethod::Bolt12).to_string())
2147 {
2148 custom_methods.push(PaymentMethod::Known(KnownMethod::Bolt12).to_string());
2149 }
2150 if onchain_supported
2151 && !custom_methods.contains(&PaymentMethod::Known(KnownMethod::Onchain).to_string())
2152 {
2153 custom_methods.push(PaymentMethod::Known(KnownMethod::Onchain).to_string());
2154 }
2155
2156 tracing::info!("Payment methods: {:?}", custom_methods);
2157
2158 if let (Some(ref auth_settings), Some(auth_db)) = (&settings.auth, &auth_localstore) {
2160 if auth_settings.auth_enabled {
2161 use std::collections::HashMap;
2162
2163 use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
2164 use cdk::nuts::AuthRequired;
2165
2166 use crate::config::AuthType;
2167
2168 let existing_endpoints = auth_db.get_auth_for_endpoints().await?;
2171 let payment_method_endpoints_to_remove: Vec<ProtectedEndpoint> = existing_endpoints
2172 .keys()
2173 .filter(|endpoint| {
2174 matches!(
2175 endpoint.path,
2176 RoutePath::MintQuote(_)
2177 | RoutePath::Mint(_)
2178 | RoutePath::MeltQuote(_)
2179 | RoutePath::Melt(_)
2180 )
2181 })
2182 .cloned()
2183 .collect();
2184
2185 if !payment_method_endpoints_to_remove.is_empty() {
2186 tracing::debug!(
2187 "Removing {} old payment method endpoints from database",
2188 payment_method_endpoints_to_remove.len()
2189 );
2190 let mut tx = auth_db.begin_transaction().await?;
2191 tx.remove_protected_endpoints(payment_method_endpoints_to_remove)
2192 .await?;
2193 tx.commit().await?;
2194 }
2195
2196 if !custom_methods.is_empty() {
2198 let mut protected_endpoints = HashMap::new();
2199
2200 for method_name in &custom_methods {
2201 tracing::debug!(
2202 "Adding auth endpoints for payment method: {}",
2203 method_name
2204 );
2205
2206 let mint_quote_auth = match auth_settings.get_mint_quote {
2208 AuthType::Clear => Some(AuthRequired::Clear),
2209 AuthType::Blind => Some(AuthRequired::Blind),
2210 AuthType::None => None,
2211 };
2212
2213 let check_mint_quote_auth = match auth_settings.check_mint_quote {
2214 AuthType::Clear => Some(AuthRequired::Clear),
2215 AuthType::Blind => Some(AuthRequired::Blind),
2216 AuthType::None => None,
2217 };
2218
2219 let mint_auth = match auth_settings.mint {
2220 AuthType::Clear => Some(AuthRequired::Clear),
2221 AuthType::Blind => Some(AuthRequired::Blind),
2222 AuthType::None => None,
2223 };
2224
2225 let melt_quote_auth = match auth_settings.get_melt_quote {
2226 AuthType::Clear => Some(AuthRequired::Clear),
2227 AuthType::Blind => Some(AuthRequired::Blind),
2228 AuthType::None => None,
2229 };
2230
2231 let check_melt_quote_auth = match auth_settings.check_melt_quote {
2232 AuthType::Clear => Some(AuthRequired::Clear),
2233 AuthType::Blind => Some(AuthRequired::Blind),
2234 AuthType::None => None,
2235 };
2236
2237 let melt_auth = match auth_settings.melt {
2238 AuthType::Clear => Some(AuthRequired::Clear),
2239 AuthType::Blind => Some(AuthRequired::Blind),
2240 AuthType::None => None,
2241 };
2242
2243 if let Some(auth) = mint_quote_auth {
2245 protected_endpoints.insert(
2246 ProtectedEndpoint::new(
2247 Method::Post,
2248 RoutePath::MintQuote(method_name.clone()),
2249 ),
2250 auth,
2251 );
2252 }
2253 if let Some(auth) = check_mint_quote_auth {
2254 protected_endpoints.insert(
2255 ProtectedEndpoint::new(
2256 Method::Get,
2257 RoutePath::MintQuote(method_name.clone()),
2258 ),
2259 auth,
2260 );
2261 }
2262 if let Some(auth) = mint_auth {
2263 protected_endpoints.insert(
2264 ProtectedEndpoint::new(
2265 Method::Post,
2266 RoutePath::Mint(method_name.clone()),
2267 ),
2268 auth,
2269 );
2270 }
2271 if let Some(auth) = melt_quote_auth {
2272 protected_endpoints.insert(
2273 ProtectedEndpoint::new(
2274 Method::Post,
2275 RoutePath::MeltQuote(method_name.clone()),
2276 ),
2277 auth,
2278 );
2279 }
2280 if let Some(auth) = check_melt_quote_auth {
2281 protected_endpoints.insert(
2282 ProtectedEndpoint::new(
2283 Method::Get,
2284 RoutePath::MeltQuote(method_name.clone()),
2285 ),
2286 auth,
2287 );
2288 }
2289 if let Some(auth) = melt_auth {
2290 protected_endpoints.insert(
2291 ProtectedEndpoint::new(
2292 Method::Post,
2293 RoutePath::Melt(method_name.clone()),
2294 ),
2295 auth,
2296 );
2297 }
2298 }
2299
2300 if !protected_endpoints.is_empty() {
2302 let mut tx = auth_db.begin_transaction().await?;
2303 tx.add_protected_endpoints(protected_endpoints).await?;
2304 tx.commit().await?;
2305 }
2306 }
2307 }
2308 }
2309
2310 let v1_service = cdk_axum::create_mint_router_with_custom_cache(
2311 Arc::clone(&mint),
2312 cache,
2313 custom_methods,
2314 settings.info.enable_info_page.unwrap_or(true),
2315 )
2316 .await?;
2317
2318 let mut mint_service = Router::new()
2319 .merge(v1_service)
2320 .layer(DefaultBodyLimit::max(REQUEST_BODY_LIMIT_BYTES))
2321 .layer(
2322 ServiceBuilder::new()
2323 .layer(RequestDecompressionLayer::new())
2324 .layer(CompressionLayer::new()),
2325 )
2326 .layer(TraceLayer::new_for_http());
2327
2328 for router in routers {
2329 mint_service = mint_service.merge(router);
2330 }
2331
2332 let (shutdown_tx, _) = tokio::sync::broadcast::channel::<()>(1);
2334
2335 Ok(Self {
2336 mint,
2337 #[cfg(feature = "prometheus")]
2338 prometheus: settings.prometheus.clone(),
2339 mint_service,
2340 listen_addr,
2341 listen_port,
2342 activation,
2343 shutdown_tx,
2344 #[cfg(feature = "management-rpc")]
2345 rpc_to_start,
2346 })
2347 }
2348
2349 async fn activate(self) -> Result<RunningMintd> {
2352 #[cfg(feature = "management-rpc")]
2353 let rpc_server = {
2354 if let Some((mut mint_rpc, tls_dir)) = self.rpc_to_start {
2355 mint_rpc.start(tls_dir).await?;
2356 Some(mint_rpc)
2357 } else {
2358 None
2359 }
2360 };
2361
2362 #[cfg(feature = "prometheus")]
2364 let prometheus_handle = {
2365 if let Some(prometheus_settings) = &self.prometheus {
2366 if prometheus_settings.enabled {
2367 let addr = prometheus_settings
2368 .address
2369 .clone()
2370 .unwrap_or("127.0.0.1".to_string());
2371 let port = prometheus_settings.port.unwrap_or(9000);
2372
2373 let address = format!("{addr}:{port}")
2374 .parse()
2375 .with_context(|| format!("Invalid Prometheus address {addr}:{port}"))?;
2376
2377 let server = cdk_prometheus::PrometheusBuilder::new()
2378 .bind_address(address)
2379 .build_with_cdk_metrics()?;
2380
2381 let mut shutdown_rx = self.shutdown_tx.subscribe();
2382 let prometheus_shutdown = async move {
2383 let _ = shutdown_rx.recv().await;
2384 };
2385
2386 Some(tokio::spawn(async move {
2387 if let Err(e) = server.start(prometheus_shutdown).await {
2388 tracing::error!("Failed to start prometheus server: {}", e);
2389 }
2390 }))
2391 } else {
2392 None
2393 }
2394 } else {
2395 None
2396 }
2397 };
2398
2399 self.mint.start().await?;
2400
2401 let socket_addr =
2402 SocketAddr::from_str(&format!("{}:{}", self.listen_addr, self.listen_port))?;
2403
2404 let listener = tokio::net::TcpListener::bind(socket_addr).await?;
2405
2406 tracing::info!("listening on {}", listener.local_addr()?);
2407
2408 if let Some(activation) = &self.activation {
2411 activation.mark_applied().await?;
2412 }
2413
2414 Ok(RunningMintd {
2415 mint: self.mint,
2416 mint_service: self.mint_service,
2417 listener,
2418 shutdown_tx: self.shutdown_tx,
2419 #[cfg(feature = "prometheus")]
2420 prometheus_handle,
2421 #[cfg(feature = "management-rpc")]
2422 rpc_server,
2423 })
2424 }
2425}
2426
2427impl RunningMintd {
2428 async fn serve(
2431 self,
2432 shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
2433 ) -> Result<()> {
2434 let shutdown_broadcast_task = {
2436 let shutdown_tx = self.shutdown_tx.clone();
2437 tokio::spawn(async move {
2438 shutdown_signal.await;
2439 tracing::info!("Shutdown signal received, broadcasting to all services");
2440 let _ = shutdown_tx.send(());
2441 })
2442 };
2443
2444 let mut axum_shutdown_rx = self.shutdown_tx.subscribe();
2446 let axum_shutdown = async move {
2447 let _ = axum_shutdown_rx.recv().await;
2448 };
2449
2450 let axum_result =
2452 axum::serve(self.listener, self.mint_service).with_graceful_shutdown(axum_shutdown);
2453
2454 match axum_result.await {
2455 Ok(_) => {
2456 tracing::info!("Axum server stopped with okay status");
2457 }
2458 Err(err) => {
2459 tracing::warn!("Axum server stopped with error");
2460 tracing::error!("{}", err);
2461 bail!("Axum exited with error")
2462 }
2463 }
2464
2465 let _ = shutdown_broadcast_task.await;
2467
2468 #[cfg(feature = "prometheus")]
2470 if let Some(handle) = self.prometheus_handle {
2471 if let Err(e) = handle.await {
2472 tracing::warn!("Prometheus server task failed: {}", e);
2473 }
2474 }
2475
2476 self.mint.stop().await?;
2477
2478 #[cfg(feature = "management-rpc")]
2479 {
2480 if let Some(rpc_server) = self.rpc_server {
2481 rpc_server.stop().await?;
2482 }
2483 }
2484
2485 Ok(())
2486 }
2487}
2488
2489#[allow(clippy::too_many_arguments)]
2491async fn start_services_with_shutdown(
2492 mint: Arc<cdk::mint::Mint>,
2493 settings: &config::Settings,
2494 work_dir: &Path,
2495 wallet_info_provider: ConfiguredWalletInfoProvider,
2496 mint_builder_info: cdk::nuts::MintInfo,
2497 shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
2498 routers: Vec<Router>,
2499 auth_localstore: Option<cdk_common::database::DynMintAuthDatabase>,
2500 activation: Option<ConfigurationActivation>,
2501) -> Result<()> {
2502 let prepared = PreparedMintd::prepare(
2503 mint,
2504 settings,
2505 work_dir,
2506 wallet_info_provider,
2507 mint_builder_info,
2508 routers,
2509 auth_localstore,
2510 activation,
2511 )
2512 .await?;
2513 let running = prepared.activate().await?;
2514 running.serve(shutdown_signal).await
2515}
2516
2517async fn shutdown_signal() {
2518 tokio::signal::ctrl_c()
2519 .await
2520 .expect("failed to install CTRL+C handler");
2521 tracing::info!("Shutdown signal received");
2522}
2523
2524fn work_dir() -> Result<PathBuf> {
2525 let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?;
2526 let dir = home_dir.join(".cdk-mintd");
2527
2528 std::fs::create_dir_all(&dir)?;
2529
2530 Ok(dir)
2531}
2532
2533pub async fn run_mintd(
2535 work_dir: &Path,
2536 settings: &config::Settings,
2537 db_password: Option<String>,
2538 enable_logging: bool,
2539 runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
2540 routers: Vec<Router>,
2541) -> Result<()> {
2542 let _guard = if enable_logging {
2543 setup_tracing(work_dir, &settings.info.logging)?
2544 } else {
2545 None
2546 };
2547
2548 let result = run_mintd_with_shutdown(
2549 work_dir,
2550 settings,
2551 shutdown_signal(),
2552 db_password,
2553 runtime,
2554 routers,
2555 )
2556 .await;
2557
2558 if let Some(guard) = _guard {
2560 tracing::info!("Shutting down logging worker thread");
2561 drop(guard);
2562 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2564 }
2565
2566 tracing::info!("Mintd shutdown");
2567
2568 result
2569}
2570
2571pub async fn run_mintd_with_shutdown(
2573 work_dir: &Path,
2574 settings: &config::Settings,
2575 shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
2576 db_password: Option<String>,
2577 runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
2578 routers: Vec<Router>,
2579) -> Result<()> {
2580 let (localstore, keystore, kv, _configuration_store) =
2581 initial_setup(work_dir, settings, db_password.clone()).await?;
2582
2583 run_mintd_with_database_and_shutdown(
2584 work_dir,
2585 settings,
2586 localstore,
2587 keystore,
2588 kv,
2589 shutdown_signal,
2590 db_password,
2591 runtime,
2592 routers,
2593 None,
2594 None,
2595 )
2596 .await
2597}
2598
2599#[allow(clippy::too_many_arguments)]
2600async fn run_mintd_with_database_and_shutdown(
2601 work_dir: &Path,
2602 settings: &config::Settings,
2603 localstore: DynMintDatabase,
2604 keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
2605 kv: Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync>,
2606 shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
2607 db_password: Option<String>,
2608 runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
2609 routers: Vec<Router>,
2610 activation: Option<ConfigurationActivation>,
2611 validated_signing_source: Option<ValidatedSigningSource>,
2612) -> Result<()> {
2613 let mint_builder = MintBuilder::new(localstore);
2614
2615 let maybe_mint_builder = {
2618 #[cfg(feature = "management-rpc")]
2619 {
2620 if activation
2621 .as_ref()
2622 .is_some_and(ConfigurationActivation::forces_configuration)
2623 {
2624 mint_builder
2625 } else if let Some(rpc_settings) = settings.mint_management_rpc.clone() {
2626 if rpc_settings.enabled {
2627 let mut tmp = mint_builder;
2629 if let Err(e) = tmp.init_from_db_if_present().await {
2630 tracing::warn!("Failed to init builder from DB: {}", e);
2631 }
2632 tmp
2633 } else {
2634 mint_builder
2635 }
2636 } else {
2637 mint_builder
2638 }
2639 }
2640 #[cfg(not(feature = "management-rpc"))]
2641 {
2642 mint_builder
2643 }
2644 };
2645
2646 let (mint_builder, wallet_info_provider) = configure_mint_builder_with_wallet_info(
2647 settings,
2648 maybe_mint_builder,
2649 runtime,
2650 work_dir,
2651 Some(kv),
2652 )
2653 .await?;
2654 let (mint_builder, auth_localstore) =
2655 setup_authentication(settings, work_dir, mint_builder, db_password).await?;
2656
2657 let config_mint_info = mint_builder.current_mint_info();
2658
2659 let mint = build_mint(
2660 settings,
2661 keystore,
2662 mint_builder,
2663 validated_signing_source.as_ref(),
2664 )
2665 .await?;
2666
2667 tracing::debug!("Mint built from builder.");
2668
2669 let mint = Arc::new(mint);
2670
2671 start_services_with_shutdown(
2672 mint.clone(),
2673 settings,
2674 work_dir,
2675 wallet_info_provider,
2676 config_mint_info,
2677 shutdown_signal,
2678 routers,
2679 auth_localstore,
2680 activation,
2681 )
2682 .await
2683}
2684
2685fn load_database_bootstrap_settings() -> Result<config::Settings> {
2686 let mut settings = config::Settings::default();
2687 if let Ok(database) = env::var(env_vars::DATABASE_ENV_VAR) {
2688 settings.database.engine =
2689 DatabaseEngine::from_str(&database).map_err(anyhow::Error::msg)?;
2690 }
2691 if settings.database.engine == DatabaseEngine::Postgres {
2692 settings.database.postgres = Some(config::PostgresConfig::default().from_env());
2693 } else {
2694 settings.database.postgres = None;
2695 }
2696 validate_database_config(&settings)?;
2697 Ok(settings)
2698}
2699
2700fn configuration_service(
2701 store: Arc<dyn KVStoreCompareAndSwap<Err = cdk_database::Error> + Send + Sync>,
2702 settings: &config::Settings,
2703) -> config_service::ConfigurationService {
2704 config_service::ConfigurationService::new(
2705 config_store::ConfigRepository::new(store),
2706 settings.database.clone(),
2707 )
2708}
2709
2710pub async fn validate_configuration_document(document: &str) -> Result<()> {
2712 config_service::ConfigurationService::validate_import(document).await?;
2713 Ok(())
2714}
2715
2716pub async fn initialize_configuration(
2718 work_dir: &Path,
2719 document: &str,
2720 mode: MintInitializationMode,
2721 bdk_wallet_policy: BdkWalletPolicy,
2722 db_password: Option<String>,
2723) -> Result<()> {
2724 let bootstrap = load_database_bootstrap_settings()?;
2725 let (localstore, keystore, _kv, configuration_store) =
2726 initial_setup(work_dir, &bootstrap, db_password).await?;
2727 let mut mint_builder = MintBuilder::new(localstore);
2728 mint_builder.init_from_db_if_present().await?;
2729 let database_pubkey = mint_builder.current_mint_info().pubkey;
2730 let mut keyset_transaction = keystore.begin_transaction().await?;
2731 let has_keysets = !keyset_transaction.get_keyset_infos().await?.is_empty();
2732 keyset_transaction.commit().await?;
2733
2734 configuration_service(configuration_store, &bootstrap)
2735 .initialize(
2736 document,
2737 mode,
2738 database_pubkey,
2739 has_keysets,
2740 work_dir,
2741 bdk_wallet_policy,
2742 )
2743 .await?;
2744 Ok(())
2745}
2746
2747pub async fn apply_configuration(
2749 work_dir: &Path,
2750 document: &str,
2751 validate_only: bool,
2752 bdk_wallet_policy: BdkWalletPolicy,
2753 db_password: Option<String>,
2754) -> Result<ApplyOutcome> {
2755 let bootstrap = load_database_bootstrap_settings()?;
2756 let (_localstore, _keystore, _kv, configuration_store) =
2757 initial_setup(work_dir, &bootstrap, db_password).await?;
2758 Ok(configuration_service(configuration_store, &bootstrap)
2759 .apply(document, validate_only, work_dir, bdk_wallet_policy)
2760 .await?)
2761}
2762
2763pub async fn rollback_configuration(
2765 work_dir: &Path,
2766 db_password: Option<String>,
2767) -> Result<RollbackOutcome> {
2768 let bootstrap = load_database_bootstrap_settings()?;
2769 let (_localstore, _keystore, _kv, configuration_store) =
2770 initial_setup(work_dir, &bootstrap, db_password).await?;
2771 Ok(configuration_service(configuration_store, &bootstrap)
2772 .rollback()
2773 .await?)
2774}
2775
2776pub async fn stored_configuration_document(
2778 work_dir: &Path,
2779 db_password: Option<String>,
2780) -> Result<String> {
2781 let bootstrap = load_database_bootstrap_settings()?;
2782 let (_localstore, _keystore, _kv, configuration_store) =
2783 initial_setup(work_dir, &bootstrap, db_password).await?;
2784 Ok(configuration_service(configuration_store, &bootstrap)
2785 .document()
2786 .await?)
2787}
2788
2789pub async fn run_mintd_from_database(
2791 work_dir: &Path,
2792 db_password: Option<String>,
2793 enable_logging: bool,
2794 runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
2795 routers: Vec<Router>,
2796) -> Result<()> {
2797 let bootstrap = load_database_bootstrap_settings()?;
2798 let (localstore, keystore, kv, configuration_store) =
2799 initial_setup(work_dir, &bootstrap, db_password.clone()).await?;
2800 let service = configuration_service(configuration_store, &bootstrap);
2801 let startup = service.startup().await?;
2802 config_service::require_existing_bdk_wallet(
2803 &startup.resolved.settings,
2804 work_dir,
2805 startup.bdk_wallet_policy,
2806 )?;
2807 let validated_signing_source = Some(ValidatedSigningSource {
2808 expected_pubkey: startup.signing_identity.pubkey,
2809 remote_signatory: startup
2810 .remote_signatory
2811 .map(|signatory| -> DynSignatory { signatory }),
2812 });
2813 let activation = Some(ConfigurationActivation::new(
2814 service,
2815 startup.state,
2816 startup.revision,
2817 ));
2818 let settings = startup.resolved.settings;
2819
2820 let guard = if enable_logging {
2821 setup_tracing(work_dir, &settings.info.logging)?
2822 } else {
2823 None
2824 };
2825
2826 let result = run_mintd_with_database_and_shutdown(
2827 work_dir,
2828 &settings,
2829 localstore,
2830 keystore,
2831 kv,
2832 shutdown_signal(),
2833 db_password,
2834 runtime,
2835 routers,
2836 activation,
2837 validated_signing_source,
2838 )
2839 .await;
2840
2841 if let Some(guard) = guard {
2842 tracing::info!("Shutting down logging worker thread");
2843 drop(guard);
2844 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2845 }
2846
2847 tracing::info!("Mintd shutdown");
2848 result
2849}
2850
2851#[cfg(test)]
2852mod tests {
2853 use std::fs;
2854
2855 use cdk::nuts::{CurrencyUnit, MintMethodSettings, PaymentMethod};
2856
2857 use super::*;
2858
2859 const TEST_MNEMONIC: &str =
2860 "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
2861
2862 fn temp_seed_file(name: &str) -> PathBuf {
2863 std::env::temp_dir().join(format!("cdk_mintd_{name}_{}", std::process::id()))
2864 }
2865
2866 #[cfg(all(feature = "sqlite", feature = "fakewallet"))]
2867 fn sqlite_configuration_document(secret_path: &Path, name: &str) -> String {
2868 format!(
2869 r#"
2870[info]
2871mnemonic = "file:{}"
2872
2873[mint_info]
2874name = "{name}"
2875
2876[payment_backend]
2877backend = "fakewallet"
2878
2879[fake_wallet]
2880
2881[database]
2882engine = "sqlite"
2883"#,
2884 secret_path.display()
2885 )
2886 }
2887
2888 #[cfg(all(feature = "sqlite", feature = "bdk"))]
2889 fn sqlite_bdk_configuration_document(secret_path: &Path, name: &str) -> String {
2890 format!(
2891 r#"
2892[info]
2893mnemonic = "file:{}"
2894
2895[mint_info]
2896name = "{name}"
2897
2898[payment_backend]
2899backend = "none"
2900
2901[onchain]
2902onchain_backend = "bdk"
2903
2904[bdk]
2905network = "regtest"
2906mnemonic = "file:{}"
2907
2908[database]
2909engine = "sqlite"
2910"#,
2911 secret_path.display(),
2912 secret_path.display()
2913 )
2914 }
2915
2916 #[cfg(feature = "sqlite")]
2917 #[tokio::test]
2918 async fn validated_remote_signatory_identity_is_checked_at_mint_build_boundary() {
2919 use cdk_signatory::db_signatory::DbSignatory;
2920 use cdk_signatory::signatory::Signatory;
2921 use cdk_sqlite::mint::memory;
2922
2923 let expected_store = Arc::new(memory::empty().await.expect("expected signatory database"));
2924 let expected_signatory =
2925 DbSignatory::new(expected_store, &[7; 32], HashMap::new(), Default::default())
2926 .await
2927 .expect("expected signatory");
2928 let expected_pubkey = expected_signatory
2929 .keysets()
2930 .await
2931 .expect("expected keysets")
2932 .pubkey;
2933
2934 let actual_store = Arc::new(memory::empty().await.expect("actual signatory database"));
2935 let actual_signatory: DynSignatory = Arc::new(
2936 DbSignatory::new(actual_store, &[9; 32], HashMap::new(), Default::default())
2937 .await
2938 .expect("actual signatory"),
2939 );
2940 let actual_pubkey = actual_signatory
2941 .keysets()
2942 .await
2943 .expect("actual keysets")
2944 .pubkey;
2945
2946 ensure_signatory_identity(&actual_signatory, actual_pubkey)
2947 .await
2948 .expect("matching identity");
2949 let error = ensure_signatory_identity(&actual_signatory, expected_pubkey)
2950 .await
2951 .expect_err("changed identity should be rejected");
2952 assert!(
2953 error
2954 .to_string()
2955 .contains("signing identity does not match this mint database"),
2956 "unexpected error: {error}"
2957 );
2958 }
2959
2960 #[cfg(feature = "sqlite")]
2961 #[tokio::test]
2962 async fn unapplied_configuration_refreshes_canonical_values_once() {
2963 use cdk_sqlite::mint::memory;
2964
2965 let database = Arc::new(memory::empty().await.expect("in-memory database"));
2966 let mut builder = MintBuilder::new(database.clone());
2967 builder
2968 .configure_unit(CurrencyUnit::Sat, Default::default())
2969 .expect("configure unit");
2970 let mint = builder
2971 .build_with_seed(database.clone(), &[7; 32])
2972 .await
2973 .expect("build mint");
2974
2975 let stored_info = MintBuilder::new(database.clone())
2976 .with_name("stored".to_owned())
2977 .current_mint_info();
2978 mint.set_mint_info(stored_info)
2979 .await
2980 .expect("set stored mint info");
2981 mint.set_quote_ttl(QuoteTTL::new(10, 20))
2982 .await
2983 .expect("set stored quote ttl");
2984
2985 let imported_info = MintBuilder::new(database.clone())
2986 .with_name("imported".to_owned())
2987 .current_mint_info();
2988 reconcile_canonical_configuration(&mint, imported_info, QuoteTTL::new(30, 40), false)
2989 .await
2990 .expect("apply imported canonical values");
2991 assert_eq!(
2992 mint.mint_info().await.expect("mint info").name.as_deref(),
2993 Some("imported")
2994 );
2995 assert_eq!(
2996 mint.quote_ttl().await.expect("quote ttl"),
2997 QuoteTTL::new(30, 40)
2998 );
2999
3000 let mut rpc_info = mint.mint_info().await.expect("mint info");
3001 rpc_info.name = Some("rpc-managed".to_owned());
3002 mint.set_mint_info(rpc_info)
3003 .await
3004 .expect("set RPC-managed mint info");
3005 mint.set_quote_ttl(QuoteTTL::new(50, 60))
3006 .await
3007 .expect("set RPC-managed quote ttl");
3008
3009 let later_document_info = MintBuilder::new(database)
3010 .with_name("document".to_owned())
3011 .current_mint_info();
3012 reconcile_canonical_configuration(&mint, later_document_info, QuoteTTL::new(70, 80), true)
3013 .await
3014 .expect("preserve RPC-managed canonical values");
3015 assert_eq!(
3016 mint.mint_info().await.expect("mint info").name.as_deref(),
3017 Some("rpc-managed")
3018 );
3019 assert_eq!(
3020 mint.quote_ttl().await.expect("quote ttl"),
3021 QuoteTTL::new(50, 60)
3022 );
3023 }
3024
3025 #[cfg(feature = "sqlite")]
3026 #[tokio::test]
3027 async fn preserve_mode_seeds_empty_mint_and_missing_quote_ttl() {
3028 use cdk_sqlite::mint::memory;
3029
3030 let database = Arc::new(memory::empty().await.expect("in-memory database"));
3031 let mut builder = MintBuilder::new(database.clone());
3032 builder
3033 .configure_unit(CurrencyUnit::Sat, Default::default())
3034 .expect("configure unit");
3035 let mint = builder
3036 .build_with_seed(database.clone(), &[9; 32])
3037 .await
3038 .expect("build mint");
3039
3040 let seeded_info = MintBuilder::new(database.clone())
3043 .with_name("seeded".to_owned())
3044 .current_mint_info();
3045 if mint.mint_info().await.is_ok() {
3048 let database = Arc::new(memory::empty().await.expect("second database"));
3051 let mut builder = MintBuilder::new(database.clone());
3052 builder
3053 .configure_unit(CurrencyUnit::Sat, Default::default())
3054 .expect("configure unit");
3055 let mint = builder
3056 .build_with_seed(database.clone(), &[11; 32])
3057 .await
3058 .expect("build mint");
3059 assert!(!mint
3060 .quote_ttl_is_persisted()
3061 .await
3062 .expect("quote ttl persistence probe"));
3063 let info = MintBuilder::new(database)
3064 .with_name("preserve-ttl".to_owned())
3065 .current_mint_info();
3066 reconcile_canonical_configuration(&mint, info, QuoteTTL::new(1, 2), true)
3067 .await
3068 .expect("preserve with missing ttl");
3069 assert!(mint
3070 .quote_ttl_is_persisted()
3071 .await
3072 .expect("quote ttl should now be persisted"));
3073 assert_eq!(
3074 mint.quote_ttl().await.expect("quote ttl"),
3075 QuoteTTL::new(1, 2)
3076 );
3077 } else {
3078 reconcile_canonical_configuration(&mint, seeded_info, QuoteTTL::new(3, 4), true)
3079 .await
3080 .expect("seed mint info when missing");
3081 assert_eq!(
3082 mint.mint_info().await.expect("mint info").name.as_deref(),
3083 Some("seeded")
3084 );
3085 }
3086 }
3087
3088 #[cfg(feature = "sqlite")]
3089 #[tokio::test]
3090 async fn unapplied_configuration_preserves_existing_mint_pubkey() {
3091 use cdk::nuts::PublicKey;
3092 use cdk_sqlite::mint::memory;
3093
3094 let database = Arc::new(memory::empty().await.expect("in-memory database"));
3095 let mut builder = MintBuilder::new(database.clone());
3096 builder
3097 .configure_unit(CurrencyUnit::Sat, Default::default())
3098 .expect("configure unit");
3099 let mint = builder
3100 .build_with_seed(database.clone(), &[13; 32])
3101 .await
3102 .expect("build mint");
3103
3104 let pubkey = PublicKey::from_hex(
3105 "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619",
3106 )
3107 .expect("static pubkey");
3108 let mut stored = MintBuilder::new(database.clone())
3109 .with_name("stored".to_owned())
3110 .current_mint_info();
3111 stored.pubkey = Some(pubkey);
3112 mint.set_mint_info(stored)
3113 .await
3114 .expect("set stored mint info");
3115
3116 let mut imported = MintBuilder::new(database)
3117 .with_name("imported".to_owned())
3118 .current_mint_info();
3119 imported.pubkey = None;
3120 reconcile_canonical_configuration(&mint, imported, QuoteTTL::new(7, 8), false)
3121 .await
3122 .expect("apply imported values");
3123 assert_eq!(
3124 mint.mint_info().await.expect("mint info").pubkey,
3125 Some(pubkey)
3126 );
3127 assert_eq!(
3128 mint.mint_info().await.expect("mint info").name.as_deref(),
3129 Some("imported")
3130 );
3131 }
3132
3133 #[cfg(all(feature = "sqlite", feature = "fakewallet"))]
3134 #[tokio::test]
3135 async fn database_configuration_public_api_round_trip() {
3136 let work_dir = crate::test_utils::unique_temp_path("cdk_mintd_public_config_api");
3137 fs::create_dir_all(&work_dir).expect("create work dir");
3138 let secret_path = work_dir.join("mnemonic.secret");
3139 fs::write(&secret_path, TEST_MNEMONIC).expect("write mnemonic secret");
3140
3141 #[cfg(feature = "sqlcipher")]
3142 let password = Some("test-password".to_string());
3143 #[cfg(not(feature = "sqlcipher"))]
3144 let password: Option<String> = None;
3145
3146 let first = sqlite_configuration_document(&secret_path, "first-public");
3147 let second = first.replace("first-public", "second-public");
3148
3149 validate_configuration_document(&first)
3150 .await
3151 .expect("validate first document");
3152 initialize_configuration(
3153 &work_dir,
3154 &first,
3155 MintInitializationMode::New,
3156 BdkWalletPolicy::RequireExisting,
3157 password.clone(),
3158 )
3159 .await
3160 .expect("initialize configuration");
3161 assert_eq!(
3162 stored_configuration_document(&work_dir, password.clone())
3163 .await
3164 .expect("read stored document"),
3165 first
3166 );
3167
3168 let validate_only = apply_configuration(
3169 &work_dir,
3170 &second,
3171 true,
3172 BdkWalletPolicy::RequireExisting,
3173 password.clone(),
3174 )
3175 .await
3176 .expect("validate-only apply");
3177 assert!(!validate_only.restart_required);
3178 assert_eq!(
3179 stored_configuration_document(&work_dir, password.clone())
3180 .await
3181 .expect("document unchanged"),
3182 first
3183 );
3184
3185 let applied = apply_configuration(
3186 &work_dir,
3187 &second,
3188 false,
3189 BdkWalletPolicy::RequireExisting,
3190 password.clone(),
3191 )
3192 .await
3193 .expect("apply replacement");
3194 assert!(applied.restart_required);
3195 assert_eq!(
3196 stored_configuration_document(&work_dir, password)
3197 .await
3198 .expect("replacement stored"),
3199 second
3200 );
3201
3202 let bootstrap = load_database_bootstrap_settings().expect("bootstrap settings");
3203 assert_eq!(bootstrap.database.engine, DatabaseEngine::Sqlite);
3204 assert!(bootstrap.database.postgres.is_none());
3205
3206 let _ = fs::remove_dir_all(&work_dir);
3207 }
3208
3209 #[cfg(all(feature = "sqlite", feature = "fakewallet"))]
3210 #[tokio::test]
3211 async fn existing_mint_initialization_rejects_empty_database_with_matching_mnemonic() {
3212 let work_dir = crate::test_utils::unique_temp_path("cdk_mintd_empty_existing_init");
3213 fs::create_dir_all(&work_dir).expect("create work dir");
3214 let secret_path = work_dir.join("mnemonic.secret");
3215 fs::write(&secret_path, TEST_MNEMONIC).expect("write mnemonic secret");
3216 let document = sqlite_configuration_document(&secret_path, "empty-existing");
3217
3218 #[cfg(feature = "sqlcipher")]
3219 let password = Some("test-password".to_string());
3220 #[cfg(not(feature = "sqlcipher"))]
3221 let password: Option<String> = None;
3222
3223 let error = initialize_configuration(
3224 &work_dir,
3225 &document,
3226 MintInitializationMode::Existing,
3227 BdkWalletPolicy::RequireExisting,
3228 password,
3229 )
3230 .await
3231 .expect_err("an empty database must not be accepted as an existing mint");
3232 assert!(
3233 error
3234 .to_string()
3235 .contains("does not contain a mint identity"),
3236 "unexpected error: {error}"
3237 );
3238
3239 let _ = fs::remove_dir_all(&work_dir);
3240 }
3241
3242 #[cfg(all(feature = "sqlite", feature = "fakewallet"))]
3243 #[tokio::test]
3244 async fn initialization_mode_distinguishes_existing_mint_state() {
3245 let work_dir = crate::test_utils::unique_temp_path("cdk_mintd_existing_init");
3246 fs::create_dir_all(&work_dir).expect("create work dir");
3247 let secret_path = work_dir.join("mnemonic.secret");
3248 fs::write(&secret_path, TEST_MNEMONIC).expect("write mnemonic secret");
3249 let document = sqlite_configuration_document(&secret_path, "existing");
3250
3251 #[cfg(feature = "sqlcipher")]
3252 let password = Some("test-password".to_string());
3253 #[cfg(not(feature = "sqlcipher"))]
3254 let password: Option<String> = None;
3255
3256 let bootstrap = load_database_bootstrap_settings().expect("bootstrap settings");
3257 let (localstore, keystore, _kv, _configuration_store) =
3258 initial_setup(&work_dir, &bootstrap, password.clone())
3259 .await
3260 .expect("initialize database");
3261 let mut builder = MintBuilder::new(localstore);
3262 builder
3263 .configure_unit(CurrencyUnit::Sat, Default::default())
3264 .expect("configure sat unit");
3265 let mnemonic = Mnemonic::parse(TEST_MNEMONIC).expect("test mnemonic");
3266 let mint = builder
3267 .build_with_seed(keystore, &mnemonic.to_seed_normalized(""))
3268 .await
3269 .expect("create existing mint state");
3270 drop(mint);
3271
3272 let error = initialize_configuration(
3273 &work_dir,
3274 &document,
3275 MintInitializationMode::New,
3276 BdkWalletPolicy::RequireExisting,
3277 password.clone(),
3278 )
3279 .await
3280 .expect_err("existing state must not be accepted as a new mint");
3281 assert!(
3282 error
3283 .to_string()
3284 .contains("contains existing mint identity or keyset state"),
3285 "unexpected error: {error}"
3286 );
3287
3288 initialize_configuration(
3289 &work_dir,
3290 &document,
3291 MintInitializationMode::Existing,
3292 BdkWalletPolicy::RequireExisting,
3293 password,
3294 )
3295 .await
3296 .expect("matching existing mint state should initialize");
3297
3298 let _ = fs::remove_dir_all(&work_dir);
3299 }
3300
3301 #[cfg(all(feature = "sqlite", feature = "bdk"))]
3302 #[tokio::test]
3303 async fn existing_mint_requires_explicit_new_bdk_wallet_intent() {
3304 let work_dir = crate::test_utils::unique_temp_path("cdk_mintd_existing_bdk_init");
3305 fs::create_dir_all(&work_dir).expect("create work dir");
3306 let secret_path = work_dir.join("mnemonic.secret");
3307 fs::write(&secret_path, TEST_MNEMONIC).expect("write mnemonic secret");
3308 let document = sqlite_bdk_configuration_document(&secret_path, "existing-with-missing-bdk");
3309
3310 #[cfg(feature = "sqlcipher")]
3311 let password = Some("test-password".to_string());
3312 #[cfg(not(feature = "sqlcipher"))]
3313 let password: Option<String> = None;
3314
3315 let bootstrap = load_database_bootstrap_settings().expect("bootstrap settings");
3316 let (localstore, keystore, _kv, _configuration_store) =
3317 initial_setup(&work_dir, &bootstrap, password.clone())
3318 .await
3319 .expect("initialize database");
3320 let mut builder = MintBuilder::new(localstore);
3321 builder
3322 .configure_unit(CurrencyUnit::Sat, Default::default())
3323 .expect("configure sat unit");
3324 let mnemonic = Mnemonic::parse(TEST_MNEMONIC).expect("test mnemonic");
3325 drop(
3326 builder
3327 .build_with_seed(keystore, &mnemonic.to_seed_normalized(""))
3328 .await
3329 .expect("create existing mint state"),
3330 );
3331
3332 let error = initialize_configuration(
3333 &work_dir,
3334 &document,
3335 MintInitializationMode::Existing,
3336 BdkWalletPolicy::RequireExisting,
3337 password.clone(),
3338 )
3339 .await
3340 .expect_err("missing BDK wallet must fail closed");
3341 assert!(
3342 error
3343 .to_string()
3344 .contains("Persisted BDK wallet database is missing"),
3345 "unexpected error: {error}"
3346 );
3347
3348 initialize_configuration(
3349 &work_dir,
3350 &document,
3351 MintInitializationMode::Existing,
3352 BdkWalletPolicy::AllowNew,
3353 password.clone(),
3354 )
3355 .await
3356 .expect("explicit new-wallet intent should permit initialization");
3357
3358 let error = apply_configuration(
3359 &work_dir,
3360 &document,
3361 true,
3362 BdkWalletPolicy::RequireExisting,
3363 password,
3364 )
3365 .await
3366 .expect_err("apply preflight must also reject a missing BDK wallet");
3367 assert!(error
3368 .to_string()
3369 .contains("Persisted BDK wallet database is missing"));
3370 assert!(!work_dir.join("bdk_wallet/bdk_wallet.sqlite").exists());
3371
3372 let _ = fs::remove_dir_all(&work_dir);
3373 }
3374
3375 #[test]
3376 fn load_database_bootstrap_settings_defaults_to_sqlite() {
3377 let _env_lock = crate::test_utils::env_lock();
3378 clear_mintd_env();
3379 std::env::remove_var(env_vars::DATABASE_ENV_VAR);
3380
3381 let settings = load_database_bootstrap_settings().expect("default bootstrap");
3382 assert_eq!(settings.database.engine, DatabaseEngine::Sqlite);
3383 assert!(settings.database.postgres.is_none());
3384 clear_mintd_env();
3385 }
3386
3387 #[test]
3388 fn apply_seed_file_sets_mint_mnemonic_from_trimmed_file_contents() {
3389 let seed_file = temp_seed_file("seed_file_sets_seed");
3390 fs::write(&seed_file, format!(" {TEST_MNEMONIC}\n")).expect("seed file should be written");
3391 let mut settings = config::Settings {
3392 info: config::Info {
3393 seed: Some("raw seed from config".to_string()),
3394 mnemonic: Some("mnemonic from config".to_string()),
3395 ..Default::default()
3396 },
3397 signatory: Some(config::Signatory {
3398 enabled: true,
3399 address: "127.0.0.1".to_string(),
3400 port: 15060,
3401 tls_dir: Some("/tmp/certs".into()),
3402 allow_insecure: false,
3403 }),
3404 ..Default::default()
3405 };
3406
3407 apply_seed_file(&mut settings, &seed_file).expect("seed file should be applied");
3408
3409 assert_eq!(settings.info.seed, None);
3410 assert_eq!(settings.info.mnemonic, Some(TEST_MNEMONIC.to_string()));
3411 assert_eq!(
3412 settings
3413 .signatory
3414 .as_ref()
3415 .map(|signatory| signatory.address.clone()),
3416 Some("127.0.0.1".to_string())
3417 );
3418 assert_eq!(
3419 settings.signatory.as_ref().map(|signatory| signatory.port),
3420 Some(15060)
3421 );
3422 assert_eq!(
3423 settings
3424 .signatory
3425 .as_ref()
3426 .and_then(|signatory| signatory.tls_dir.clone()),
3427 Some("/tmp/certs".into())
3428 );
3429
3430 let _ = fs::remove_file(&seed_file);
3431 }
3432
3433 #[cfg(feature = "bdk")]
3434 #[test]
3435 fn apply_seed_file_sets_active_bdk_mnemonic() {
3436 use crate::config::{Bdk, Onchain, OnchainBackend};
3437
3438 let seed_file = temp_seed_file("seed_file_sets_bdk_seed");
3439 fs::write(&seed_file, TEST_MNEMONIC).expect("seed file should be written");
3440 let mut settings = config::Settings {
3441 onchain: Some(Onchain {
3442 onchain_backend: OnchainBackend::Bdk,
3443 ..Default::default()
3444 }),
3445 bdk: Some(Bdk {
3446 mnemonic: Some("old bdk mnemonic".to_string()),
3447 ..Default::default()
3448 }),
3449 ..Default::default()
3450 };
3451
3452 apply_seed_file(&mut settings, &seed_file).expect("seed file should be applied");
3453
3454 assert_eq!(
3455 settings
3456 .bdk
3457 .expect("bdk settings should be present")
3458 .mnemonic,
3459 Some(TEST_MNEMONIC.to_string())
3460 );
3461
3462 let _ = fs::remove_file(&seed_file);
3463 }
3464
3465 #[cfg(feature = "ldk-node")]
3466 #[test]
3467 fn apply_seed_file_sets_active_ldk_node_mnemonic() {
3468 use crate::config::{LdkNode, PaymentBackend, PaymentBackendType};
3469
3470 let seed_file = temp_seed_file("seed_file_sets_ldk_seed");
3471 fs::write(&seed_file, TEST_MNEMONIC).expect("seed file should be written");
3472 let mut settings = config::Settings {
3473 payment_backend: vec![PaymentBackend {
3474 backend: PaymentBackendType::LdkNode,
3475 ..Default::default()
3476 }],
3477 ldk_node: Some(LdkNode {
3478 ldk_node_mnemonic: Some("old ldk mnemonic".to_string()),
3479 ..Default::default()
3480 }),
3481 ..Default::default()
3482 };
3483
3484 apply_seed_file(&mut settings, &seed_file).expect("seed file should be applied");
3485
3486 assert_eq!(
3487 settings
3488 .ldk_node
3489 .expect("ldk node settings should be present")
3490 .ldk_node_mnemonic,
3491 Some(TEST_MNEMONIC.to_string())
3492 );
3493
3494 let _ = fs::remove_file(&seed_file);
3495 }
3496
3497 #[test]
3498 fn apply_seed_file_rejects_empty_seed_file() {
3499 let seed_file = temp_seed_file("empty_seed_file");
3500 fs::write(&seed_file, "\n\t ").expect("seed file should be written");
3501 let mut settings = config::Settings::default();
3502
3503 let err = apply_seed_file(&mut settings, &seed_file)
3504 .expect_err("empty seed file should be rejected");
3505
3506 assert!(err.to_string().contains("is empty"));
3507 assert_eq!(settings.info.seed, None);
3508
3509 let _ = fs::remove_file(&seed_file);
3510 }
3511
3512 #[test]
3513 fn apply_seed_file_rejects_invalid_seed_phrase() {
3514 let seed_file = temp_seed_file("invalid_seed_file");
3515 fs::write(&seed_file, "not a valid seed phrase").expect("seed file should be written");
3516 let mut settings = config::Settings::default();
3517
3518 let err = apply_seed_file(&mut settings, &seed_file)
3519 .expect_err("invalid seed phrase should be rejected");
3520
3521 assert!(err.to_string().contains("Invalid seed phrase"));
3522 assert_eq!(settings.info.mnemonic, None);
3523
3524 let _ = fs::remove_file(&seed_file);
3525 }
3526
3527 #[cfg(feature = "fakewallet")]
3528 #[test]
3529 fn load_settings_from_args_applies_seed_file_before_validation() {
3530 let _env_lock = crate::test_utils::env_lock();
3531 clear_mintd_env();
3532
3533 let temp_dir = crate::test_utils::unique_temp_path("seed_file_only_signing");
3534 fs::create_dir_all(&temp_dir).expect("temp directory should be created");
3535 let config_path = temp_dir.join("config.toml");
3536 fs::write(
3537 &config_path,
3538 r#"
3539[database]
3540engine = "sqlite"
3541
3542[payment_backend]
3543backend = "fakewallet"
3544"#,
3545 )
3546 .expect("config file should be written");
3547 let seed_file = temp_dir.join("seed.txt");
3548 fs::write(&seed_file, TEST_MNEMONIC).expect("seed file should be written");
3549
3550 let args = CLIArgs {
3551 work_dir: None,
3552 #[cfg(feature = "sqlcipher")]
3553 password: Some("test-password".to_string()),
3554 config: Some(config_path),
3555 seed_file: Some(seed_file),
3556 enable_logging: false,
3557 command: None,
3558 };
3559
3560 let settings = load_settings_from_args(&temp_dir, &args)
3561 .expect("seed-file-only signing should pass validation");
3562
3563 assert_eq!(settings.info.mnemonic.as_deref(), Some(TEST_MNEMONIC));
3564 let _ = fs::remove_dir_all(&temp_dir);
3565 clear_mintd_env();
3566 }
3567
3568 #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
3569 #[tokio::test]
3570 async fn fakewallet_dispatcher_uses_payment_backend_entry_unit() {
3571 use cdk::mint::MintBuilder;
3572 use cdk_sqlite::mint::memory;
3573
3574 use crate::config::{FakeWallet, PaymentBackend, PaymentBackendType};
3575
3576 let settings = config::Settings {
3577 payment_backend: vec![PaymentBackend {
3578 backend: PaymentBackendType::FakeWallet,
3579 unit: CurrencyUnit::Eur,
3580 ..Default::default()
3581 }],
3582 fake_wallet: Some(FakeWallet::default()),
3583 ..Default::default()
3584 };
3585
3586 let localstore = Arc::new(memory::empty().await.unwrap());
3587 let builder = MintBuilder::new(localstore);
3588 let builder =
3589 configure_payment_backends(&settings, builder, None, &std::env::temp_dir(), None)
3590 .await
3591 .expect("dispatcher should succeed");
3592
3593 let mint_info = builder.current_mint_info();
3594 let units: Vec<_> = mint_info
3595 .nuts
3596 .nut04
3597 .methods
3598 .iter()
3599 .map(|m| m.unit.clone())
3600 .collect();
3601 assert!(
3602 units.contains(&CurrencyUnit::Eur),
3603 "expected Eur, got {units:?}"
3604 );
3605 assert!(
3606 !units.contains(&CurrencyUnit::Sat),
3607 "Sat would only appear if supported_units leaked through; got {units:?}"
3608 );
3609 }
3610
3611 #[test]
3612 fn backend_unit_validation_allows_matching_units() {
3613 validate_backend_unit(&CurrencyUnit::Eur, "EUR").expect("matching units should pass");
3614 }
3615
3616 #[test]
3617 fn backend_unit_validation_allows_sat_msat_pair() {
3618 validate_backend_unit(&CurrencyUnit::Sat, "MSAT")
3619 .expect("sat/msat compatible units should pass");
3620 validate_backend_unit(&CurrencyUnit::Msat, "SAT")
3621 .expect("msat/sat compatible units should pass");
3622 }
3623
3624 #[test]
3625 fn backend_unit_validation_rejects_unsupported_conversion() {
3626 let err = validate_backend_unit(&CurrencyUnit::Eur, "SAT")
3627 .expect_err("sat backend should not advertise eur");
3628
3629 assert!(
3630 err.to_string().contains("only matching units"),
3631 "error should explain the supported conversions: {err}"
3632 );
3633 }
3634
3635 #[cfg(feature = "cln")]
3636 #[test]
3637 fn expand_path_expands_bare_tilde_without_panic() {
3638 let expanded = expand_path("~");
3639
3640 assert_eq!(expanded, home::home_dir());
3641 }
3642
3643 #[cfg(feature = "cln")]
3644 #[test]
3645 fn expand_path_keeps_named_tilde_paths_literal() {
3646 let expanded = expand_path("~foo").expect("path should be returned");
3647
3648 assert_eq!(expanded, PathBuf::from("~foo"));
3649 }
3650
3651 #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
3652 #[tokio::test]
3653 async fn duplicate_payment_backend_unit_method_pair_is_rejected() {
3654 use cdk::mint::MintBuilder;
3655 use cdk_sqlite::mint::memory;
3656
3657 use crate::config::{FakeWallet, PaymentBackend, PaymentBackendType};
3658
3659 let settings = config::Settings {
3660 payment_backend: vec![
3661 PaymentBackend {
3662 backend: PaymentBackendType::FakeWallet,
3663 unit: CurrencyUnit::Sat,
3664 ..Default::default()
3665 },
3666 PaymentBackend {
3667 backend: PaymentBackendType::FakeWallet,
3668 unit: CurrencyUnit::Sat,
3669 ..Default::default()
3670 },
3671 ],
3672 fake_wallet: Some(FakeWallet::default()),
3673 ..Default::default()
3674 };
3675
3676 let localstore = Arc::new(memory::empty().await.unwrap());
3677 let builder = MintBuilder::new(localstore);
3678 let err = configure_payment_backends(&settings, builder, None, &std::env::temp_dir(), None)
3679 .await
3680 .expect_err("duplicate unit/method pair should be rejected");
3681
3682 assert!(err.to_string().contains("Duplicate payment processor"));
3683 }
3684
3685 #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
3686 #[tokio::test]
3687 async fn empty_payment_backend_vec_returns_unchanged_builder() {
3688 use cdk::mint::MintBuilder;
3689 use cdk_sqlite::mint::memory;
3690
3691 let settings = config::Settings {
3692 payment_backend: vec![],
3693 ..Default::default()
3694 };
3695
3696 let localstore = Arc::new(memory::empty().await.unwrap());
3697 let builder = MintBuilder::new(localstore);
3698 let builder =
3699 configure_payment_backends(&settings, builder, None, &std::env::temp_dir(), None)
3700 .await
3701 .expect("empty payment_backend should succeed");
3702
3703 let mint_info = builder.current_mint_info();
3704 assert!(
3705 mint_info.nuts.nut04.methods.is_empty(),
3706 "no backends should be registered"
3707 );
3708 }
3709
3710 #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
3711 #[tokio::test]
3712 async fn payment_backend_none_logs_and_continues() {
3713 use cdk::mint::MintBuilder;
3714 use cdk_sqlite::mint::memory;
3715
3716 use crate::config::{PaymentBackend, PaymentBackendType};
3717
3718 let settings = config::Settings {
3719 payment_backend: vec![PaymentBackend {
3720 backend: PaymentBackendType::None,
3721 unit: CurrencyUnit::Sat,
3722 ..Default::default()
3723 }],
3724 ..Default::default()
3725 };
3726
3727 let localstore = Arc::new(memory::empty().await.unwrap());
3728 let builder = MintBuilder::new(localstore);
3729 let builder =
3730 configure_payment_backends(&settings, builder, None, &std::env::temp_dir(), None)
3731 .await
3732 .expect("PaymentBackendType::None should succeed");
3733
3734 let mint_info = builder.current_mint_info();
3735 assert!(
3736 mint_info.nuts.nut04.methods.is_empty(),
3737 "PaymentBackendType::None should not register any methods"
3738 );
3739 }
3740
3741 #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
3742 #[tokio::test]
3743 async fn onchain_backend_none_returns_unchanged() {
3744 use cdk::mint::MintBuilder;
3745 use cdk_sqlite::mint::memory;
3746
3747 use crate::config::{Onchain, OnchainBackend};
3748
3749 let settings = config::Settings {
3750 onchain: Some(Onchain {
3751 onchain_backend: OnchainBackend::None,
3752 ..Default::default()
3753 }),
3754 ..Default::default()
3755 };
3756
3757 let localstore = Arc::new(memory::empty().await.unwrap());
3758 let builder = MintBuilder::new(localstore);
3759 let builder =
3760 configure_onchain_backend(&settings, builder, None, &std::env::temp_dir(), None)
3761 .await
3762 .expect("OnchainBackend::None should succeed");
3763
3764 let mint_info = builder.current_mint_info();
3765 assert!(
3766 mint_info.nuts.nut04.methods.is_empty(),
3767 "OnchainBackend::None should not register any methods"
3768 );
3769 }
3770
3771 #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
3772 #[tokio::test]
3773 async fn fakewallet_onchain_no_payment_backend_configures_onchain_methods() {
3774 use cdk::mint::MintBuilder;
3775 use cdk_sqlite::mint::memory;
3776
3777 use crate::config::{
3778 FakeWallet, Onchain, OnchainBackend, PaymentBackend, PaymentBackendType,
3779 };
3780
3781 let settings = config::Settings {
3782 payment_backend: vec![PaymentBackend {
3783 backend: PaymentBackendType::None,
3784 ..Default::default()
3785 }],
3786 onchain: Some(Onchain {
3787 onchain_backend: OnchainBackend::FakeWallet,
3788 ..Default::default()
3789 }),
3790 fake_wallet: Some(FakeWallet::default()),
3791 ..Default::default()
3792 };
3793
3794 let localstore = Arc::new(memory::empty().await.unwrap());
3795 let builder = MintBuilder::new(localstore);
3796 let builder =
3797 configure_onchain_backend(&settings, builder, None, &std::env::temp_dir(), None)
3798 .await
3799 .expect("fakewallet onchain should succeed");
3800
3801 let mint_info = builder.current_mint_info();
3802 let methods: Vec<_> = mint_info
3803 .nuts
3804 .nut04
3805 .methods
3806 .iter()
3807 .map(|m| m.method.clone())
3808 .collect();
3809 assert!(
3810 methods.contains(&PaymentMethod::Known(KnownMethod::Onchain)),
3811 "expected onchain method, got {methods:?}"
3812 );
3813 }
3814
3815 #[cfg(all(feature = "fakewallet", feature = "cln", feature = "sqlite"))]
3816 #[tokio::test]
3817 async fn fakewallet_onchain_with_real_payment_backend_bails() {
3818 use cdk::mint::MintBuilder;
3819 use cdk_sqlite::mint::memory;
3820
3821 use crate::config::{Onchain, OnchainBackend, PaymentBackend, PaymentBackendType};
3822
3823 let settings = config::Settings {
3824 payment_backend: vec![PaymentBackend {
3825 backend: PaymentBackendType::Cln,
3826 unit: CurrencyUnit::Sat,
3827 ..Default::default()
3828 }],
3829 onchain: Some(Onchain {
3830 onchain_backend: OnchainBackend::FakeWallet,
3831 ..Default::default()
3832 }),
3833 ..Default::default()
3834 };
3835
3836 let localstore = Arc::new(memory::empty().await.unwrap());
3837 let builder = MintBuilder::new(localstore);
3838 let err = configure_onchain_backend(&settings, builder, None, &std::env::temp_dir(), None)
3839 .await
3840 .expect_err("fakewallet onchain with real payment backend should bail");
3841
3842 assert!(
3843 err.to_string().contains("fakewallet"),
3844 "error should mention fakewallet: {err}"
3845 );
3846 }
3847
3848 #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
3849 #[tokio::test]
3850 async fn configure_mint_builder_no_backends_bails() {
3851 use cdk::mint::MintBuilder;
3852 use cdk_sqlite::mint::memory;
3853
3854 use crate::config::{PaymentBackend, PaymentBackendType};
3855
3856 let settings = config::Settings {
3857 payment_backend: vec![PaymentBackend {
3858 backend: PaymentBackendType::None,
3859 ..Default::default()
3860 }],
3861 ..Default::default()
3862 };
3863
3864 let localstore = Arc::new(memory::empty().await.unwrap());
3865 let builder = MintBuilder::new(localstore);
3866 let err = configure_mint_builder(&settings, builder, None, &std::env::temp_dir(), None)
3867 .await
3868 .expect_err("no payment backends should bail");
3869
3870 assert!(
3871 err.to_string().contains("At least one payment backend"),
3872 "error should mention missing backends: {err}"
3873 );
3874 }
3875
3876 #[cfg(all(feature = "fakewallet", feature = "sqlite", feature = "bdk"))]
3877 #[tokio::test]
3878 async fn configure_mint_builder_fake_wallet_with_bdk_onchain_bails() {
3879 use cdk::mint::MintBuilder;
3880 use cdk_sqlite::mint::memory;
3881
3882 use crate::config::{
3883 Bdk, FakeWallet, Onchain, OnchainBackend, PaymentBackend, PaymentBackendType,
3884 };
3885
3886 let settings = config::Settings {
3887 payment_backend: vec![PaymentBackend {
3888 backend: PaymentBackendType::FakeWallet,
3889 ..Default::default()
3890 }],
3891 onchain: Some(Onchain {
3892 onchain_backend: OnchainBackend::Bdk,
3893 ..Default::default()
3894 }),
3895 fake_wallet: Some(FakeWallet::default()),
3896 bdk: Some(Bdk {
3897 network: Some("mainnet".to_string()),
3898 ..Default::default()
3899 }),
3900 ..Default::default()
3901 };
3902
3903 let localstore = Arc::new(memory::empty().await.unwrap());
3904 let builder = MintBuilder::new(localstore);
3905 let err = configure_mint_builder(&settings, builder, None, &std::env::temp_dir(), None)
3906 .await
3907 .expect_err("fake wallet with BDK onchain should bail");
3908
3909 assert!(
3910 err.to_string().contains("fakewallet") && err.to_string().contains("bdk"),
3911 "error should mention backend pairing validation: {err}"
3912 );
3913 }
3914
3915 #[cfg(all(feature = "management-rpc", feature = "bdk", feature = "sqlite"))]
3916 #[tokio::test]
3917 async fn bdk_onchain_exposes_wallet_info_provider() {
3918 use cdk::mint::MintBuilder;
3919 use cdk_sqlite::mint::memory;
3920
3921 use crate::config::{Bdk, Onchain, OnchainBackend};
3922
3923 let work_dir = test_utils::unique_temp_path("cdk_mintd_wallet_info_provider");
3924 let settings = config::Settings {
3925 onchain: Some(Onchain {
3926 onchain_backend: OnchainBackend::Bdk,
3927 ..Default::default()
3928 }),
3929 bdk: Some(Bdk {
3930 network: Some("regtest".to_string()),
3931 chain_source_type: Some("esplora".to_string()),
3932 esplora_url: Some("http://127.0.0.1:1".to_string()),
3933 mnemonic: Some(
3934 "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
3935 .to_string(),
3936 ),
3937 ..Default::default()
3938 }),
3939 ..Default::default()
3940 };
3941
3942 let localstore = Arc::new(memory::empty().await.expect("in-memory database"));
3943 let builder = MintBuilder::new(localstore.clone());
3944 let (builder, provider) = configure_onchain_backend_with_wallet_info(
3945 &settings,
3946 builder,
3947 None,
3948 &work_dir,
3949 Some(localstore),
3950 )
3951 .await
3952 .expect("configure BDK backend");
3953
3954 assert!(builder
3955 .current_mint_info()
3956 .nuts
3957 .nut04
3958 .methods
3959 .iter()
3960 .any(|method| method.method == PaymentMethod::Known(KnownMethod::Onchain)));
3961
3962 let provider = provider.expect("wallet info provider");
3963 let first_address = provider
3964 .create_deposit_address()
3965 .await
3966 .expect("create first operator deposit address");
3967 let second_address = provider
3968 .create_deposit_address()
3969 .await
3970 .expect("create second operator deposit address");
3971 assert_ne!(first_address, second_address);
3972
3973 let addresses = provider
3974 .list_addresses(0, 20)
3975 .await
3976 .expect("list wallet addresses");
3977 assert_eq!(addresses.total, 2);
3978 assert!(addresses
3979 .addresses
3980 .iter()
3981 .any(|address| address.address == first_address));
3982 assert!(addresses
3983 .addresses
3984 .iter()
3985 .any(|address| address.address == second_address));
3986
3987 let balance = provider.get_balance().await.expect("get wallet balance");
3988 assert_eq!(balance.network, "regtest");
3989 assert_eq!(balance.total_sat, 0);
3990
3991 drop(builder);
3992 let _ = std::fs::remove_dir_all(work_dir);
3993 }
3994
3995 #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
3996 #[tokio::test]
3997 async fn configure_backend_for_methods_registers_websockets_and_fee() {
3998 use cdk::mint::MintBuilder;
3999 use cdk_sqlite::mint::memory;
4000
4001 use crate::config::{FakeWallet, PaymentBackend, PaymentBackendType};
4002
4003 let settings = config::Settings {
4004 payment_backend: vec![PaymentBackend {
4005 backend: PaymentBackendType::FakeWallet,
4006 unit: CurrencyUnit::Sat,
4007 ..Default::default()
4008 }],
4009 fake_wallet: Some(FakeWallet::default()),
4010 info: config::Info {
4011 input_fee_ppk: Some(100),
4012 ..Default::default()
4013 },
4014 ..Default::default()
4015 };
4016
4017 let localstore = Arc::new(memory::empty().await.unwrap());
4018 let builder = MintBuilder::new(localstore);
4019
4020 let fake_wallet = settings.fake_wallet.clone().expect("fake wallet config");
4021 let fake = fake_wallet
4022 .setup(
4023 &settings,
4024 CurrencyUnit::Sat,
4025 None,
4026 &std::env::temp_dir(),
4027 None,
4028 )
4029 .await
4030 .expect("fake wallet setup");
4031
4032 let mint_melt_limits = cdk::mint::MintMeltLimits {
4033 mint_min: 1.into(),
4034 mint_max: 500_000.into(),
4035 melt_min: 1.into(),
4036 melt_max: 500_000.into(),
4037 };
4038
4039 let builder = configure_backend_for_methods(
4040 &settings,
4041 builder,
4042 CurrencyUnit::Sat,
4043 mint_melt_limits,
4044 Arc::new(fake),
4045 vec![PaymentMethod::Known(KnownMethod::Bolt11)],
4046 )
4047 .await
4048 .expect("configure_backend_for_methods should succeed");
4049
4050 let mint_info = builder.current_mint_info();
4051 assert!(
4052 !mint_info.nuts.nut04.methods.is_empty(),
4053 "bolt11 method should be registered"
4054 );
4055 assert!(
4056 !mint_info.nuts.nut17.supported.is_empty(),
4057 "websocket support should be configured"
4058 );
4059 }
4060
4061 #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
4062 #[tokio::test]
4063 async fn fakewallet_onchain_with_fake_payment_backend_does_not_duplicate() {
4064 use cdk::mint::MintBuilder;
4065 use cdk_sqlite::mint::memory;
4066
4067 use crate::config::{
4068 FakeWallet, Onchain, OnchainBackend, PaymentBackend, PaymentBackendType,
4069 };
4070
4071 let settings = config::Settings {
4072 payment_backend: vec![PaymentBackend {
4073 backend: PaymentBackendType::FakeWallet,
4074 unit: CurrencyUnit::Sat,
4075 ..Default::default()
4076 }],
4077 onchain: Some(Onchain {
4078 onchain_backend: OnchainBackend::FakeWallet,
4079 ..Default::default()
4080 }),
4081 fake_wallet: Some(FakeWallet::default()),
4082 ..Default::default()
4083 };
4084
4085 let localstore = Arc::new(memory::empty().await.unwrap());
4086 let builder = MintBuilder::new(localstore);
4087 let builder = configure_onchain_backend(
4088 &settings,
4089 builder,
4090 None,
4091 &std::env::temp_dir(),
4092 None,
4093 )
4094 .await
4095 .expect("fakewallet onchain with fake payment backend should succeed without duplicating");
4096
4097 let mint_info = builder.current_mint_info();
4098 assert!(
4099 mint_info.nuts.nut04.methods.is_empty(),
4100 "when has_payment_backend is true and no real payment backend, fakewallet onchain should skip; got {:?}",
4101 mint_info.nuts.nut04.methods
4102 );
4103 }
4104
4105 #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
4106 #[tokio::test]
4107 async fn fakewallet_onchain_missing_fake_wallet_config_bails() {
4108 use cdk::mint::MintBuilder;
4109 use cdk_sqlite::mint::memory;
4110
4111 use crate::config::{Onchain, OnchainBackend, PaymentBackend, PaymentBackendType};
4112
4113 let settings = config::Settings {
4114 payment_backend: vec![PaymentBackend {
4115 backend: PaymentBackendType::None,
4116 ..Default::default()
4117 }],
4118 onchain: Some(Onchain {
4119 onchain_backend: OnchainBackend::FakeWallet,
4120 ..Default::default()
4121 }),
4122 fake_wallet: None,
4123 ..Default::default()
4124 };
4125
4126 let localstore = Arc::new(memory::empty().await.unwrap());
4127 let builder = MintBuilder::new(localstore);
4128 let err = configure_onchain_backend(&settings, builder, None, &std::env::temp_dir(), None)
4129 .await
4130 .expect_err("missing fake_wallet config should bail");
4131
4132 assert!(
4133 err.to_string().contains("Fake wallet config"),
4134 "error should mention missing config: {err}"
4135 );
4136 }
4137
4138 #[test]
4139 fn test_postgres_auth_url_validation() {
4140 let auth_config = config::PostgresAuthConfig {
4144 url: "".to_string(),
4145 ..Default::default()
4146 };
4147 assert!(auth_config.url.is_empty());
4148
4149 let auth_config = config::PostgresAuthConfig {
4151 url: "postgresql://user:password@localhost:5432/auth_db".to_string(),
4152 ..Default::default()
4153 };
4154 assert!(!auth_config.url.is_empty());
4155 }
4156
4157 #[test]
4158 fn test_extract_supported_payment_methods_unique_ordered() {
4159 let mut mint_info = cdk::nuts::MintInfo::default();
4160 mint_info.nuts.nut04.methods = vec![
4161 MintMethodSettings {
4162 method: PaymentMethod::Known(KnownMethod::Bolt11),
4163 unit: CurrencyUnit::Sat,
4164 method_name: None,
4165 min_amount: None,
4166 max_amount: None,
4167 options: None,
4168 },
4169 MintMethodSettings {
4170 method: PaymentMethod::Known(KnownMethod::Bolt12),
4171 unit: CurrencyUnit::Sat,
4172 method_name: None,
4173 min_amount: None,
4174 max_amount: None,
4175 options: None,
4176 },
4177 MintMethodSettings {
4178 method: PaymentMethod::Known(KnownMethod::Bolt11),
4179 unit: CurrencyUnit::Msat,
4180 method_name: None,
4181 min_amount: None,
4182 max_amount: None,
4183 options: None,
4184 },
4185 MintMethodSettings {
4186 method: PaymentMethod::Custom("paypal".to_string()),
4187 unit: CurrencyUnit::Usd,
4188 method_name: None,
4189 min_amount: None,
4190 max_amount: None,
4191 options: None,
4192 },
4193 MintMethodSettings {
4194 method: PaymentMethod::Custom("paypal".to_string()),
4195 unit: CurrencyUnit::Eur,
4196 method_name: None,
4197 min_amount: None,
4198 max_amount: None,
4199 options: None,
4200 },
4201 ];
4202
4203 let methods = extract_supported_payment_methods(&mint_info);
4204
4205 assert_eq!(methods, vec!["bolt11", "bolt12", "paypal"]);
4206 }
4207
4208 fn clear_mintd_env() {
4209 for var in [
4210 "CDK_MINTD_DATABASE",
4211 "CDK_MINTD_DATABASE_URL",
4212 "CDK_MINTD_POSTGRES_URL",
4213 "CDK_MINTD_POSTGRES_TLS_MODE",
4214 "CDK_MINTD_POSTGRES_MAX_CONNECTIONS",
4215 "CDK_MINTD_POSTGRES_CONNECTION_TIMEOUT_SECONDS",
4216 "CDK_MINTD_SEED",
4217 "CDK_MINTD_MNEMONIC",
4218 "CDK_MINTD_SIGNATORY_ENABLED",
4219 "CDK_MINTD_SIGNATORY_ADDRESS",
4220 "CDK_MINTD_SIGNATORY_PORT",
4221 "CDK_MINTD_SIGNATORY_TLS_DIR",
4222 "CDK_MINTD_SIGNATORY_ALLOW_INSECURE",
4223 "CDK_MINTD_LISTEN_HOST",
4224 "CDK_MINTD_LISTEN_PORT",
4225 "CDK_MINTD_PAYMENT_BACKEND",
4226 "CDK_MINTD_PAYMENT_BACKEND_MIN_MINT",
4227 "CDK_MINTD_PAYMENT_BACKEND_MAX_MINT",
4228 "CDK_MINTD_PAYMENT_BACKEND_MIN_MELT",
4229 "CDK_MINTD_PAYMENT_BACKEND_MAX_MELT",
4230 "CDK_MINTD_AUTH_ENABLED",
4231 "CDK_MINTD_AUTH_OPENID_DISCOVERY",
4232 "CDK_MINTD_AUTH_OPENID_CLIENT_ID",
4233 "CDK_MINTD_AUTH_MINT_MAX_BAT",
4234 "CDK_MINTD_AUTH_MINT",
4235 "CDK_MINTD_AUTH_GET_MINT_QUOTE",
4236 "CDK_MINTD_AUTH_CHECK_MINT_QUOTE",
4237 "CDK_MINTD_AUTH_MELT",
4238 "CDK_MINTD_AUTH_GET_MELT_QUOTE",
4239 "CDK_MINTD_AUTH_CHECK_MELT_QUOTE",
4240 "CDK_MINTD_AUTH_SWAP",
4241 "CDK_MINTD_AUTH_RESTORE",
4242 "CDK_MINTD_AUTH_CHECK_PROOF_STATE",
4243 "CDK_MINTD_AUTH_WEBSOCKET",
4244 "CDK_MINTD_AUTH_POSTGRES_URL",
4245 "CDK_MINTD_AUTH_POSTGRES_TLS_MODE",
4246 "CDK_MINTD_AUTH_POSTGRES_MAX_CONNECTIONS",
4247 "CDK_MINTD_AUTH_POSTGRES_CONNECTION_TIMEOUT_SECONDS",
4248 "CDK_MINTD_CLN_RPC_PATH",
4249 "CDK_MINTD_CLN_FEE_PERCENT",
4250 "CDK_MINTD_LND_ADDRESS",
4251 "CDK_MINTD_LND_CERT_FILE",
4252 "CDK_MINTD_LND_MACAROON_FILE",
4253 "CDK_MINTD_LND_FEE_PERCENT",
4254 "CDK_MINTD_FAKE_WALLET_SUPPORTED_UNITS",
4255 "CDK_MINTD_FAKE_WALLET_FEE_PERCENT",
4256 "CDK_MINTD_FAKE_WALLET_RESERVE_FEE_MIN",
4257 "CDK_MINTD_FAKE_WALLET_MIN_DELAY",
4258 "CDK_MINTD_FAKE_WALLET_MAX_DELAY",
4259 "CDK_MINTD_GRPC_PAYMENT_PROCESSOR_SUPPORTED_UNITS",
4260 "CDK_MINTD_GRPC_PAYMENT_PROCESSOR_ADDRESS",
4261 "CDK_MINTD_GRPC_PAYMENT_PROCESSOR_PORT",
4262 "CDK_MINTD_PROMETHEUS_ENABLED",
4263 "CDK_MINTD_PROMETHEUS_ADDRESS",
4264 "CDK_MINTD_PROMETHEUS_PORT",
4265 "CDK_MINTD_MINT_MANAGEMENT_ENABLED",
4266 "CDK_MINTD_MANAGEMENT_ADDRESS",
4267 "CDK_MINTD_MANAGEMENT_PORT",
4268 ] {
4269 std::env::remove_var(var);
4270 }
4271 }
4272
4273 fn load_settings_from_toml(name: &str, config_content: &str) -> Result<config::Settings> {
4274 use std::fs;
4275
4276 let temp_dir = crate::test_utils::unique_temp_path(name);
4277 let _ = fs::remove_dir_all(&temp_dir);
4278 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
4279 let config_path = temp_dir.join("config.toml");
4280 fs::write(&config_path, config_content).expect("Failed to write config file");
4281
4282 let result = load_settings(&temp_dir, Some(config_path));
4283
4284 let _ = fs::remove_dir_all(&temp_dir);
4285
4286 result
4287 }
4288
4289 fn assert_load_settings_error(config_content: &str, expected: &str) {
4290 let _env_lock = crate::test_utils::env_lock();
4291 clear_mintd_env();
4292 let err = load_settings_from_toml("cdk_mintd_invalid_config", config_content)
4293 .expect_err("Settings should fail validation");
4294 assert!(
4295 err.to_string().contains(expected),
4296 "expected error containing `{expected}`, got `{err}`"
4297 );
4298 }
4299
4300 #[cfg(all(feature = "prometheus", feature = "fakewallet"))]
4301 #[test]
4302 fn test_load_settings_merges_partial_postgres_toml_with_env() {
4303 use std::{env, fs};
4304
4305 let _env_lock = crate::test_utils::env_lock();
4306 clear_mintd_env();
4307 env::remove_var(crate::env_vars::DATABASE_URL_ENV_VAR);
4308 env::remove_var(crate::env_vars::ENV_POSTGRES_URL);
4309 env::remove_var(crate::env_vars::ENV_PROMETHEUS_ENABLED);
4310 env::remove_var(crate::env_vars::ENV_PROMETHEUS_ADDRESS);
4311 env::remove_var(crate::env_vars::ENV_PROMETHEUS_PORT);
4312
4313 let postgres_url = "postgresql://user:password@localhost:5432/cdk_mint";
4314 env::set_var(crate::env_vars::ENV_POSTGRES_URL, postgres_url);
4315
4316 let temp_dir = crate::test_utils::unique_temp_path("cdk_mintd_partial_config");
4317 let _ = fs::remove_dir_all(&temp_dir);
4318 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
4319 let config_path = temp_dir.join("config.toml");
4320
4321 let config_content = r#"
4322[info]
4323mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
4324
4325[database]
4326engine = "postgres"
4327
4328[database.postgres]
4329tls_mode = "require"
4330max_connections = 30
4331connection_timeout_seconds = 15
4332
4333[payment_backend]
4334backend = "fakewallet"
4335
4336[prometheus]
4337enabled = true
4338address = "0.0.0.0"
4339port = 9090
4340"#;
4341 fs::write(&config_path, config_content).expect("Failed to write config file");
4342
4343 let settings =
4344 load_settings(&temp_dir, Some(config_path)).expect("Failed to load settings");
4345
4346 let postgres = settings
4347 .database
4348 .postgres
4349 .as_ref()
4350 .expect("Postgres config should be present");
4351 assert_eq!(postgres.url, postgres_url);
4352 assert_eq!(postgres.tls_mode.as_deref(), Some("require"));
4353
4354 let prometheus = settings
4355 .prometheus
4356 .as_ref()
4357 .expect("Prometheus config should be loaded from TOML");
4358 assert!(prometheus.enabled);
4359 assert_eq!(prometheus.address.as_deref(), Some("0.0.0.0"));
4360 assert_eq!(prometheus.port, Some(9090));
4361
4362 env::remove_var(crate::env_vars::ENV_POSTGRES_URL);
4363 let _ = fs::remove_dir_all(&temp_dir);
4364 }
4365
4366 #[cfg(feature = "fakewallet")]
4367 #[test]
4368 fn test_load_settings_reports_missing_postgres_url_after_merge() {
4369 use std::{env, fs};
4370
4371 let _env_lock = crate::test_utils::env_lock();
4372 clear_mintd_env();
4373 env::remove_var(crate::env_vars::DATABASE_URL_ENV_VAR);
4374 env::remove_var(crate::env_vars::ENV_POSTGRES_URL);
4375
4376 let temp_dir = crate::test_utils::unique_temp_path("cdk_mintd_invalid_config");
4377 let _ = fs::remove_dir_all(&temp_dir);
4378 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
4379 let config_path = temp_dir.join("config.toml");
4380
4381 let config_content = r#"
4382[info]
4383mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
4384
4385[database]
4386engine = "postgres"
4387
4388[database.postgres]
4389tls_mode = "require"
4390
4391[payment_backend]
4392backend = "fakewallet"
4393"#;
4394 fs::write(&config_path, config_content).expect("Failed to write config file");
4395
4396 let err = load_settings(&temp_dir, Some(config_path))
4397 .expect_err("Settings should fail validation without a Postgres URL");
4398 assert!(err.to_string().contains("PostgreSQL URL is required"));
4399
4400 let _ = fs::remove_dir_all(&temp_dir);
4401 }
4402
4403 #[cfg(feature = "fakewallet")]
4404 #[test]
4405 fn test_load_settings_reports_short_seed() {
4406 assert_load_settings_error(
4407 r#"
4408[info]
4409seed = "tooshort"
4410
4411[database]
4412engine = "sqlite"
4413
4414[payment_backend]
4415backend = "fakewallet"
4416"#,
4417 "Seed in [info].seed is too short",
4418 );
4419 }
4420
4421 #[cfg(feature = "fakewallet")]
4422 #[test]
4423 fn test_load_settings_reports_missing_signing_source() {
4424 assert_load_settings_error(
4425 r#"
4426[database]
4427engine = "sqlite"
4428
4429[payment_backend]
4430backend = "fakewallet"
4431"#,
4432 "No signing source configured",
4433 );
4434 }
4435
4436 #[test]
4437 fn test_load_settings_reports_missing_payment_backend() {
4438 assert_load_settings_error(
4439 &format!(
4440 r#"
4441[info]
4442mnemonic = "{TEST_MNEMONIC}"
4443
4444[database]
4445engine = "sqlite"
4446"#
4447 ),
4448 "At least one payment backend",
4449 );
4450 }
4451
4452 #[cfg(feature = "cln")]
4453 #[test]
4454 fn test_load_settings_reports_missing_cln_config() {
4455 assert_load_settings_error(
4456 &format!(
4457 r#"
4458[info]
4459mnemonic = "{TEST_MNEMONIC}"
4460
4461[database]
4462engine = "sqlite"
4463
4464[payment_backend]
4465backend = "cln"
4466"#
4467 ),
4468 "CLN backend selected but [cln] config section is missing",
4469 );
4470 }
4471
4472 #[cfg(feature = "lnd")]
4473 #[test]
4474 fn test_load_settings_reports_missing_lnd_config() {
4475 assert_load_settings_error(
4476 &format!(
4477 r#"
4478[info]
4479mnemonic = "{TEST_MNEMONIC}"
4480
4481[database]
4482engine = "sqlite"
4483
4484[payment_backend]
4485backend = "lnd"
4486"#
4487 ),
4488 "LND backend selected but [lnd] config section is missing",
4489 );
4490 }
4491
4492 #[cfg(feature = "grpc-processor")]
4493 #[test]
4494 fn test_load_settings_reports_missing_grpc_supported_units() {
4495 assert_load_settings_error(
4496 &format!(
4497 r#"
4498[info]
4499mnemonic = "{TEST_MNEMONIC}"
4500
4501[database]
4502engine = "sqlite"
4503
4504[payment_backend]
4505backend = "grpcprocessor"
4506
4507[grpc_processor]
4508addr = "http://127.0.0.1"
4509"#
4510 ),
4511 "gRPC payment processor supported_units must contain at least one unit",
4512 );
4513 }
4514
4515 #[cfg(feature = "fakewallet")]
4516 #[test]
4517 fn test_load_settings_reports_invalid_fakewallet_delay_range() {
4518 assert_load_settings_error(
4519 &format!(
4520 r#"
4521[info]
4522mnemonic = "{TEST_MNEMONIC}"
4523
4524[database]
4525engine = "sqlite"
4526
4527[payment_backend]
4528backend = "fakewallet"
4529
4530[fake_wallet]
4531min_delay_time = 10
4532max_delay_time = 1
4533"#
4534 ),
4535 "Fake wallet min_delay_time cannot be greater than max_delay_time",
4536 );
4537 }
4538
4539 #[cfg(feature = "fakewallet")]
4540 #[test]
4541 fn test_load_settings_reports_missing_auth_openid_config() {
4542 assert_load_settings_error(
4543 &format!(
4544 r#"
4545[info]
4546mnemonic = "{TEST_MNEMONIC}"
4547
4548[database]
4549engine = "sqlite"
4550
4551[payment_backend]
4552backend = "fakewallet"
4553
4554[auth]
4555auth_enabled = true
4556"#
4557 ),
4558 "Auth openid_discovery must be set",
4559 );
4560 }
4561
4562 #[test]
4563 fn test_load_settings_reports_toml_parse_errors() {
4564 assert_load_settings_error(
4565 r#"
4566[info
4567mnemonic = "not valid toml"
4568"#,
4569 "Failed to read config file",
4570 );
4571 }
4572
4573 #[cfg(feature = "fakewallet")]
4574 #[test]
4575 fn test_load_settings_reports_invalid_payment_backend_limit_range() {
4576 assert_load_settings_error(
4577 &format!(
4578 r#"
4579[info]
4580mnemonic = "{TEST_MNEMONIC}"
4581
4582[database]
4583engine = "sqlite"
4584
4585[payment_backend]
4586backend = "fakewallet"
4587min_mint = 10
4588max_mint = 1
4589"#
4590 ),
4591 "Payment backend min_mint cannot be greater than max_mint",
4592 );
4593 }
4594
4595 #[cfg(feature = "fakewallet")]
4596 #[test]
4597 fn test_load_settings_merges_partial_onchain_config_with_defaults() {
4598 let _env_lock = crate::test_utils::env_lock();
4599 clear_mintd_env();
4600
4601 let settings = load_settings_from_toml(
4602 "cdk_mintd_partial_onchain_config",
4603 &format!(
4604 r#"
4605[info]
4606mnemonic = "{TEST_MNEMONIC}"
4607
4608[database]
4609engine = "sqlite"
4610
4611[onchain]
4612onchain_backend = "fakewallet"
4613
4614[fake_wallet]
4615"#
4616 ),
4617 )
4618 .expect("partial on-chain config should use defaults");
4619
4620 let onchain = settings.onchain.expect("on-chain config should be present");
4621 assert_eq!(onchain.min_mint, 1.into());
4622 assert_eq!(onchain.max_mint, 500_000.into());
4623 assert_eq!(onchain.min_melt, 1.into());
4624 assert_eq!(onchain.max_melt, 500_000.into());
4625 }
4626
4627 #[cfg(feature = "fakewallet")]
4628 #[test]
4629 fn test_load_settings_reports_invalid_onchain_mint_limit_range() {
4630 assert_load_settings_error(
4631 &format!(
4632 r#"
4633[info]
4634mnemonic = "{TEST_MNEMONIC}"
4635
4636[database]
4637engine = "sqlite"
4638
4639[onchain]
4640onchain_backend = "fakewallet"
4641min_mint = 10
4642max_mint = 1
4643"#
4644 ),
4645 "On-chain min_mint cannot be greater than max_mint",
4646 );
4647 }
4648
4649 #[cfg(feature = "fakewallet")]
4650 #[test]
4651 fn test_load_settings_reports_invalid_onchain_melt_limit_range() {
4652 assert_load_settings_error(
4653 &format!(
4654 r#"
4655[info]
4656mnemonic = "{TEST_MNEMONIC}"
4657
4658[database]
4659engine = "sqlite"
4660
4661[onchain]
4662onchain_backend = "fakewallet"
4663min_melt = 10
4664max_melt = 1
4665"#
4666 ),
4667 "On-chain min_melt cannot be greater than max_melt",
4668 );
4669 }
4670
4671 #[cfg(all(feature = "prometheus", feature = "fakewallet"))]
4672 #[test]
4673 fn test_load_settings_reports_invalid_prometheus_address() {
4674 assert_load_settings_error(
4675 &format!(
4676 r#"
4677[info]
4678mnemonic = "{TEST_MNEMONIC}"
4679
4680[database]
4681engine = "sqlite"
4682
4683[payment_backend]
4684backend = "fakewallet"
4685
4686[prometheus]
4687enabled = true
4688address = "localhost"
4689port = 9090
4690"#
4691 ),
4692 "Invalid Prometheus address",
4693 );
4694 }
4695
4696 #[cfg(all(feature = "management-rpc", feature = "fakewallet"))]
4697 #[test]
4698 fn test_load_settings_reports_invalid_management_rpc_address() {
4699 assert_load_settings_error(
4700 &format!(
4701 r#"
4702[info]
4703mnemonic = "{TEST_MNEMONIC}"
4704
4705[database]
4706engine = "sqlite"
4707
4708[payment_backend]
4709backend = "fakewallet"
4710
4711[mint_management_rpc]
4712enabled = true
4713address = "localhost"
4714port = 8086
4715"#
4716 ),
4717 "Invalid mint management RPC address",
4718 );
4719 }
4720
4721 #[cfg(feature = "fakewallet")]
4722 #[test]
4723 fn test_load_settings_valid_config() {
4724 let _env_lock = crate::test_utils::env_lock();
4725 clear_mintd_env();
4726 load_settings_from_toml(
4727 "cdk_mintd_valid",
4728 &format!(
4729 r#"
4730[info]
4731mnemonic = "{TEST_MNEMONIC}"
4732
4733[database]
4734engine = "sqlite"
4735
4736[payment_backend]
4737backend = "fakewallet"
4738"#
4739 ),
4740 )
4741 .expect("valid config should load without error");
4742 }
4743
4744 #[cfg(feature = "fakewallet")]
4745 #[test]
4746 fn test_load_settings_valid_config_with_insecure_signatory() {
4747 let _env_lock = crate::test_utils::env_lock();
4748 clear_mintd_env();
4749 load_settings_from_toml(
4750 "cdk_mintd_valid_signatory",
4751 r#"
4752[signatory]
4753enabled = true
4754allow_insecure = true
4755
4756[database]
4757engine = "sqlite"
4758
4759[payment_backend]
4760backend = "fakewallet"
4761"#,
4762 )
4763 .expect("valid config with an insecure signatory should load without error");
4764 }
4765
4766 #[cfg(feature = "fakewallet")]
4767 #[test]
4768 fn test_load_settings_rejects_signatory_without_tls() {
4769 assert_load_settings_error(
4770 r#"
4771[signatory]
4772enabled = true
4773
4774[database]
4775engine = "sqlite"
4776
4777[payment_backend]
4778backend = "fakewallet"
4779"#,
4780 "gRPC signatory TLS is not configured",
4781 );
4782 }
4783
4784 #[cfg(feature = "fakewallet")]
4785 #[test]
4786 fn test_load_settings_rejects_empty_seed_before_mnemonic() {
4787 assert_load_settings_error(
4788 &format!(
4789 r#"
4790[info]
4791seed = ""
4792mnemonic = "{TEST_MNEMONIC}"
4793
4794[database]
4795engine = "sqlite"
4796
4797[payment_backend]
4798backend = "fakewallet"
4799"#
4800 ),
4801 "Seed in [info].seed must not be empty",
4802 );
4803 }
4804
4805 #[cfg(feature = "fakewallet")]
4806 #[test]
4807 fn test_load_settings_reports_invalid_mnemonic() {
4808 assert_load_settings_error(
4809 r#"
4810[info]
4811mnemonic = "not a valid mnemonic phrase at all"
4812
4813[database]
4814engine = "sqlite"
4815
4816[payment_backend]
4817backend = "fakewallet"
4818"#,
4819 "Invalid mnemonic",
4820 );
4821 }
4822
4823 #[cfg(feature = "fakewallet")]
4824 #[test]
4825 fn test_load_settings_reports_invalid_listen_address() {
4826 assert_load_settings_error(
4827 &format!(
4828 r#"
4829[info]
4830mnemonic = "{TEST_MNEMONIC}"
4831listen_host = "999.999.999.999"
4832
4833[database]
4834engine = "sqlite"
4835
4836[payment_backend]
4837backend = "fakewallet"
4838"#
4839 ),
4840 "Invalid mint listen address",
4841 );
4842 }
4843
4844 #[cfg(feature = "fakewallet")]
4845 #[test]
4846 fn test_load_settings_reports_missing_auth_openid_client_id() {
4847 assert_load_settings_error(
4848 &format!(
4849 r#"
4850[info]
4851mnemonic = "{TEST_MNEMONIC}"
4852
4853[database]
4854engine = "sqlite"
4855
4856[payment_backend]
4857backend = "fakewallet"
4858
4859[auth]
4860auth_enabled = true
4861openid_discovery = "https://issuer.example.com/.well-known/openid-configuration"
4862"#
4863 ),
4864 "Auth openid_client_id must be set",
4865 );
4866 }
4867
4868 #[cfg(feature = "fakewallet")]
4869 #[test]
4870 fn test_load_settings_reports_invalid_melt_limit_range() {
4871 assert_load_settings_error(
4872 &format!(
4873 r#"
4874[info]
4875mnemonic = "{TEST_MNEMONIC}"
4876
4877[database]
4878engine = "sqlite"
4879
4880[payment_backend]
4881backend = "fakewallet"
4882min_melt = 10
4883max_melt = 1
4884"#
4885 ),
4886 "Payment backend min_melt cannot be greater than max_melt",
4887 );
4888 }
4889
4890 #[cfg(feature = "fakewallet")]
4891 #[test]
4892 fn test_load_settings_reports_missing_fakewallet_supported_units() {
4893 assert_load_settings_error(
4894 &format!(
4895 r#"
4896[info]
4897mnemonic = "{TEST_MNEMONIC}"
4898
4899[database]
4900engine = "sqlite"
4901
4902[payment_backend]
4903backend = "fakewallet"
4904
4905[fake_wallet]
4906supported_units = []
4907"#
4908 ),
4909 "Fake wallet supported_units must contain at least one unit",
4910 );
4911 }
4912
4913 #[cfg(feature = "lnd")]
4914 #[test]
4915 fn test_load_settings_reports_missing_lnd_cert_file() {
4916 assert_load_settings_error(
4917 &format!(
4918 r#"
4919[info]
4920mnemonic = "{TEST_MNEMONIC}"
4921
4922[database]
4923engine = "sqlite"
4924
4925[payment_backend]
4926backend = "lnd"
4927
4928[lnd]
4929address = "127.0.0.1:10009"
4930"#
4931 ),
4932 "LND cert_file must be set",
4933 );
4934 }
4935
4936 #[cfg(feature = "lnd")]
4937 #[test]
4938 fn test_load_settings_reports_missing_lnd_macaroon_file() {
4939 assert_load_settings_error(
4940 &format!(
4941 r#"
4942[info]
4943mnemonic = "{TEST_MNEMONIC}"
4944
4945[database]
4946engine = "sqlite"
4947
4948[payment_backend]
4949backend = "lnd"
4950
4951[lnd]
4952address = "127.0.0.1:10009"
4953cert_file = "/path/to/tls.cert"
4954"#
4955 ),
4956 "LND macaroon_file must be set",
4957 );
4958 }
4959
4960 #[cfg(feature = "grpc-processor")]
4961 #[test]
4962 fn test_load_settings_reports_missing_grpc_processor_address() {
4963 assert_load_settings_error(
4964 &format!(
4965 r#"
4966[info]
4967mnemonic = "{TEST_MNEMONIC}"
4968
4969[database]
4970engine = "sqlite"
4971
4972[payment_backend]
4973backend = "grpcprocessor"
4974
4975[grpc_processor]
4976supported_units = ["sat"]
4977address = ""
4978"#
4979 ),
4980 "gRPC payment processor address must be set",
4981 );
4982 }
4983
4984 #[cfg(feature = "fakewallet")]
4985 #[test]
4986 fn test_load_settings_reports_missing_auth_postgres_url() {
4987 assert_load_settings_error(
4988 &format!(
4989 r#"
4990[info]
4991mnemonic = "{TEST_MNEMONIC}"
4992
4993[database]
4994engine = "postgres"
4995
4996[database.postgres]
4997url = "postgresql://user:password@localhost:5432/cdk_mint"
4998
4999[payment_backend]
5000backend = "fakewallet"
5001
5002[auth]
5003auth_enabled = true
5004openid_discovery = "https://issuer.example.com/.well-known/openid-configuration"
5005openid_client_id = "mintd"
5006"#
5007 ),
5008 "Auth database PostgreSQL URL is required",
5009 );
5010 }
5011
5012 fn load_settings_with_env(
5013 name: &str,
5014 config_content: &str,
5015 setup_env: impl FnOnce(),
5016 ) -> Result<config::Settings> {
5017 use std::fs;
5018
5019 let _env_lock = crate::test_utils::env_lock();
5020 clear_mintd_env();
5021
5022 let temp_dir = crate::test_utils::unique_temp_path(name);
5023 let _ = fs::remove_dir_all(&temp_dir);
5024 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
5025 let config_path = temp_dir.join("config.toml");
5026 fs::write(&config_path, config_content).expect("Failed to write config file");
5027
5028 setup_env();
5029
5030 let result = load_settings(&temp_dir, Some(config_path));
5031 let _ = fs::remove_dir_all(&temp_dir);
5032 clear_mintd_env();
5033 result
5034 }
5035
5036 #[cfg(feature = "fakewallet")]
5037 #[test]
5038 fn env_only_auth_preserves_protected_endpoint_defaults() {
5039 let settings = load_settings_with_env(
5040 "cdk_mintd_env_auth_defaults",
5041 &format!(
5042 r#"
5043[info]
5044mnemonic = "{TEST_MNEMONIC}"
5045
5046[database]
5047engine = "sqlite"
5048
5049[payment_backend]
5050backend = "fakewallet"
5051"#
5052 ),
5053 || {
5054 std::env::set_var("CDK_MINTD_AUTH_ENABLED", "true");
5055 std::env::set_var(
5056 "CDK_MINTD_AUTH_OPENID_DISCOVERY",
5057 "https://issuer.example.com/.well-known/openid-configuration",
5058 );
5059 std::env::set_var("CDK_MINTD_AUTH_OPENID_CLIENT_ID", "mintd");
5060 },
5061 )
5062 .expect("environment-only auth configuration should load");
5063
5064 let auth = settings.auth.expect("auth should be enabled");
5065 assert_eq!(auth.mint, config::AuthType::Blind);
5066 assert_eq!(auth.swap, config::AuthType::Blind);
5067 assert_eq!(auth.restore, config::AuthType::Blind);
5068 assert_eq!(auth.websocket_auth, config::AuthType::Blind);
5069 }
5070
5071 #[cfg(feature = "lnd")]
5072 #[test]
5073 fn invalid_lnd_fee_percent_from_env_is_rejected() {
5074 for (name, fee_percent) in [
5075 ("nan", "NaN"),
5076 ("negative", "-1"),
5077 ("not-a-number", "invalid"),
5078 ] {
5079 let error = load_settings_with_env(
5080 &format!("cdk_mintd_lnd_fee_percent_{name}"),
5081 &format!(
5082 r#"
5083[info]
5084mnemonic = "{TEST_MNEMONIC}"
5085
5086[database]
5087engine = "sqlite"
5088
5089[payment_backend]
5090backend = "lnd"
5091"#
5092 ),
5093 || {
5094 std::env::set_var("CDK_MINTD_LND_ADDRESS", "https://127.0.0.1:10009");
5095 std::env::set_var("CDK_MINTD_LND_CERT_FILE", "/certs/tls.cert");
5096 std::env::set_var("CDK_MINTD_LND_MACAROON_FILE", "/data/admin.macaroon");
5097 std::env::set_var("CDK_MINTD_LND_FEE_PERCENT", fee_percent);
5098 },
5099 )
5100 .expect_err("invalid LND fee percentage should fail startup validation");
5101
5102 assert!(
5103 error.to_string().contains("CDK_MINTD_LND_FEE_PERCENT"),
5104 "unexpected error for {fee_percent}: {error}"
5105 );
5106 }
5107 }
5108
5109 #[cfg(feature = "fakewallet")]
5110 #[test]
5111 fn test_env_var_provides_mnemonic_when_toml_has_none() {
5112 let settings = load_settings_with_env(
5113 "cdk_mintd_env_mnemonic",
5114 r#"
5115[database]
5116engine = "sqlite"
5117
5118[payment_backend]
5119backend = "fakewallet"
5120"#,
5121 || std::env::set_var("CDK_MINTD_MNEMONIC", TEST_MNEMONIC),
5122 )
5123 .expect("valid config with env mnemonic should load");
5124
5125 let mnemonic = settings
5126 .info
5127 .mnemonic
5128 .expect("mnemonic should be set from env");
5129 assert_eq!(mnemonic, TEST_MNEMONIC);
5130 }
5131
5132 #[cfg(feature = "fakewallet")]
5133 #[test]
5134 fn test_env_var_provides_seed_when_toml_has_none() {
5135 let seed = "a".repeat(32);
5136 let settings = load_settings_with_env(
5137 "cdk_mintd_env_seed",
5138 r#"
5139[database]
5140engine = "sqlite"
5141
5142[payment_backend]
5143backend = "fakewallet"
5144"#,
5145 || std::env::set_var("CDK_MINTD_SEED", &seed),
5146 )
5147 .expect("valid config with env seed should load");
5148
5149 let loaded_seed = settings.info.seed.expect("seed should be set from env");
5150 assert_eq!(loaded_seed, seed);
5151 }
5152
5153 #[cfg(feature = "fakewallet")]
5154 #[test]
5155 fn test_env_var_provides_payment_backend_when_toml_has_none() {
5156 let settings = load_settings_with_env(
5157 "cdk_mintd_env_payment_backend_only",
5158 &format!(
5159 r#"
5160[info]
5161mnemonic = "{TEST_MNEMONIC}"
5162
5163[database]
5164engine = "sqlite"
5165"#
5166 ),
5167 || {
5168 std::env::set_var("CDK_MINTD_PAYMENT_BACKEND", "fakewallet");
5169 std::env::set_var("CDK_MINTD_PAYMENT_BACKEND_MIN_MINT", "10");
5170 },
5171 )
5172 .expect("env-only payment backend config should load");
5173
5174 assert_eq!(settings.payment_backend.len(), 1);
5175 assert_eq!(
5176 settings.payment_backend[0].backend,
5177 config::PaymentBackendType::FakeWallet
5178 );
5179 }
5180
5181 #[cfg(feature = "fakewallet")]
5182 #[test]
5183 fn test_env_var_overrides_toml_listen_host() {
5184 let settings = load_settings_with_env(
5185 "cdk_mintd_env_override_listen",
5186 &format!(
5187 r#"
5188[info]
5189mnemonic = "{TEST_MNEMONIC}"
5190listen_host = "127.0.0.1"
5191
5192[database]
5193engine = "sqlite"
5194
5195[payment_backend]
5196backend = "fakewallet"
5197"#
5198 ),
5199 || std::env::set_var("CDK_MINTD_LISTEN_HOST", "0.0.0.0"),
5200 )
5201 .expect("config with env override should load");
5202
5203 assert_eq!(settings.info.listen_host, "0.0.0.0");
5204 }
5205
5206 #[cfg(feature = "fakewallet")]
5207 #[test]
5208 fn test_env_var_overrides_toml_listen_port() {
5209 let settings = load_settings_with_env(
5210 "cdk_mintd_env_override_port",
5211 &format!(
5212 r#"
5213[info]
5214mnemonic = "{TEST_MNEMONIC}"
5215listen_port = 8080
5216
5217[database]
5218engine = "sqlite"
5219
5220[payment_backend]
5221backend = "fakewallet"
5222"#
5223 ),
5224 || std::env::set_var("CDK_MINTD_LISTEN_PORT", "9090"),
5225 )
5226 .expect("config with env port override should load");
5227
5228 assert_eq!(settings.info.listen_port, 9090);
5229 }
5230}