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