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