1#![allow(missing_docs)]
2use std::collections::HashMap;
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, Result};
14use axum::Router;
15use bip39::Mnemonic;
16use cdk::cdk_database::{self, KVStore, MintDatabase, MintKeysDatabase};
17use cdk::mint::{Mint, MintBuilder, MintMeltLimits};
18use cdk::nuts::nut00::KnownMethod;
19#[cfg(any(
20 feature = "cln",
21 feature = "lnbits",
22 feature = "lnd",
23 feature = "ldk-node",
24 feature = "fakewallet",
25 feature = "grpc-processor"
26))]
27use cdk::nuts::nut17::SupportedMethods;
28use cdk::nuts::nut19::{CachedEndpoint, Method as NUT19Method, Path as NUT19Path};
29#[cfg(any(
30 feature = "cln",
31 feature = "lnbits",
32 feature = "lnd",
33 feature = "ldk-node"
34))]
35use cdk::nuts::CurrencyUnit;
36use cdk::nuts::{
37 AuthRequired, ContactInfo, Method, MintVersion, PaymentMethod, ProtectedEndpoint, RoutePath,
38};
39use cdk_axum::cache::HttpCache;
40use cdk_common::common::QuoteTTL;
41use cdk_common::database::DynMintDatabase;
42#[cfg(feature = "prometheus")]
44use cdk_common::payment::MetricsMintPayment;
45use cdk_common::payment::MintPayment;
46#[cfg(feature = "postgres")]
47use cdk_postgres::MintPgAuthDatabase;
48#[cfg(feature = "postgres")]
49use cdk_postgres::MintPgDatabase;
50#[cfg(feature = "sqlite")]
51use cdk_sqlite::mint::MintSqliteAuthDatabase;
52#[cfg(feature = "sqlite")]
53use cdk_sqlite::MintSqliteDatabase;
54use cli::CLIArgs;
55use config::{AuthType, DatabaseEngine, LnBackend};
56use env_vars::ENV_WORK_DIR;
57use setup::LnBackendSetup;
58use tower::ServiceBuilder;
59use tower_http::compression::CompressionLayer;
60use tower_http::decompression::RequestDecompressionLayer;
61use tower_http::trace::TraceLayer;
62use tracing_appender::{non_blocking, rolling};
63use tracing_subscriber::fmt::writer::MakeWriterExt;
64use tracing_subscriber::EnvFilter;
65#[cfg(feature = "swagger")]
66use utoipa::OpenApi;
67
68pub mod cli;
69pub mod config;
70pub mod env_vars;
71pub mod setup;
72
73const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
74
75#[cfg(feature = "cln")]
76fn expand_path(path: &str) -> Option<PathBuf> {
77 if path.starts_with('~') {
78 if let Some(home_dir) = home::home_dir().as_mut() {
79 let remainder = &path[2..];
80 home_dir.push(remainder);
81 let expanded_path = home_dir;
82 Some(expanded_path.clone())
83 } else {
84 None
85 }
86 } else {
87 Some(PathBuf::from(path))
88 }
89}
90
91async fn initial_setup(
95 work_dir: &Path,
96 settings: &config::Settings,
97 db_password: Option<String>,
98) -> Result<(
99 DynMintDatabase,
100 Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
101 Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync>,
102)> {
103 let (localstore, keystore, kv) = setup_database(settings, work_dir, db_password).await?;
104 Ok((localstore, keystore, kv))
105}
106
107pub fn setup_tracing(
111 work_dir: &Path,
112 logging_config: &config::LoggingConfig,
113) -> Result<Option<tracing_appender::non_blocking::WorkerGuard>> {
114 let default_filter = "debug";
115 let hyper_filter = "hyper=warn,rustls=warn,reqwest=warn";
116 let h2_filter = "h2=warn";
117 let tower_filter = "tower=warn";
118 let tower_http = "tower_http=warn";
119 let rustls = "rustls=warn";
120 let tungstenite = "tungstenite=warn";
121 let tokio_postgres = "tokio_postgres=warn";
122
123 let env_filter = EnvFilter::new(format!(
124 "{default_filter},{hyper_filter},{h2_filter},{tower_filter},{tower_http},{rustls},{tungstenite},{tokio_postgres}"
125 ));
126
127 use config::LoggingOutput;
128 match logging_config.output {
129 LoggingOutput::Stderr => {
130 let console_level = logging_config
132 .console_level
133 .as_deref()
134 .unwrap_or("info")
135 .parse::<tracing::Level>()
136 .unwrap_or(tracing::Level::INFO);
137
138 let stderr = std::io::stderr.with_max_level(console_level);
139
140 tracing_subscriber::fmt()
141 .with_env_filter(env_filter)
142 .with_ansi(false)
143 .with_writer(stderr)
144 .init();
145
146 tracing::info!("Logging initialized: console only ({}+)", console_level);
147 Ok(None)
148 }
149 LoggingOutput::File => {
150 let file_level = logging_config
152 .file_level
153 .as_deref()
154 .unwrap_or("debug")
155 .parse::<tracing::Level>()
156 .unwrap_or(tracing::Level::DEBUG);
157
158 let logs_dir = work_dir.join("logs");
160 std::fs::create_dir_all(&logs_dir)?;
161
162 let file_appender = rolling::daily(&logs_dir, "cdk-mintd.log");
164 let (non_blocking_appender, guard) = non_blocking(file_appender);
165
166 let file_writer = non_blocking_appender.with_max_level(file_level);
167
168 tracing_subscriber::fmt()
169 .with_env_filter(env_filter)
170 .with_ansi(false)
171 .with_writer(file_writer)
172 .init();
173
174 tracing::info!(
175 "Logging initialized: file only at {}/cdk-mintd.log ({}+)",
176 logs_dir.display(),
177 file_level
178 );
179 Ok(Some(guard))
180 }
181 LoggingOutput::Both => {
182 let console_level = logging_config
184 .console_level
185 .as_deref()
186 .unwrap_or("info")
187 .parse::<tracing::Level>()
188 .unwrap_or(tracing::Level::INFO);
189 let file_level = logging_config
190 .file_level
191 .as_deref()
192 .unwrap_or("debug")
193 .parse::<tracing::Level>()
194 .unwrap_or(tracing::Level::DEBUG);
195
196 let logs_dir = work_dir.join("logs");
198 std::fs::create_dir_all(&logs_dir)?;
199
200 let file_appender = rolling::daily(&logs_dir, "cdk-mintd.log");
202 let (non_blocking_appender, guard) = non_blocking(file_appender);
203
204 let stderr = std::io::stderr.with_max_level(console_level);
206 let file_writer = non_blocking_appender.with_max_level(file_level);
207
208 tracing_subscriber::fmt()
209 .with_env_filter(env_filter)
210 .with_ansi(false)
211 .with_writer(stderr.and(file_writer))
212 .init();
213
214 tracing::info!(
215 "Logging initialized: console ({}+) and file at {}/cdk-mintd.log ({}+)",
216 console_level,
217 logs_dir.display(),
218 file_level
219 );
220 Ok(Some(guard))
221 }
222 }
223}
224
225pub async fn get_work_directory(args: &CLIArgs) -> Result<PathBuf> {
227 let work_dir = if let Some(work_dir) = &args.work_dir {
228 tracing::info!("Using work dir from cmd arg");
229 work_dir.clone()
230 } else if let Ok(env_work_dir) = env::var(ENV_WORK_DIR) {
231 tracing::info!("Using work dir from env var");
232 env_work_dir.into()
233 } else {
234 work_dir()?
235 };
236 tracing::info!("Using work dir: {}", work_dir.display());
237 Ok(work_dir)
238}
239
240pub fn load_settings(work_dir: &Path, config_path: Option<PathBuf>) -> Result<config::Settings> {
242 let config_file_arg = match config_path {
244 Some(c) => c,
245 None => work_dir.join("config.toml"),
246 };
247
248 let mut settings = if config_file_arg.exists() {
249 config::Settings::new(Some(config_file_arg))
250 } else {
251 tracing::info!("Config file does not exist. Attempting to read env vars");
252 config::Settings::default()
253 };
254
255 settings.from_env()
258}
259
260async fn setup_database(
261 settings: &config::Settings,
262 _work_dir: &Path,
263 _db_password: Option<String>,
264) -> Result<(
265 DynMintDatabase,
266 Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
267 Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync>,
268)> {
269 match settings.database.engine {
270 #[cfg(feature = "sqlite")]
271 DatabaseEngine::Sqlite => {
272 let db = setup_sqlite_database(_work_dir, _db_password).await?;
273 let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> = db.clone();
274 let kv: Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync> = db.clone();
275 let keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync> = db;
276 Ok((localstore, keystore, kv))
277 }
278 #[cfg(feature = "postgres")]
279 DatabaseEngine::Postgres => {
280 let pg_config = settings.database.postgres.as_ref().ok_or_else(|| {
282 anyhow!("PostgreSQL configuration is required when using PostgreSQL engine")
283 })?;
284
285 if pg_config.url.is_empty() {
286 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");
287 }
288
289 #[cfg(feature = "postgres")]
290 let pg_db = Arc::new(MintPgDatabase::new(pg_config.url.as_str()).await?);
291 #[cfg(feature = "postgres")]
292 let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> =
293 pg_db.clone();
294 #[cfg(feature = "postgres")]
295 let kv: Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync> = pg_db.clone();
296 #[cfg(feature = "postgres")]
297 let keystore: Arc<
298 dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync,
299 > = pg_db;
300 #[cfg(feature = "postgres")]
301 return Ok((localstore, keystore, kv));
302
303 #[cfg(not(feature = "postgres"))]
304 bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
305 }
306 #[cfg(not(feature = "sqlite"))]
307 DatabaseEngine::Sqlite => {
308 bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
309 }
310 #[cfg(not(feature = "postgres"))]
311 DatabaseEngine::Postgres => {
312 bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
313 }
314 }
315}
316
317#[cfg(feature = "sqlite")]
318async fn setup_sqlite_database(
319 work_dir: &Path,
320 _password: Option<String>,
321) -> Result<Arc<MintSqliteDatabase>> {
322 let sql_db_path = work_dir.join("cdk-mintd.sqlite");
323
324 #[cfg(not(feature = "sqlcipher"))]
325 let db = MintSqliteDatabase::new(&sql_db_path).await?;
326 #[cfg(feature = "sqlcipher")]
327 let db = {
328 let password = _password
330 .ok_or_else(|| anyhow!("Password required when sqlcipher feature is enabled"))?;
331 MintSqliteDatabase::new((sql_db_path, password)).await?
332 };
333
334 Ok(Arc::new(db))
335}
336
337async fn configure_mint_builder(
342 settings: &config::Settings,
343 mint_builder: MintBuilder,
344 runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
345 work_dir: &Path,
346 kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
347) -> Result<MintBuilder> {
348 let mint_builder = configure_basic_info(settings, mint_builder);
350
351 let mint_builder =
353 configure_lightning_backend(settings, mint_builder, runtime, work_dir, kv_store).await?;
354
355 let mint_info = mint_builder.current_mint_info();
357 let payment_methods: Vec<String> = mint_info
358 .nuts
359 .nut04
360 .methods
361 .iter()
362 .map(|m| m.method.to_string())
363 .collect();
364
365 let mint_builder = configure_cache(settings, mint_builder, &payment_methods);
367
368 let mint_builder =
370 mint_builder.with_limits(settings.limits.max_inputs, settings.limits.max_outputs);
371
372 Ok(mint_builder)
373}
374
375fn configure_basic_info(settings: &config::Settings, mint_builder: MintBuilder) -> MintBuilder {
377 let mut contacts = Vec::new();
379 if let Some(nostr_key) = &settings.mint_info.contact_nostr_public_key {
380 if !nostr_key.is_empty() {
381 contacts.push(ContactInfo::new("nostr".to_string(), nostr_key.to_string()));
382 }
383 }
384 if let Some(email) = &settings.mint_info.contact_email {
385 if !email.is_empty() {
386 contacts.push(ContactInfo::new("email".to_string(), email.to_string()));
387 }
388 }
389
390 let mint_version = MintVersion::new(
392 "cdk-mintd".to_string(),
393 CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
394 );
395
396 let mut builder = mint_builder.with_version(mint_version);
398
399 if !settings.mint_info.name.is_empty() {
401 builder = builder.with_name(settings.mint_info.name.clone());
402 }
403
404 if !settings.mint_info.description.is_empty() {
406 builder = builder.with_description(settings.mint_info.description.clone());
407 }
408
409 if let Some(long_description) = &settings.mint_info.description_long {
411 if !long_description.is_empty() {
412 builder = builder.with_long_description(long_description.to_string());
413 }
414 }
415
416 for contact in contacts {
417 builder = builder.with_contact_info(contact);
418 }
419
420 if let Some(pubkey) = settings.mint_info.pubkey {
421 builder = builder.with_pubkey(pubkey);
422 }
423
424 if let Some(icon_url) = &settings.mint_info.icon_url {
425 if !icon_url.is_empty() {
426 builder = builder.with_icon_url(icon_url.to_string());
427 }
428 }
429
430 if let Some(motd) = &settings.mint_info.motd {
431 if !motd.is_empty() {
432 builder = builder.with_motd(motd.to_string());
433 }
434 }
435
436 if let Some(tos_url) = &settings.mint_info.tos_url {
437 if !tos_url.is_empty() {
438 builder = builder.with_tos_url(tos_url.to_string());
439 }
440 }
441
442 builder = builder.with_keyset_v2(settings.info.use_keyset_v2);
443
444 builder
445}
446async fn configure_lightning_backend(
448 settings: &config::Settings,
449 mut mint_builder: MintBuilder,
450 _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
451 work_dir: &Path,
452 _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
453) -> Result<MintBuilder> {
454 let mint_melt_limits = MintMeltLimits {
455 mint_min: settings.ln.min_mint,
456 mint_max: settings.ln.max_mint,
457 melt_min: settings.ln.min_melt,
458 melt_max: settings.ln.max_melt,
459 };
460
461 tracing::debug!("Ln backend: {:?}", settings.ln.ln_backend);
462
463 match settings.ln.ln_backend {
464 #[cfg(feature = "cln")]
465 LnBackend::Cln => {
466 let cln_settings = settings
467 .cln
468 .clone()
469 .expect("Config checked at load that cln is some");
470 let cln = cln_settings
471 .setup(settings, CurrencyUnit::Msat, None, work_dir, _kv_store)
472 .await?;
473 #[cfg(feature = "prometheus")]
474 let cln = MetricsMintPayment::new(cln);
475
476 mint_builder = configure_backend_for_unit(
477 settings,
478 mint_builder,
479 CurrencyUnit::Sat,
480 mint_melt_limits,
481 Arc::new(cln),
482 )
483 .await?;
484 }
485 #[cfg(feature = "lnbits")]
486 LnBackend::LNbits => {
487 let lnbits_settings = settings.clone().lnbits.expect("Checked on config load");
488 let lnbits = lnbits_settings
489 .setup(settings, CurrencyUnit::Sat, None, work_dir, None)
490 .await?;
491 #[cfg(feature = "prometheus")]
492 let lnbits = MetricsMintPayment::new(lnbits);
493
494 mint_builder = configure_backend_for_unit(
495 settings,
496 mint_builder,
497 CurrencyUnit::Sat,
498 mint_melt_limits,
499 Arc::new(lnbits),
500 )
501 .await?;
502 }
503 #[cfg(feature = "lnd")]
504 LnBackend::Lnd => {
505 let lnd_settings = settings.clone().lnd.expect("Checked at config load");
506 let lnd = lnd_settings
507 .setup(settings, CurrencyUnit::Msat, None, work_dir, _kv_store)
508 .await?;
509 #[cfg(feature = "prometheus")]
510 let lnd = MetricsMintPayment::new(lnd);
511
512 mint_builder = configure_backend_for_unit(
513 settings,
514 mint_builder,
515 CurrencyUnit::Sat,
516 mint_melt_limits,
517 Arc::new(lnd),
518 )
519 .await?;
520 }
521 #[cfg(feature = "fakewallet")]
522 LnBackend::FakeWallet => {
523 let fake_wallet = settings.clone().fake_wallet.expect("Fake wallet defined");
524 tracing::info!("Using fake wallet: {:?}", fake_wallet);
525
526 for unit in fake_wallet.clone().supported_units {
527 let fake = fake_wallet
528 .setup(settings, unit.clone(), None, work_dir, _kv_store.clone())
529 .await?;
530 #[cfg(feature = "prometheus")]
531 let fake = MetricsMintPayment::new(fake);
532
533 mint_builder = configure_backend_for_unit(
534 settings,
535 mint_builder,
536 unit.clone(),
537 mint_melt_limits,
538 Arc::new(fake),
539 )
540 .await?;
541 }
542 }
543 #[cfg(feature = "grpc-processor")]
544 LnBackend::GrpcProcessor => {
545 let grpc_processor = settings
546 .clone()
547 .grpc_processor
548 .expect("grpc processor config defined");
549
550 tracing::info!(
551 "Attempting to start with gRPC payment processor at {}:{}.",
552 grpc_processor.addr,
553 grpc_processor.port
554 );
555
556 for unit in grpc_processor.clone().supported_units {
557 tracing::debug!("Adding unit: {:?}", unit);
558 let processor = grpc_processor
559 .setup(settings, unit.clone(), None, work_dir, None)
560 .await?;
561 #[cfg(feature = "prometheus")]
562 let processor = MetricsMintPayment::new(processor);
563
564 mint_builder = configure_backend_for_unit(
565 settings,
566 mint_builder,
567 unit.clone(),
568 mint_melt_limits,
569 Arc::new(processor),
570 )
571 .await?;
572 }
573 }
574 #[cfg(feature = "ldk-node")]
575 LnBackend::LdkNode => {
576 let ldk_node_settings = settings.clone().ldk_node.expect("Checked at config load");
577 tracing::info!("Using LDK Node backend: {:?}", ldk_node_settings);
578
579 let ldk_node = ldk_node_settings
580 .setup(settings, CurrencyUnit::Sat, _runtime, work_dir, None)
581 .await?;
582
583 mint_builder = configure_backend_for_unit(
584 settings,
585 mint_builder,
586 CurrencyUnit::Sat,
587 mint_melt_limits,
588 Arc::new(ldk_node),
589 )
590 .await?;
591 }
592 LnBackend::None => {
593 tracing::error!(
594 "Payment backend was not set or feature disabled. {:?}",
595 settings.ln.ln_backend
596 );
597 bail!("Lightning backend must be configured");
598 }
599 };
600
601 Ok(mint_builder)
602}
603
604async fn configure_backend_for_unit(
606 settings: &config::Settings,
607 mut mint_builder: MintBuilder,
608 unit: cdk::nuts::CurrencyUnit,
609 mint_melt_limits: MintMeltLimits,
610 backend: Arc<dyn MintPayment<Err = cdk_common::payment::Error> + Send + Sync>,
611) -> Result<MintBuilder> {
612 let payment_settings = backend.get_settings().await?;
613
614 let mut methods = Vec::new();
615
616 if payment_settings.bolt11.is_some() {
618 methods.push(PaymentMethod::Known(KnownMethod::Bolt11));
619 }
620
621 if payment_settings.bolt12.is_some() {
623 methods.push(PaymentMethod::Known(KnownMethod::Bolt12));
624 }
625
626 for method_name in payment_settings.custom.keys() {
628 methods.push(PaymentMethod::from(method_name.as_str()));
629 }
630
631 for method in &methods {
633 mint_builder
634 .add_payment_processor(
635 unit.clone(),
636 method.clone(),
637 mint_melt_limits,
638 backend.clone(),
639 )
640 .await?;
641 }
642
643 for method in &methods {
645 let method_str = method.to_string();
646 let nut17_supported = match method_str.as_str() {
647 "bolt11" => SupportedMethods::default_bolt11(unit.clone()),
648 "bolt12" => SupportedMethods::default_bolt12(unit.clone()),
649 _ => SupportedMethods::default_custom(method.clone(), unit.clone()),
650 };
651 mint_builder = mint_builder.with_supported_websockets(nut17_supported);
652 }
653
654 if let Some(input_fee) = settings.info.input_fee_ppk {
655 mint_builder.set_unit_fee(&unit, input_fee)?;
656 }
657
658 Ok(mint_builder)
659}
660
661fn configure_cache(
663 settings: &config::Settings,
664 mint_builder: MintBuilder,
665 payment_methods: &[String],
666) -> MintBuilder {
667 let mut cached_endpoints = vec![
668 CachedEndpoint::new(NUT19Method::Post, NUT19Path::Swap),
670 ];
671
672 for method in payment_methods {
674 cached_endpoints.push(CachedEndpoint::new(
676 NUT19Method::Post,
677 NUT19Path::custom_mint(method),
678 ));
679 cached_endpoints.push(CachedEndpoint::new(
680 NUT19Method::Post,
681 NUT19Path::custom_melt(method),
682 ));
683 }
684
685 let cache: HttpCache = settings.info.http_cache.clone().into();
686 mint_builder.with_cache(Some(cache.ttl.as_secs()), cached_endpoints)
687}
688
689async fn setup_authentication(
690 settings: &config::Settings,
691 _work_dir: &Path,
692 mut mint_builder: MintBuilder,
693 _password: Option<String>,
694) -> Result<(
695 MintBuilder,
696 Option<cdk_common::database::DynMintAuthDatabase>,
697)> {
698 if let Some(auth_settings) = settings.auth.clone() {
699 use cdk_common::database::DynMintAuthDatabase;
700
701 tracing::info!("Auth settings are defined. {:?}", auth_settings);
702 let auth_localstore: DynMintAuthDatabase = match settings.database.engine {
703 #[cfg(feature = "sqlite")]
704 DatabaseEngine::Sqlite => {
705 #[cfg(feature = "sqlite")]
706 {
707 let sql_db_path = _work_dir.join("cdk-mintd-auth.sqlite");
708 #[cfg(not(feature = "sqlcipher"))]
709 let sqlite_db = MintSqliteAuthDatabase::new(&sql_db_path).await?;
710 #[cfg(feature = "sqlcipher")]
711 let sqlite_db = {
712 let password = _password.clone().ok_or_else(|| {
714 anyhow!("Password required when sqlcipher feature is enabled")
715 })?;
716 MintSqliteAuthDatabase::new((sql_db_path, password)).await?
717 };
718
719 Arc::new(sqlite_db)
720 }
721 #[cfg(not(feature = "sqlite"))]
722 {
723 bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
724 }
725 }
726 #[cfg(feature = "postgres")]
727 DatabaseEngine::Postgres => {
728 #[cfg(feature = "postgres")]
729 {
730 let auth_db_config = settings.auth_database.as_ref().ok_or_else(|| {
732 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")
733 })?;
734
735 let auth_pg_config = auth_db_config.postgres.as_ref().ok_or_else(|| {
736 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")
737 })?;
738
739 if auth_pg_config.url.is_empty() {
740 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");
741 }
742
743 Arc::new(MintPgAuthDatabase::new(auth_pg_config.url.as_str()).await?)
744 }
745 #[cfg(not(feature = "postgres"))]
746 {
747 bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
748 }
749 }
750 #[cfg(not(feature = "sqlite"))]
751 DatabaseEngine::Sqlite => {
752 bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
753 }
754 #[cfg(not(feature = "postgres"))]
755 DatabaseEngine::Postgres => {
756 bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
757 }
758 };
759
760 let mut protected_endpoints = HashMap::new();
761 let mut blind_auth_endpoints = vec![];
762 let mut clear_auth_endpoints = vec![];
763 let mut unprotected_endpoints = vec![];
764
765 let mint_blind_auth_endpoint =
766 ProtectedEndpoint::new(Method::Post, RoutePath::MintBlindAuth);
767
768 protected_endpoints.insert(mint_blind_auth_endpoint.clone(), AuthRequired::Clear);
769
770 clear_auth_endpoints.push(mint_blind_auth_endpoint);
771
772 let mut add_endpoint = |endpoint: ProtectedEndpoint, auth_type: &AuthType| {
774 match auth_type {
775 AuthType::Blind => {
776 protected_endpoints.insert(endpoint.clone(), AuthRequired::Blind);
777 blind_auth_endpoints.push(endpoint);
778 }
779 AuthType::Clear => {
780 protected_endpoints.insert(endpoint.clone(), AuthRequired::Clear);
781 clear_auth_endpoints.push(endpoint);
782 }
783 AuthType::None => {
784 unprotected_endpoints.push(endpoint);
785 }
786 };
787 };
788
789 {
796 let swap_protected_endpoint = ProtectedEndpoint::new(Method::Post, RoutePath::Swap);
797 add_endpoint(swap_protected_endpoint, &auth_settings.swap);
798 }
799
800 {
802 let restore_protected_endpoint =
803 ProtectedEndpoint::new(Method::Post, RoutePath::Restore);
804 add_endpoint(restore_protected_endpoint, &auth_settings.restore);
805 }
806
807 {
809 let state_protected_endpoint =
810 ProtectedEndpoint::new(Method::Post, RoutePath::Checkstate);
811 add_endpoint(state_protected_endpoint, &auth_settings.check_proof_state);
812 }
813
814 {
816 let ws_protected_endpoint = ProtectedEndpoint::new(Method::Get, RoutePath::Ws);
817 add_endpoint(ws_protected_endpoint, &auth_settings.websocket_auth);
818 }
819
820 mint_builder = mint_builder.with_auth(
826 auth_localstore.clone(),
827 auth_settings.openid_discovery,
828 auth_settings.openid_client_id,
829 clear_auth_endpoints,
830 );
831 mint_builder =
832 mint_builder.with_blind_auth(auth_settings.mint_max_bat, blind_auth_endpoints);
833
834 let mut tx = auth_localstore.begin_transaction().await?;
835
836 tx.remove_protected_endpoints(unprotected_endpoints).await?;
837 tx.add_protected_endpoints(protected_endpoints).await?;
838 tx.commit().await?;
839
840 Ok((mint_builder, Some(auth_localstore)))
841 } else {
842 Ok((mint_builder, None))
843 }
844}
845
846async fn build_mint(
848 settings: &config::Settings,
849 keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
850 mint_builder: MintBuilder,
851) -> Result<Mint> {
852 if let Some(signatory_url) = settings.info.signatory_url.clone() {
853 tracing::info!(
854 "Connecting to remote signatory to {} with certs {:?}",
855 signatory_url,
856 settings.info.signatory_certs.clone()
857 );
858
859 Ok(mint_builder
860 .build_with_signatory(Arc::new(
861 cdk_signatory::SignatoryRpcClient::new(
862 signatory_url,
863 settings.info.signatory_certs.clone(),
864 )
865 .await?,
866 ))
867 .await?)
868 } else if let Some(seed) = settings.info.seed.clone() {
869 let seed_bytes: Vec<u8> = seed.into();
870 Ok(mint_builder.build_with_seed(keystore, &seed_bytes).await?)
871 } else if let Some(mnemonic) = settings
872 .info
873 .mnemonic
874 .clone()
875 .map(|s| Mnemonic::from_str(&s))
876 .transpose()?
877 {
878 Ok(mint_builder
879 .build_with_seed(keystore, &mnemonic.to_seed_normalized(""))
880 .await?)
881 } else {
882 bail!("No seed nor remote signatory set");
883 }
884}
885
886async fn start_services_with_shutdown(
887 mint: Arc<cdk::mint::Mint>,
888 settings: &config::Settings,
889 _work_dir: &Path,
890 mint_builder_info: cdk::nuts::MintInfo,
891 shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
892 routers: Vec<Router>,
893 auth_localstore: Option<cdk_common::database::DynMintAuthDatabase>,
894) -> Result<()> {
895 let listen_addr = settings.info.listen_host.clone();
896 let listen_port = settings.info.listen_port;
897 let cache: HttpCache = settings.info.http_cache.clone().into();
898
899 #[cfg(feature = "management-rpc")]
900 let mut rpc_enabled = false;
901 #[cfg(not(feature = "management-rpc"))]
902 let rpc_enabled = false;
903
904 #[cfg(feature = "management-rpc")]
905 let mut rpc_server: Option<cdk_mint_rpc::MintRPCServer> = None;
906
907 #[cfg(feature = "management-rpc")]
908 {
909 if let Some(rpc_settings) = settings.mint_management_rpc.clone() {
910 if rpc_settings.enabled {
911 let addr = rpc_settings.address.unwrap_or("127.0.0.1".to_string());
912 let port = rpc_settings.port.unwrap_or(8086);
913 let mut mint_rpc = cdk_mint_rpc::MintRPCServer::new(&addr, port, mint.clone())?;
914
915 let tls_dir = rpc_settings.tls_dir_path.unwrap_or(_work_dir.join("tls"));
916
917 let tls_dir = if tls_dir.exists() {
918 Some(tls_dir)
919 } else {
920 tracing::warn!(
921 "TLS directory does not exist: {}. Starting RPC server in INSECURE mode without TLS encryption",
922 tls_dir.display()
923 );
924 None
925 };
926
927 mint_rpc.start(tls_dir).await?;
928
929 rpc_server = Some(mint_rpc);
930
931 rpc_enabled = true;
932 }
933 }
934 }
935
936 let desired_quote_ttl: QuoteTTL = settings.info.quote_ttl.unwrap_or_default();
938
939 if rpc_enabled {
940 if mint.mint_info().await.is_err() {
941 tracing::info!("Mint info not set on mint, setting.");
942 mint.set_mint_info(mint_builder_info).await?;
944 mint.set_quote_ttl(desired_quote_ttl).await?;
945 } else {
946 if !mint.quote_ttl_is_persisted().await? {
948 mint.set_quote_ttl(desired_quote_ttl).await?;
949 }
950 let mint_version = MintVersion::new(
952 "cdk-mintd".to_string(),
953 CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
954 );
955 let mut stored_mint_info = mint.mint_info().await?;
956 stored_mint_info.version = Some(mint_version);
957 mint.set_mint_info(stored_mint_info).await?;
958
959 tracing::info!("Mint info already set, not using config file settings.");
960 }
961 } else {
962 tracing::info!("RPC not enabled, using mint info and quote TTL from config.");
964 let mut mint_builder_info = mint_builder_info;
965
966 if let Ok(mint_info) = mint.mint_info().await {
967 if mint_builder_info.pubkey.is_none() {
968 mint_builder_info.pubkey = mint_info.pubkey;
969 }
970 }
971
972 mint.set_mint_info(mint_builder_info).await?;
973 mint.set_quote_ttl(desired_quote_ttl).await?;
974 }
975
976 let mint_info = mint.mint_info().await?;
977 let nut04_methods = mint_info.nuts.nut04.supported_methods();
978 let nut05_methods = mint_info.nuts.nut05.supported_methods();
979
980 let mut custom_methods = mint.get_custom_payment_methods().await?;
982
983 let bolt11_method = PaymentMethod::Known(KnownMethod::Bolt11);
985 let bolt11_supported =
986 nut04_methods.contains(&&bolt11_method) || nut05_methods.contains(&&bolt11_method);
987 let bolt12_method = PaymentMethod::Known(KnownMethod::Bolt12);
989 let bolt12_supported =
990 nut04_methods.contains(&&bolt12_method) || nut05_methods.contains(&&bolt12_method);
991
992 if bolt11_supported
993 && !custom_methods.contains(&PaymentMethod::Known(KnownMethod::Bolt11).to_string())
994 {
995 custom_methods.push(PaymentMethod::Known(KnownMethod::Bolt11).to_string());
996 }
997 if bolt12_supported
998 && !custom_methods.contains(&PaymentMethod::Known(KnownMethod::Bolt12).to_string())
999 {
1000 custom_methods.push(PaymentMethod::Known(KnownMethod::Bolt12).to_string());
1001 }
1002
1003 tracing::info!("Payment methods: {:?}", custom_methods);
1004
1005 if let (Some(ref auth_settings), Some(auth_db)) = (&settings.auth, &auth_localstore) {
1007 if auth_settings.auth_enabled {
1008 use std::collections::HashMap;
1009
1010 use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
1011 use cdk::nuts::AuthRequired;
1012
1013 use crate::config::AuthType;
1014
1015 let existing_endpoints = auth_db.get_auth_for_endpoints().await?;
1018 let payment_method_endpoints_to_remove: Vec<ProtectedEndpoint> = existing_endpoints
1019 .keys()
1020 .filter(|endpoint| {
1021 matches!(
1022 endpoint.path,
1023 RoutePath::MintQuote(_)
1024 | RoutePath::Mint(_)
1025 | RoutePath::MeltQuote(_)
1026 | RoutePath::Melt(_)
1027 )
1028 })
1029 .cloned()
1030 .collect();
1031
1032 if !payment_method_endpoints_to_remove.is_empty() {
1033 tracing::debug!(
1034 "Removing {} old payment method endpoints from database",
1035 payment_method_endpoints_to_remove.len()
1036 );
1037 let mut tx = auth_db.begin_transaction().await?;
1038 tx.remove_protected_endpoints(payment_method_endpoints_to_remove)
1039 .await?;
1040 tx.commit().await?;
1041 }
1042
1043 if !custom_methods.is_empty() {
1045 let mut protected_endpoints = HashMap::new();
1046
1047 for method_name in &custom_methods {
1048 tracing::debug!("Adding auth endpoints for payment method: {}", method_name);
1049
1050 let mint_quote_auth = match auth_settings.get_mint_quote {
1052 AuthType::Clear => Some(AuthRequired::Clear),
1053 AuthType::Blind => Some(AuthRequired::Blind),
1054 AuthType::None => None,
1055 };
1056
1057 let check_mint_quote_auth = match auth_settings.check_mint_quote {
1058 AuthType::Clear => Some(AuthRequired::Clear),
1059 AuthType::Blind => Some(AuthRequired::Blind),
1060 AuthType::None => None,
1061 };
1062
1063 let mint_auth = match auth_settings.mint {
1064 AuthType::Clear => Some(AuthRequired::Clear),
1065 AuthType::Blind => Some(AuthRequired::Blind),
1066 AuthType::None => None,
1067 };
1068
1069 let melt_quote_auth = match auth_settings.get_melt_quote {
1070 AuthType::Clear => Some(AuthRequired::Clear),
1071 AuthType::Blind => Some(AuthRequired::Blind),
1072 AuthType::None => None,
1073 };
1074
1075 let check_melt_quote_auth = match auth_settings.check_melt_quote {
1076 AuthType::Clear => Some(AuthRequired::Clear),
1077 AuthType::Blind => Some(AuthRequired::Blind),
1078 AuthType::None => None,
1079 };
1080
1081 let melt_auth = match auth_settings.melt {
1082 AuthType::Clear => Some(AuthRequired::Clear),
1083 AuthType::Blind => Some(AuthRequired::Blind),
1084 AuthType::None => None,
1085 };
1086
1087 if let Some(auth) = mint_quote_auth {
1089 protected_endpoints.insert(
1090 ProtectedEndpoint::new(
1091 Method::Post,
1092 RoutePath::MintQuote(method_name.clone()),
1093 ),
1094 auth,
1095 );
1096 }
1097 if let Some(auth) = check_mint_quote_auth {
1098 protected_endpoints.insert(
1099 ProtectedEndpoint::new(
1100 Method::Get,
1101 RoutePath::MintQuote(method_name.clone()),
1102 ),
1103 auth,
1104 );
1105 }
1106 if let Some(auth) = mint_auth {
1107 protected_endpoints.insert(
1108 ProtectedEndpoint::new(
1109 Method::Post,
1110 RoutePath::Mint(method_name.clone()),
1111 ),
1112 auth,
1113 );
1114 }
1115 if let Some(auth) = melt_quote_auth {
1116 protected_endpoints.insert(
1117 ProtectedEndpoint::new(
1118 Method::Post,
1119 RoutePath::MeltQuote(method_name.clone()),
1120 ),
1121 auth,
1122 );
1123 }
1124 if let Some(auth) = check_melt_quote_auth {
1125 protected_endpoints.insert(
1126 ProtectedEndpoint::new(
1127 Method::Get,
1128 RoutePath::MeltQuote(method_name.clone()),
1129 ),
1130 auth,
1131 );
1132 }
1133 if let Some(auth) = melt_auth {
1134 protected_endpoints.insert(
1135 ProtectedEndpoint::new(
1136 Method::Post,
1137 RoutePath::Melt(method_name.clone()),
1138 ),
1139 auth,
1140 );
1141 }
1142 }
1143
1144 if !protected_endpoints.is_empty() {
1146 let mut tx = auth_db.begin_transaction().await?;
1147 tx.add_protected_endpoints(protected_endpoints).await?;
1148 tx.commit().await?;
1149 }
1150 }
1151 }
1152 }
1153
1154 let v1_service =
1155 cdk_axum::create_mint_router_with_custom_cache(Arc::clone(&mint), cache, custom_methods)
1156 .await?;
1157
1158 let mut mint_service = Router::new()
1159 .merge(v1_service)
1160 .layer(
1161 ServiceBuilder::new()
1162 .layer(RequestDecompressionLayer::new())
1163 .layer(CompressionLayer::new()),
1164 )
1165 .layer(TraceLayer::new_for_http());
1166
1167 for router in routers {
1168 mint_service = mint_service.merge(router);
1169 }
1170
1171 #[cfg(feature = "swagger")]
1172 {
1173 if settings.info.enable_swagger_ui.unwrap_or(false) {
1174 mint_service = mint_service.merge(
1175 utoipa_swagger_ui::SwaggerUi::new("/swagger-ui")
1176 .url("/api-docs/openapi.json", cdk_axum::ApiDoc::openapi()),
1177 );
1178 }
1179 }
1180 let (shutdown_tx, _) = tokio::sync::broadcast::channel::<()>(1);
1182
1183 #[cfg(feature = "prometheus")]
1185 let prometheus_handle = {
1186 if let Some(prometheus_settings) = &settings.prometheus {
1187 if prometheus_settings.enabled {
1188 let addr = prometheus_settings
1189 .address
1190 .clone()
1191 .unwrap_or("127.0.0.1".to_string());
1192 let port = prometheus_settings.port.unwrap_or(9000);
1193
1194 let address = format!("{}:{}", addr, port)
1195 .parse()
1196 .expect("Invalid prometheus address");
1197
1198 let server = cdk_prometheus::PrometheusBuilder::new()
1199 .bind_address(address)
1200 .build_with_cdk_metrics()?;
1201
1202 let mut shutdown_rx = shutdown_tx.subscribe();
1203 let prometheus_shutdown = async move {
1204 let _ = shutdown_rx.recv().await;
1205 };
1206
1207 Some(tokio::spawn(async move {
1208 if let Err(e) = server.start(prometheus_shutdown).await {
1209 tracing::error!("Failed to start prometheus server: {}", e);
1210 }
1211 }))
1212 } else {
1213 None
1214 }
1215 } else {
1216 None
1217 }
1218 };
1219
1220 mint.start().await?;
1221
1222 let socket_addr = SocketAddr::from_str(&format!("{listen_addr}:{listen_port}"))?;
1223
1224 let listener = tokio::net::TcpListener::bind(socket_addr).await?;
1225
1226 tracing::info!("listening on {}", listener.local_addr()?);
1227
1228 let shutdown_broadcast_task = {
1230 let shutdown_tx = shutdown_tx.clone();
1231 tokio::spawn(async move {
1232 shutdown_signal.await;
1233 tracing::info!("Shutdown signal received, broadcasting to all services");
1234 let _ = shutdown_tx.send(());
1235 })
1236 };
1237
1238 let mut axum_shutdown_rx = shutdown_tx.subscribe();
1240 let axum_shutdown = async move {
1241 let _ = axum_shutdown_rx.recv().await;
1242 };
1243
1244 let axum_result = axum::serve(listener, mint_service).with_graceful_shutdown(axum_shutdown);
1246
1247 match axum_result.await {
1248 Ok(_) => {
1249 tracing::info!("Axum server stopped with okay status");
1250 }
1251 Err(err) => {
1252 tracing::warn!("Axum server stopped with error");
1253 tracing::error!("{}", err);
1254 bail!("Axum exited with error")
1255 }
1256 }
1257
1258 let _ = shutdown_broadcast_task.await;
1260
1261 #[cfg(feature = "prometheus")]
1263 if let Some(handle) = prometheus_handle {
1264 if let Err(e) = handle.await {
1265 tracing::warn!("Prometheus server task failed: {}", e);
1266 }
1267 }
1268
1269 mint.stop().await?;
1270
1271 #[cfg(feature = "management-rpc")]
1272 {
1273 if let Some(rpc_server) = rpc_server {
1274 rpc_server.stop().await?;
1275 }
1276 }
1277
1278 Ok(())
1279}
1280
1281async fn shutdown_signal() {
1282 tokio::signal::ctrl_c()
1283 .await
1284 .expect("failed to install CTRL+C handler");
1285 tracing::info!("Shutdown signal received");
1286}
1287
1288fn work_dir() -> Result<PathBuf> {
1289 let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?;
1290 let dir = home_dir.join(".cdk-mintd");
1291
1292 std::fs::create_dir_all(&dir)?;
1293
1294 Ok(dir)
1295}
1296
1297pub async fn run_mintd(
1299 work_dir: &Path,
1300 settings: &config::Settings,
1301 db_password: Option<String>,
1302 enable_logging: bool,
1303 runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1304 routers: Vec<Router>,
1305) -> Result<()> {
1306 let _guard = if enable_logging {
1307 setup_tracing(work_dir, &settings.info.logging)?
1308 } else {
1309 None
1310 };
1311
1312 let result = run_mintd_with_shutdown(
1313 work_dir,
1314 settings,
1315 shutdown_signal(),
1316 db_password,
1317 runtime,
1318 routers,
1319 )
1320 .await;
1321
1322 if let Some(guard) = _guard {
1324 tracing::info!("Shutting down logging worker thread");
1325 drop(guard);
1326 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
1328 }
1329
1330 tracing::info!("Mintd shutdown");
1331
1332 result
1333}
1334
1335pub async fn run_mintd_with_shutdown(
1337 work_dir: &Path,
1338 settings: &config::Settings,
1339 shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
1340 db_password: Option<String>,
1341 runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1342 routers: Vec<Router>,
1343) -> Result<()> {
1344 let (localstore, keystore, kv) = initial_setup(work_dir, settings, db_password.clone()).await?;
1345
1346 let mint_builder = MintBuilder::new(localstore);
1347
1348 let maybe_mint_builder = {
1351 #[cfg(feature = "management-rpc")]
1352 {
1353 if let Some(rpc_settings) = settings.mint_management_rpc.clone() {
1354 if rpc_settings.enabled {
1355 let mut tmp = mint_builder;
1357 if let Err(e) = tmp.init_from_db_if_present().await {
1358 tracing::warn!("Failed to init builder from DB: {}", e);
1359 }
1360 tmp
1361 } else {
1362 mint_builder
1363 }
1364 } else {
1365 mint_builder
1366 }
1367 }
1368 #[cfg(not(feature = "management-rpc"))]
1369 {
1370 mint_builder
1371 }
1372 };
1373
1374 let mint_builder =
1375 configure_mint_builder(settings, maybe_mint_builder, runtime, work_dir, Some(kv)).await?;
1376 let (mint_builder, auth_localstore) =
1377 setup_authentication(settings, work_dir, mint_builder, db_password).await?;
1378
1379 let config_mint_info = mint_builder.current_mint_info();
1380
1381 let mint = build_mint(settings, keystore, mint_builder).await?;
1382
1383 tracing::debug!("Mint built from builder.");
1384
1385 let mint = Arc::new(mint);
1386
1387 start_services_with_shutdown(
1388 mint.clone(),
1389 settings,
1390 work_dir,
1391 config_mint_info,
1392 shutdown_signal,
1393 routers,
1394 auth_localstore,
1395 )
1396 .await
1397}
1398
1399#[cfg(test)]
1400mod tests {
1401 use super::*;
1402
1403 #[test]
1404 fn test_postgres_auth_url_validation() {
1405 let auth_config = config::PostgresAuthConfig {
1409 url: "".to_string(),
1410 ..Default::default()
1411 };
1412 assert!(auth_config.url.is_empty());
1413
1414 let auth_config = config::PostgresAuthConfig {
1416 url: "postgresql://user:password@localhost:5432/auth_db".to_string(),
1417 ..Default::default()
1418 };
1419 assert!(!auth_config.url.is_empty());
1420 }
1421}