1#[cfg(feature = "auth")]
5use 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, MintDatabase, MintKVStore, MintKeysDatabase};
17use cdk::mint::{Mint, MintBuilder, MintMeltLimits};
18#[cfg(any(
19 feature = "cln",
20 feature = "lnbits",
21 feature = "lnd",
22 feature = "ldk-node",
23 feature = "fakewallet",
24 feature = "grpc-processor"
25))]
26use cdk::nuts::nut17::SupportedMethods;
27use cdk::nuts::nut19::{CachedEndpoint, Method as NUT19Method, Path as NUT19Path};
28#[cfg(any(
29 feature = "cln",
30 feature = "lnbits",
31 feature = "lnd",
32 feature = "ldk-node",
33 feature = "fakewallet"
34))]
35use cdk::nuts::CurrencyUnit;
36#[cfg(feature = "auth")]
37use cdk::nuts::{AuthRequired, Method, ProtectedEndpoint, RoutePath};
38use cdk::nuts::{ContactInfo, MintVersion, PaymentMethod};
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(all(feature = "auth", feature = "postgres"))]
47use cdk_postgres::MintPgAuthDatabase;
48#[cfg(feature = "postgres")]
49use cdk_postgres::MintPgDatabase;
50#[cfg(all(feature = "auth", feature = "sqlite"))]
51use cdk_sqlite::mint::MintSqliteAuthDatabase;
52#[cfg(feature = "sqlite")]
53use cdk_sqlite::MintSqliteDatabase;
54use cli::CLIArgs;
55#[cfg(feature = "auth")]
56use config::AuthType;
57use config::{DatabaseEngine, LnBackend};
58use env_vars::ENV_WORK_DIR;
59use setup::LnBackendSetup;
60use tower::ServiceBuilder;
61use tower_http::compression::CompressionLayer;
62use tower_http::decompression::RequestDecompressionLayer;
63use tower_http::trace::TraceLayer;
64use tracing_appender::{non_blocking, rolling};
65use tracing_subscriber::fmt::writer::MakeWriterExt;
66use tracing_subscriber::EnvFilter;
67#[cfg(feature = "swagger")]
68use utoipa::OpenApi;
69
70pub mod cli;
71pub mod config;
72pub mod env_vars;
73pub mod setup;
74
75const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
76
77#[cfg(feature = "cln")]
78fn expand_path(path: &str) -> Option<PathBuf> {
79 if path.starts_with('~') {
80 if let Some(home_dir) = home::home_dir().as_mut() {
81 let remainder = &path[2..];
82 home_dir.push(remainder);
83 let expanded_path = home_dir;
84 Some(expanded_path.clone())
85 } else {
86 None
87 }
88 } else {
89 Some(PathBuf::from(path))
90 }
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 MintKVStore<Err = cdk_database::Error> + Send + Sync>,
104)> {
105 let (localstore, keystore, kv) = setup_database(settings, work_dir, db_password).await?;
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_http=warn";
120 let rustls = "rustls=warn";
121 let tungstenite = "tungstenite=warn";
122 let tokio_postgres = "tokio_postgres=warn";
123
124 let env_filter = EnvFilter::new(format!(
125 "{default_filter},{hyper_filter},{h2_filter},{tower_filter},{rustls},{tungstenite},{tokio_postgres}"
126 ));
127
128 use config::LoggingOutput;
129 match logging_config.output {
130 LoggingOutput::Stderr => {
131 let console_level = logging_config
133 .console_level
134 .as_deref()
135 .unwrap_or("info")
136 .parse::<tracing::Level>()
137 .unwrap_or(tracing::Level::INFO);
138
139 let stderr = std::io::stderr.with_max_level(console_level);
140
141 tracing_subscriber::fmt()
142 .with_env_filter(env_filter)
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_writer(file_writer)
171 .init();
172
173 tracing::info!(
174 "Logging initialized: file only at {}/cdk-mintd.log ({}+)",
175 logs_dir.display(),
176 file_level
177 );
178 Ok(Some(guard))
179 }
180 LoggingOutput::Both => {
181 let console_level = logging_config
183 .console_level
184 .as_deref()
185 .unwrap_or("info")
186 .parse::<tracing::Level>()
187 .unwrap_or(tracing::Level::INFO);
188 let file_level = logging_config
189 .file_level
190 .as_deref()
191 .unwrap_or("debug")
192 .parse::<tracing::Level>()
193 .unwrap_or(tracing::Level::DEBUG);
194
195 let logs_dir = work_dir.join("logs");
197 std::fs::create_dir_all(&logs_dir)?;
198
199 let file_appender = rolling::daily(&logs_dir, "cdk-mintd.log");
201 let (non_blocking_appender, guard) = non_blocking(file_appender);
202
203 let stderr = std::io::stderr.with_max_level(console_level);
205 let file_writer = non_blocking_appender.with_max_level(file_level);
206
207 tracing_subscriber::fmt()
208 .with_env_filter(env_filter)
209 .with_writer(stderr.and(file_writer))
210 .init();
211
212 tracing::info!(
213 "Logging initialized: console ({}+) and file at {}/cdk-mintd.log ({}+)",
214 console_level,
215 logs_dir.display(),
216 file_level
217 );
218 Ok(Some(guard))
219 }
220 }
221}
222
223pub async fn get_work_directory(args: &CLIArgs) -> Result<PathBuf> {
225 let work_dir = if let Some(work_dir) = &args.work_dir {
226 tracing::info!("Using work dir from cmd arg");
227 work_dir.clone()
228 } else if let Ok(env_work_dir) = env::var(ENV_WORK_DIR) {
229 tracing::info!("Using work dir from env var");
230 env_work_dir.into()
231 } else {
232 work_dir()?
233 };
234 tracing::info!("Using work dir: {}", work_dir.display());
235 Ok(work_dir)
236}
237
238pub fn load_settings(work_dir: &Path, config_path: Option<PathBuf>) -> Result<config::Settings> {
240 let config_file_arg = match config_path {
242 Some(c) => c,
243 None => work_dir.join("config.toml"),
244 };
245
246 let mut settings = if config_file_arg.exists() {
247 config::Settings::new(Some(config_file_arg))
248 } else {
249 tracing::info!("Config file does not exist. Attempting to read env vars");
250 config::Settings::default()
251 };
252
253 settings.from_env()
256}
257
258async fn setup_database(
259 settings: &config::Settings,
260 _work_dir: &Path,
261 _db_password: Option<String>,
262) -> Result<(
263 DynMintDatabase,
264 Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
265 Arc<dyn MintKVStore<Err = cdk_database::Error> + Send + Sync>,
266)> {
267 match settings.database.engine {
268 #[cfg(feature = "sqlite")]
269 DatabaseEngine::Sqlite => {
270 let db = setup_sqlite_database(_work_dir, _db_password).await?;
271 let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> = db.clone();
272 let kv: Arc<dyn MintKVStore<Err = cdk_database::Error> + Send + Sync> = db.clone();
273 let keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync> = db;
274 Ok((localstore, keystore, kv))
275 }
276 #[cfg(feature = "postgres")]
277 DatabaseEngine::Postgres => {
278 let pg_config = settings.database.postgres.as_ref().ok_or_else(|| {
280 anyhow!("PostgreSQL configuration is required when using PostgreSQL engine")
281 })?;
282
283 if pg_config.url.is_empty() {
284 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");
285 }
286
287 #[cfg(feature = "postgres")]
288 let pg_db = Arc::new(MintPgDatabase::new(pg_config.url.as_str()).await?);
289 #[cfg(feature = "postgres")]
290 let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> =
291 pg_db.clone();
292 #[cfg(feature = "postgres")]
293 let kv: Arc<dyn MintKVStore<Err = cdk_database::Error> + Send + Sync> = pg_db.clone();
294 #[cfg(feature = "postgres")]
295 let keystore: Arc<
296 dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync,
297 > = pg_db;
298 #[cfg(feature = "postgres")]
299 return Ok((localstore, keystore, kv));
300
301 #[cfg(not(feature = "postgres"))]
302 bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
303 }
304 #[cfg(not(feature = "sqlite"))]
305 DatabaseEngine::Sqlite => {
306 bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
307 }
308 #[cfg(not(feature = "postgres"))]
309 DatabaseEngine::Postgres => {
310 bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
311 }
312 }
313}
314
315#[cfg(feature = "sqlite")]
316async fn setup_sqlite_database(
317 work_dir: &Path,
318 _password: Option<String>,
319) -> Result<Arc<MintSqliteDatabase>> {
320 let sql_db_path = work_dir.join("cdk-mintd.sqlite");
321
322 #[cfg(not(feature = "sqlcipher"))]
323 let db = MintSqliteDatabase::new(&sql_db_path).await?;
324 #[cfg(feature = "sqlcipher")]
325 let db = {
326 MintSqliteDatabase::new((sql_db_path, _password.unwrap())).await?
328 };
329
330 Ok(Arc::new(db))
331}
332
333async fn configure_mint_builder(
338 settings: &config::Settings,
339 mint_builder: MintBuilder,
340 runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
341 work_dir: &Path,
342 kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
343) -> Result<MintBuilder> {
344 let mint_builder = configure_basic_info(settings, mint_builder);
346
347 let mint_builder =
349 configure_lightning_backend(settings, mint_builder, runtime, work_dir, kv_store).await?;
350
351 let mint_builder = configure_cache(settings, mint_builder);
353
354 Ok(mint_builder)
355}
356
357fn configure_basic_info(settings: &config::Settings, mint_builder: MintBuilder) -> MintBuilder {
359 let mut contacts = Vec::new();
361 if let Some(nostr_key) = &settings.mint_info.contact_nostr_public_key {
362 contacts.push(ContactInfo::new("nostr".to_string(), nostr_key.to_string()));
363 }
364 if let Some(email) = &settings.mint_info.contact_email {
365 contacts.push(ContactInfo::new("email".to_string(), email.to_string()));
366 }
367
368 let mint_version = MintVersion::new(
370 "cdk-mintd".to_string(),
371 CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
372 );
373
374 let mut builder = mint_builder
376 .with_name(settings.mint_info.name.clone())
377 .with_version(mint_version)
378 .with_description(settings.mint_info.description.clone());
379
380 if let Some(long_description) = &settings.mint_info.description_long {
382 builder = builder.with_long_description(long_description.to_string());
383 }
384
385 for contact in contacts {
386 builder = builder.with_contact_info(contact);
387 }
388
389 if let Some(pubkey) = settings.mint_info.pubkey {
390 builder = builder.with_pubkey(pubkey);
391 }
392
393 if let Some(icon_url) = &settings.mint_info.icon_url {
394 builder = builder.with_icon_url(icon_url.to_string());
395 }
396
397 if let Some(motd) = &settings.mint_info.motd {
398 builder = builder.with_motd(motd.to_string());
399 }
400
401 if let Some(tos_url) = &settings.mint_info.tos_url {
402 builder = builder.with_tos_url(tos_url.to_string());
403 }
404
405 builder
406}
407async fn configure_lightning_backend(
409 settings: &config::Settings,
410 mut mint_builder: MintBuilder,
411 _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
412 work_dir: &Path,
413 _kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
414) -> Result<MintBuilder> {
415 let mint_melt_limits = MintMeltLimits {
416 mint_min: settings.ln.min_mint,
417 mint_max: settings.ln.max_mint,
418 melt_min: settings.ln.min_melt,
419 melt_max: settings.ln.max_melt,
420 };
421
422 tracing::debug!("Ln backend: {:?}", settings.ln.ln_backend);
423
424 match settings.ln.ln_backend {
425 #[cfg(feature = "cln")]
426 LnBackend::Cln => {
427 let cln_settings = settings
428 .cln
429 .clone()
430 .expect("Config checked at load that cln is some");
431 let cln = cln_settings
432 .setup(settings, CurrencyUnit::Msat, None, work_dir, _kv_store)
433 .await?;
434 #[cfg(feature = "prometheus")]
435 let cln = MetricsMintPayment::new(cln);
436
437 mint_builder = configure_backend_for_unit(
438 settings,
439 mint_builder,
440 CurrencyUnit::Sat,
441 mint_melt_limits,
442 Arc::new(cln),
443 )
444 .await?;
445 }
446 #[cfg(feature = "lnbits")]
447 LnBackend::LNbits => {
448 let lnbits_settings = settings.clone().lnbits.expect("Checked on config load");
449 let lnbits = lnbits_settings
450 .setup(settings, CurrencyUnit::Sat, None, work_dir, None)
451 .await?;
452 #[cfg(feature = "prometheus")]
453 let lnbits = MetricsMintPayment::new(lnbits);
454
455 mint_builder = configure_backend_for_unit(
456 settings,
457 mint_builder,
458 CurrencyUnit::Sat,
459 mint_melt_limits,
460 Arc::new(lnbits),
461 )
462 .await?;
463 }
464 #[cfg(feature = "lnd")]
465 LnBackend::Lnd => {
466 let lnd_settings = settings.clone().lnd.expect("Checked at config load");
467 let lnd = lnd_settings
468 .setup(settings, CurrencyUnit::Msat, None, work_dir, _kv_store)
469 .await?;
470 #[cfg(feature = "prometheus")]
471 let lnd = MetricsMintPayment::new(lnd);
472
473 mint_builder = configure_backend_for_unit(
474 settings,
475 mint_builder,
476 CurrencyUnit::Sat,
477 mint_melt_limits,
478 Arc::new(lnd),
479 )
480 .await?;
481 }
482 #[cfg(feature = "fakewallet")]
483 LnBackend::FakeWallet => {
484 let fake_wallet = settings.clone().fake_wallet.expect("Fake wallet defined");
485 tracing::info!("Using fake wallet: {:?}", fake_wallet);
486
487 for unit in fake_wallet.clone().supported_units {
488 let fake = fake_wallet
489 .setup(settings, unit.clone(), None, work_dir, _kv_store.clone())
490 .await?;
491 #[cfg(feature = "prometheus")]
492 let fake = MetricsMintPayment::new(fake);
493
494 mint_builder = configure_backend_for_unit(
495 settings,
496 mint_builder,
497 unit.clone(),
498 mint_melt_limits,
499 Arc::new(fake),
500 )
501 .await?;
502 }
503 }
504 #[cfg(feature = "grpc-processor")]
505 LnBackend::GrpcProcessor => {
506 let grpc_processor = settings
507 .clone()
508 .grpc_processor
509 .expect("grpc processor config defined");
510
511 tracing::info!(
512 "Attempting to start with gRPC payment processor at {}:{}.",
513 grpc_processor.addr,
514 grpc_processor.port
515 );
516
517 for unit in grpc_processor.clone().supported_units {
518 tracing::debug!("Adding unit: {:?}", unit);
519 let processor = grpc_processor
520 .setup(settings, unit.clone(), None, work_dir, None)
521 .await?;
522 #[cfg(feature = "prometheus")]
523 let processor = MetricsMintPayment::new(processor);
524
525 mint_builder = configure_backend_for_unit(
526 settings,
527 mint_builder,
528 unit.clone(),
529 mint_melt_limits,
530 Arc::new(processor),
531 )
532 .await?;
533 }
534 }
535 #[cfg(feature = "ldk-node")]
536 LnBackend::LdkNode => {
537 let ldk_node_settings = settings.clone().ldk_node.expect("Checked at config load");
538 tracing::info!("Using LDK Node backend: {:?}", ldk_node_settings);
539
540 let ldk_node = ldk_node_settings
541 .setup(settings, CurrencyUnit::Sat, _runtime, work_dir, None)
542 .await?;
543
544 mint_builder = configure_backend_for_unit(
545 settings,
546 mint_builder,
547 CurrencyUnit::Sat,
548 mint_melt_limits,
549 Arc::new(ldk_node),
550 )
551 .await?;
552 }
553 LnBackend::None => {
554 tracing::error!(
555 "Payment backend was not set or feature disabled. {:?}",
556 settings.ln.ln_backend
557 );
558 bail!("Lightning backend must be configured");
559 }
560 };
561
562 Ok(mint_builder)
563}
564
565async fn configure_backend_for_unit(
567 settings: &config::Settings,
568 mut mint_builder: MintBuilder,
569 unit: cdk::nuts::CurrencyUnit,
570 mint_melt_limits: MintMeltLimits,
571 backend: Arc<dyn MintPayment<Err = cdk_common::payment::Error> + Send + Sync>,
572) -> Result<MintBuilder> {
573 let payment_settings = backend.get_settings().await?;
574
575 if let Some(bolt12) = payment_settings.get("bolt12") {
576 if bolt12.as_bool().unwrap_or_default() {
577 mint_builder
578 .add_payment_processor(
579 unit.clone(),
580 PaymentMethod::Bolt12,
581 mint_melt_limits,
582 Arc::clone(&backend),
583 )
584 .await?;
585
586 let nut17_supported = SupportedMethods::default_bolt12(unit.clone());
587 mint_builder = mint_builder.with_supported_websockets(nut17_supported);
588 }
589 }
590
591 mint_builder
592 .add_payment_processor(
593 unit.clone(),
594 PaymentMethod::Bolt11,
595 mint_melt_limits,
596 backend,
597 )
598 .await?;
599
600 if let Some(input_fee) = settings.info.input_fee_ppk {
601 mint_builder.set_unit_fee(&unit, input_fee)?;
602 }
603
604 #[cfg(any(
605 feature = "cln",
606 feature = "lnbits",
607 feature = "lnd",
608 feature = "fakewallet",
609 feature = "grpc-processor",
610 feature = "ldk-node"
611 ))]
612 {
613 let nut17_supported = SupportedMethods::default_bolt11(unit);
614 mint_builder = mint_builder.with_supported_websockets(nut17_supported);
615 }
616
617 Ok(mint_builder)
618}
619
620fn configure_cache(settings: &config::Settings, mint_builder: MintBuilder) -> MintBuilder {
622 let cached_endpoints = vec![
623 CachedEndpoint::new(NUT19Method::Post, NUT19Path::MintBolt11),
624 CachedEndpoint::new(NUT19Method::Post, NUT19Path::MeltBolt11),
625 CachedEndpoint::new(NUT19Method::Post, NUT19Path::Swap),
626 ];
627
628 let cache: HttpCache = settings.info.http_cache.clone().into();
629 mint_builder.with_cache(Some(cache.ttl.as_secs()), cached_endpoints)
630}
631
632#[cfg(feature = "auth")]
633async fn setup_authentication(
634 settings: &config::Settings,
635 _work_dir: &Path,
636 mut mint_builder: MintBuilder,
637 _password: Option<String>,
638) -> Result<MintBuilder> {
639 if let Some(auth_settings) = settings.auth.clone() {
640 use cdk_common::database::DynMintAuthDatabase;
641
642 tracing::info!("Auth settings are defined. {:?}", auth_settings);
643 let auth_localstore: DynMintAuthDatabase = match settings.database.engine {
644 #[cfg(feature = "sqlite")]
645 DatabaseEngine::Sqlite => {
646 #[cfg(feature = "sqlite")]
647 {
648 let sql_db_path = _work_dir.join("cdk-mintd-auth.sqlite");
649 #[cfg(not(feature = "sqlcipher"))]
650 let sqlite_db = MintSqliteAuthDatabase::new(&sql_db_path).await?;
651 #[cfg(feature = "sqlcipher")]
652 let sqlite_db = {
653 MintSqliteAuthDatabase::new((sql_db_path, _password.unwrap())).await?
655 };
656
657 Arc::new(sqlite_db)
658 }
659 #[cfg(not(feature = "sqlite"))]
660 {
661 bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
662 }
663 }
664 #[cfg(feature = "postgres")]
665 DatabaseEngine::Postgres => {
666 #[cfg(feature = "postgres")]
667 {
668 let auth_db_config = settings.auth_database.as_ref().ok_or_else(|| {
670 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")
671 })?;
672
673 let auth_pg_config = auth_db_config.postgres.as_ref().ok_or_else(|| {
674 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")
675 })?;
676
677 if auth_pg_config.url.is_empty() {
678 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");
679 }
680
681 Arc::new(MintPgAuthDatabase::new(auth_pg_config.url.as_str()).await?)
682 }
683 #[cfg(not(feature = "postgres"))]
684 {
685 bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
686 }
687 }
688 #[cfg(not(feature = "sqlite"))]
689 DatabaseEngine::Sqlite => {
690 bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
691 }
692 #[cfg(not(feature = "postgres"))]
693 DatabaseEngine::Postgres => {
694 bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
695 }
696 };
697
698 let mut protected_endpoints = HashMap::new();
699 let mut blind_auth_endpoints = vec![];
700 let mut clear_auth_endpoints = vec![];
701 let mut unprotected_endpoints = vec![];
702
703 let mint_blind_auth_endpoint =
704 ProtectedEndpoint::new(Method::Post, RoutePath::MintBlindAuth);
705
706 protected_endpoints.insert(mint_blind_auth_endpoint, AuthRequired::Clear);
707
708 clear_auth_endpoints.push(mint_blind_auth_endpoint);
709
710 let mut add_endpoint = |endpoint: ProtectedEndpoint, auth_type: &AuthType| {
712 match auth_type {
713 AuthType::Blind => {
714 protected_endpoints.insert(endpoint, AuthRequired::Blind);
715 blind_auth_endpoints.push(endpoint);
716 }
717 AuthType::Clear => {
718 protected_endpoints.insert(endpoint, AuthRequired::Clear);
719 clear_auth_endpoints.push(endpoint);
720 }
721 AuthType::None => {
722 unprotected_endpoints.push(endpoint);
723 }
724 };
725 };
726
727 {
729 let mint_quote_protected_endpoint =
730 ProtectedEndpoint::new(cdk::nuts::Method::Post, RoutePath::MintQuoteBolt11);
731 add_endpoint(mint_quote_protected_endpoint, &auth_settings.get_mint_quote);
732 }
733
734 {
736 let check_mint_protected_endpoint =
737 ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11);
738 add_endpoint(
739 check_mint_protected_endpoint,
740 &auth_settings.check_mint_quote,
741 );
742 }
743
744 {
746 let mint_protected_endpoint =
747 ProtectedEndpoint::new(cdk::nuts::Method::Post, RoutePath::MintBolt11);
748 add_endpoint(mint_protected_endpoint, &auth_settings.mint);
749 }
750
751 {
753 let melt_quote_protected_endpoint = ProtectedEndpoint::new(
754 cdk::nuts::Method::Post,
755 cdk::nuts::RoutePath::MeltQuoteBolt11,
756 );
757 add_endpoint(melt_quote_protected_endpoint, &auth_settings.get_melt_quote);
758 }
759
760 {
762 let check_melt_protected_endpoint =
763 ProtectedEndpoint::new(Method::Get, RoutePath::MeltQuoteBolt11);
764 add_endpoint(
765 check_melt_protected_endpoint,
766 &auth_settings.check_melt_quote,
767 );
768 }
769
770 {
772 let melt_protected_endpoint =
773 ProtectedEndpoint::new(Method::Post, RoutePath::MeltBolt11);
774 add_endpoint(melt_protected_endpoint, &auth_settings.melt);
775 }
776
777 {
779 let swap_protected_endpoint = ProtectedEndpoint::new(Method::Post, RoutePath::Swap);
780 add_endpoint(swap_protected_endpoint, &auth_settings.swap);
781 }
782
783 {
785 let restore_protected_endpoint =
786 ProtectedEndpoint::new(Method::Post, RoutePath::Restore);
787 add_endpoint(restore_protected_endpoint, &auth_settings.restore);
788 }
789
790 {
792 let state_protected_endpoint =
793 ProtectedEndpoint::new(Method::Post, RoutePath::Checkstate);
794 add_endpoint(state_protected_endpoint, &auth_settings.check_proof_state);
795 }
796
797 {
799 let ws_protected_endpoint = ProtectedEndpoint::new(Method::Get, RoutePath::Ws);
800 add_endpoint(ws_protected_endpoint, &auth_settings.websocket_auth);
801 }
802
803 mint_builder = mint_builder.with_auth(
804 auth_localstore.clone(),
805 auth_settings.openid_discovery,
806 auth_settings.openid_client_id,
807 clear_auth_endpoints,
808 );
809 mint_builder =
810 mint_builder.with_blind_auth(auth_settings.mint_max_bat, blind_auth_endpoints);
811
812 let mut tx = auth_localstore.begin_transaction().await?;
813
814 tx.remove_protected_endpoints(unprotected_endpoints).await?;
815 tx.add_protected_endpoints(protected_endpoints).await?;
816 tx.commit().await?;
817 }
818 Ok(mint_builder)
819}
820
821async fn build_mint(
823 settings: &config::Settings,
824 keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
825 mint_builder: MintBuilder,
826) -> Result<Mint> {
827 if let Some(signatory_url) = settings.info.signatory_url.clone() {
828 tracing::info!(
829 "Connecting to remote signatory to {} with certs {:?}",
830 signatory_url,
831 settings.info.signatory_certs.clone()
832 );
833
834 Ok(mint_builder
835 .build_with_signatory(Arc::new(
836 cdk_signatory::SignatoryRpcClient::new(
837 signatory_url,
838 settings.info.signatory_certs.clone(),
839 )
840 .await?,
841 ))
842 .await?)
843 } else if let Some(seed) = settings.info.seed.clone() {
844 let seed_bytes: Vec<u8> = seed.into();
845 Ok(mint_builder.build_with_seed(keystore, &seed_bytes).await?)
846 } else if let Some(mnemonic) = settings
847 .info
848 .mnemonic
849 .clone()
850 .map(|s| Mnemonic::from_str(&s))
851 .transpose()?
852 {
853 Ok(mint_builder
854 .build_with_seed(keystore, &mnemonic.to_seed_normalized(""))
855 .await?)
856 } else {
857 bail!("No seed nor remote signatory set");
858 }
859}
860
861async fn start_services_with_shutdown(
862 mint: Arc<cdk::mint::Mint>,
863 settings: &config::Settings,
864 work_dir: &Path,
865 mint_builder_info: cdk::nuts::MintInfo,
866 shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
867 routers: Vec<Router>,
868) -> Result<()> {
869 let listen_addr = settings.info.listen_host.clone();
870 let listen_port = settings.info.listen_port;
871 let cache: HttpCache = settings.info.http_cache.clone().into();
872
873 #[cfg(feature = "management-rpc")]
874 let mut rpc_enabled = false;
875 #[cfg(not(feature = "management-rpc"))]
876 let rpc_enabled = false;
877
878 #[cfg(feature = "management-rpc")]
879 let mut rpc_server: Option<cdk_mint_rpc::MintRPCServer> = None;
880
881 #[cfg(feature = "management-rpc")]
882 {
883 if let Some(rpc_settings) = settings.mint_management_rpc.clone() {
884 if rpc_settings.enabled {
885 let addr = rpc_settings.address.unwrap_or("127.0.0.1".to_string());
886 let port = rpc_settings.port.unwrap_or(8086);
887 let mut mint_rpc = cdk_mint_rpc::MintRPCServer::new(&addr, port, mint.clone())?;
888
889 let tls_dir = rpc_settings.tls_dir_path.unwrap_or(work_dir.join("tls"));
890
891 let tls_dir = if tls_dir.exists() {
892 Some(tls_dir)
893 } else {
894 tracing::warn!(
895 "TLS directory does not exist: {}. Starting RPC server in INSECURE mode without TLS encryption",
896 tls_dir.display()
897 );
898 None
899 };
900
901 mint_rpc.start(tls_dir).await?;
902
903 rpc_server = Some(mint_rpc);
904
905 rpc_enabled = true;
906 }
907 }
908 }
909
910 let desired_quote_ttl: QuoteTTL = settings.info.quote_ttl.unwrap_or_default();
912
913 if rpc_enabled {
914 if mint.mint_info().await.is_err() {
915 tracing::info!("Mint info not set on mint, setting.");
916 mint.set_mint_info(mint_builder_info).await?;
918 mint.set_quote_ttl(desired_quote_ttl).await?;
919 } else {
920 if !mint.quote_ttl_is_persisted().await? {
922 mint.set_quote_ttl(desired_quote_ttl).await?;
923 }
924 let mint_version = MintVersion::new(
926 "cdk-mintd".to_string(),
927 CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
928 );
929 let mut stored_mint_info = mint.mint_info().await?;
930 stored_mint_info.version = Some(mint_version);
931 mint.set_mint_info(stored_mint_info).await?;
932
933 tracing::info!("Mint info already set, not using config file settings.");
934 }
935 } else {
936 tracing::info!("RPC not enabled, using mint info and quote TTL from config.");
938 let mut mint_builder_info = mint_builder_info;
939
940 if let Ok(mint_info) = mint.mint_info().await {
941 if mint_builder_info.pubkey.is_none() {
942 mint_builder_info.pubkey = mint_info.pubkey;
943 }
944 }
945
946 mint.set_mint_info(mint_builder_info).await?;
947 mint.set_quote_ttl(desired_quote_ttl).await?;
948 }
949
950 let mint_info = mint.mint_info().await?;
951 let nut04_methods = mint_info.nuts.nut04.supported_methods();
952 let nut05_methods = mint_info.nuts.nut05.supported_methods();
953
954 let bolt12_supported = nut04_methods.contains(&&PaymentMethod::Bolt12)
955 || nut05_methods.contains(&&PaymentMethod::Bolt12);
956
957 let v1_service =
958 cdk_axum::create_mint_router_with_custom_cache(Arc::clone(&mint), cache, bolt12_supported)
959 .await?;
960
961 let mut mint_service = Router::new()
962 .merge(v1_service)
963 .layer(
964 ServiceBuilder::new()
965 .layer(RequestDecompressionLayer::new())
966 .layer(CompressionLayer::new()),
967 )
968 .layer(TraceLayer::new_for_http());
969
970 for router in routers {
971 mint_service = mint_service.merge(router);
972 }
973
974 #[cfg(feature = "swagger")]
975 {
976 if settings.info.enable_swagger_ui.unwrap_or(false) {
977 mint_service = mint_service.merge(
978 utoipa_swagger_ui::SwaggerUi::new("/swagger-ui")
979 .url("/api-docs/openapi.json", cdk_axum::ApiDoc::openapi()),
980 );
981 }
982 }
983 let (shutdown_tx, _) = tokio::sync::broadcast::channel::<()>(1);
985
986 #[cfg(feature = "prometheus")]
988 let prometheus_handle = {
989 if let Some(prometheus_settings) = &settings.prometheus {
990 if prometheus_settings.enabled {
991 let addr = prometheus_settings
992 .address
993 .clone()
994 .unwrap_or("127.0.0.1".to_string());
995 let port = prometheus_settings.port.unwrap_or(9000);
996
997 let address = format!("{}:{}", addr, port)
998 .parse()
999 .expect("Invalid prometheus address");
1000
1001 let server = cdk_prometheus::PrometheusBuilder::new()
1002 .bind_address(address)
1003 .build_with_cdk_metrics()?;
1004
1005 let mut shutdown_rx = shutdown_tx.subscribe();
1006 let prometheus_shutdown = async move {
1007 let _ = shutdown_rx.recv().await;
1008 };
1009
1010 Some(tokio::spawn(async move {
1011 if let Err(e) = server.start(prometheus_shutdown).await {
1012 tracing::error!("Failed to start prometheus server: {}", e);
1013 }
1014 }))
1015 } else {
1016 None
1017 }
1018 } else {
1019 None
1020 }
1021 };
1022
1023 #[cfg(not(feature = "prometheus"))]
1024 let prometheus_handle: Option<tokio::task::JoinHandle<()>> = None;
1025
1026 mint.start().await?;
1027
1028 let socket_addr = SocketAddr::from_str(&format!("{listen_addr}:{listen_port}"))?;
1029
1030 let listener = tokio::net::TcpListener::bind(socket_addr).await?;
1031
1032 tracing::info!("listening on {}", listener.local_addr().unwrap());
1033
1034 let shutdown_broadcast_task = {
1036 let shutdown_tx = shutdown_tx.clone();
1037 tokio::spawn(async move {
1038 shutdown_signal.await;
1039 tracing::info!("Shutdown signal received, broadcasting to all services");
1040 let _ = shutdown_tx.send(());
1041 })
1042 };
1043
1044 let mut axum_shutdown_rx = shutdown_tx.subscribe();
1046 let axum_shutdown = async move {
1047 let _ = axum_shutdown_rx.recv().await;
1048 };
1049
1050 let axum_result = axum::serve(listener, mint_service).with_graceful_shutdown(axum_shutdown);
1052
1053 match axum_result.await {
1054 Ok(_) => {
1055 tracing::info!("Axum server stopped with okay status");
1056 }
1057 Err(err) => {
1058 tracing::warn!("Axum server stopped with error");
1059 tracing::error!("{}", err);
1060 bail!("Axum exited with error")
1061 }
1062 }
1063
1064 let _ = shutdown_broadcast_task.await;
1066
1067 #[cfg(feature = "prometheus")]
1069 if let Some(handle) = prometheus_handle {
1070 if let Err(e) = handle.await {
1071 tracing::warn!("Prometheus server task failed: {}", e);
1072 }
1073 }
1074
1075 mint.stop().await?;
1076
1077 #[cfg(feature = "management-rpc")]
1078 {
1079 if let Some(rpc_server) = rpc_server {
1080 rpc_server.stop().await?;
1081 }
1082 }
1083
1084 Ok(())
1085}
1086
1087async fn shutdown_signal() {
1088 tokio::signal::ctrl_c()
1089 .await
1090 .expect("failed to install CTRL+C handler");
1091 tracing::info!("Shutdown signal received");
1092}
1093
1094fn work_dir() -> Result<PathBuf> {
1095 let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?;
1096 let dir = home_dir.join(".cdk-mintd");
1097
1098 std::fs::create_dir_all(&dir)?;
1099
1100 Ok(dir)
1101}
1102
1103pub async fn run_mintd(
1105 work_dir: &Path,
1106 settings: &config::Settings,
1107 db_password: Option<String>,
1108 enable_logging: bool,
1109 runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1110 routers: Vec<Router>,
1111) -> Result<()> {
1112 let _guard = if enable_logging {
1113 setup_tracing(work_dir, &settings.info.logging)?
1114 } else {
1115 None
1116 };
1117
1118 let result = run_mintd_with_shutdown(
1119 work_dir,
1120 settings,
1121 shutdown_signal(),
1122 db_password,
1123 runtime,
1124 routers,
1125 )
1126 .await;
1127
1128 if let Some(guard) = _guard {
1130 tracing::info!("Shutting down logging worker thread");
1131 drop(guard);
1132 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
1134 }
1135
1136 tracing::info!("Mintd shutdown");
1137
1138 result
1139}
1140
1141pub async fn run_mintd_with_shutdown(
1143 work_dir: &Path,
1144 settings: &config::Settings,
1145 shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
1146 db_password: Option<String>,
1147 runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1148 routers: Vec<Router>,
1149) -> Result<()> {
1150 let (localstore, keystore, kv) = initial_setup(work_dir, settings, db_password.clone()).await?;
1151
1152 let mint_builder = MintBuilder::new(localstore);
1153
1154 let maybe_mint_builder = {
1157 #[cfg(feature = "management-rpc")]
1158 {
1159 if let Some(rpc_settings) = settings.mint_management_rpc.clone() {
1160 if rpc_settings.enabled {
1161 let mut tmp = mint_builder;
1163 if let Err(e) = tmp.init_from_db_if_present().await {
1164 tracing::warn!("Failed to init builder from DB: {}", e);
1165 }
1166 tmp
1167 } else {
1168 mint_builder
1169 }
1170 } else {
1171 mint_builder
1172 }
1173 }
1174 #[cfg(not(feature = "management-rpc"))]
1175 {
1176 mint_builder
1177 }
1178 };
1179
1180 let mint_builder =
1181 configure_mint_builder(settings, maybe_mint_builder, runtime, work_dir, Some(kv)).await?;
1182 #[cfg(feature = "auth")]
1183 let mint_builder = setup_authentication(settings, work_dir, mint_builder, db_password).await?;
1184
1185 let config_mint_info = mint_builder.current_mint_info();
1186
1187 let mint = build_mint(settings, keystore, mint_builder).await?;
1188
1189 tracing::debug!("Mint built from builder.");
1190
1191 let mint = Arc::new(mint);
1192
1193 start_services_with_shutdown(
1194 mint.clone(),
1195 settings,
1196 work_dir,
1197 config_mint_info,
1198 shutdown_signal,
1199 routers,
1200 )
1201 .await
1202}
1203
1204#[cfg(test)]
1205mod tests {
1206 use super::*;
1207
1208 #[test]
1209 fn test_postgres_auth_url_validation() {
1210 let auth_config = config::PostgresAuthConfig {
1214 url: "".to_string(),
1215 ..Default::default()
1216 };
1217 assert!(auth_config.url.is_empty());
1218
1219 let auth_config = config::PostgresAuthConfig {
1221 url: "postgresql://user:password@localhost:5432/auth_db".to_string(),
1222 ..Default::default()
1223 };
1224 assert!(!auth_config.url.is_empty());
1225 }
1226}