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