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