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