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