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, Context, Result};
14use axum::extract::DefaultBodyLimit;
15use axum::Router;
16use bip39::Mnemonic;
17use cdk::cdk_database::{self, KVStore, MintDatabase, MintKeysDatabase};
18use cdk::mint::{Mint, MintBuilder, MintMeltLimits};
19use cdk::nuts::nut00::KnownMethod;
20#[cfg(any(
21 feature = "cln",
22 feature = "lnbits",
23 feature = "lnd",
24 feature = "ldk-node",
25 feature = "fakewallet",
26 feature = "bdk",
27 feature = "grpc-processor"
28))]
29use cdk::nuts::nut17::SupportedMethods;
30use cdk::nuts::nut19::{CachedEndpoint, Method as NUT19Method, Path as NUT19Path};
31use cdk::nuts::{
32 AuthRequired, ContactInfo, Method, MintVersion, PaymentMethod, ProtectedEndpoint, RoutePath,
33};
34use cdk_axum::cache::HttpCache;
35use cdk_common::common::QuoteTTL;
36use cdk_common::database::DynMintDatabase;
37#[cfg(feature = "prometheus")]
39use cdk_common::payment::MetricsMintPayment;
40use cdk_common::payment::MintPayment;
41#[cfg(feature = "postgres")]
42use cdk_postgres::{MintPgAuthDatabase, MintPgDatabase, PgConfig};
43#[cfg(feature = "sqlite")]
44use cdk_sqlite::mint::MintSqliteAuthDatabase;
45#[cfg(feature = "sqlite")]
46use cdk_sqlite::MintSqliteDatabase;
47use cli::CLIArgs;
48use config::{AuthType, DatabaseEngine, LnBackend};
49use env_vars::ENV_WORK_DIR;
50use setup::LnBackendSetup;
51use tower::ServiceBuilder;
52use tower_http::compression::CompressionLayer;
53use tower_http::decompression::RequestDecompressionLayer;
54use tower_http::trace::TraceLayer;
55use tracing_appender::{non_blocking, rolling};
56use tracing_subscriber::fmt::writer::MakeWriterExt;
57use tracing_subscriber::EnvFilter;
58
59pub mod cli;
60pub mod config;
61pub mod env_vars;
62pub mod setup;
63
64const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
65const DEFAULT_BATCH_MINT_SIZE: u64 = 100;
66const REQUEST_BODY_LIMIT_BYTES: usize = 1_048_576;
67
68#[cfg(all(feature = "management-rpc", feature = "bdk"))]
69type ConfiguredWalletInfoProvider = Option<cdk_mint_rpc::DynWalletInfoProvider>;
70#[cfg(not(all(feature = "management-rpc", feature = "bdk")))]
71type ConfiguredWalletInfoProvider = ();
72
73#[cfg(all(feature = "management-rpc", feature = "bdk"))]
74#[derive(Clone)]
75struct BdkWalletInfoProvider {
76 bdk: Arc<cdk_bdk::CdkBdk>,
77}
78
79#[cfg(all(feature = "management-rpc", feature = "bdk"))]
80#[async_trait::async_trait]
81impl cdk_mint_rpc::WalletInfoProvider for BdkWalletInfoProvider {
82 async fn get_balance(
83 &self,
84 ) -> std::result::Result<cdk_mint_rpc::wallet::GetBalanceResponse, cdk_mint_rpc::WalletInfoError>
85 {
86 let balance = self.bdk.wallet_balance().await;
87
88 Ok(cdk_mint_rpc::wallet::GetBalanceResponse {
89 confirmed_sat: balance.confirmed_sat,
90 trusted_pending_sat: balance.trusted_pending_sat,
91 untrusted_pending_sat: balance.untrusted_pending_sat,
92 immature_sat: balance.immature_sat,
93 trusted_spendable_sat: balance.trusted_spendable_sat,
94 total_sat: balance.total_sat,
95 network: balance.network,
96 synced_height: balance.synced_height,
97 })
98 }
99
100 async fn list_transactions(
101 &self,
102 offset: usize,
103 limit: usize,
104 ) -> std::result::Result<cdk_mint_rpc::WalletTransactionPage, cdk_mint_rpc::WalletInfoError>
105 {
106 let page = self
107 .bdk
108 .wallet_transactions(offset, limit)
109 .await
110 .map_err(|err| cdk_mint_rpc::WalletInfoError::new(err.to_string()))?;
111
112 Ok(cdk_mint_rpc::WalletTransactionPage {
113 transactions: page
114 .items
115 .into_iter()
116 .map(|transaction| cdk_mint_rpc::wallet::WalletTransaction {
117 txid: transaction.txid,
118 received_sat: transaction.received_sat,
119 sent_sat: transaction.sent_sat,
120 fee_sat: transaction.fee_sat,
121 balance_delta_sat: transaction.balance_delta_sat,
122 confirmation_height: transaction.confirmation_height,
123 confirmation_time: transaction.confirmation_time,
124 first_seen: transaction.first_seen,
125 })
126 .collect(),
127 total: page.total,
128 })
129 }
130
131 async fn list_addresses(
132 &self,
133 offset: usize,
134 limit: usize,
135 ) -> std::result::Result<cdk_mint_rpc::WalletAddressPage, cdk_mint_rpc::WalletInfoError> {
136 let page = self
137 .bdk
138 .wallet_addresses(offset, limit)
139 .await
140 .map_err(|err| cdk_mint_rpc::WalletInfoError::new(err.to_string()))?;
141
142 Ok(cdk_mint_rpc::WalletAddressPage {
143 addresses: page
144 .items
145 .into_iter()
146 .map(|address| cdk_mint_rpc::wallet::WalletAddress {
147 address: address.address,
148 keychain: match address.keychain {
149 cdk_bdk::WalletKeychain::External => {
150 cdk_mint_rpc::wallet::KeychainKind::External.into()
151 }
152 cdk_bdk::WalletKeychain::Internal => {
153 cdk_mint_rpc::wallet::KeychainKind::Internal.into()
154 }
155 },
156 derivation_index: address.derivation_index,
157 used: address.used,
158 balance_sat: address.balance_sat,
159 confirmed_balance_sat: address.confirmed_balance_sat,
160 })
161 .collect(),
162 total: page.total,
163 })
164 }
165}
166
167#[cfg(all(feature = "management-rpc", feature = "bdk"))]
168fn no_wallet_info_provider() -> ConfiguredWalletInfoProvider {
169 None
170}
171
172#[cfg(not(all(feature = "management-rpc", feature = "bdk")))]
173fn no_wallet_info_provider() -> ConfiguredWalletInfoProvider {}
174
175fn extract_supported_payment_methods(mint_info: &cdk::nuts::MintInfo) -> Vec<String> {
176 let mut seen = HashSet::new();
177 mint_info
178 .nuts
179 .nut04
180 .methods
181 .iter()
182 .map(|method| method.method.to_string())
183 .filter(|method| seen.insert(method.clone()))
184 .collect()
185}
186
187#[cfg(feature = "cln")]
188fn expand_path(path: &str) -> Option<PathBuf> {
189 if path == "~" {
190 return home::home_dir();
191 }
192
193 if let Some(remainder) = path.strip_prefix("~/") {
194 return home::home_dir().map(|home_dir| home_dir.join(remainder));
195 }
196
197 Some(PathBuf::from(path))
198}
199
200async fn initial_setup(
204 work_dir: &Path,
205 settings: &config::Settings,
206 db_password: Option<String>,
207) -> Result<(
208 DynMintDatabase,
209 Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
210 Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync>,
211)> {
212 tracing::info!("Initializing database...");
213 let (localstore, keystore, kv) = setup_database(settings, work_dir, db_password).await?;
214 tracing::info!("Database initialized successfully");
215 Ok((localstore, keystore, kv))
216}
217
218pub fn setup_tracing(
222 work_dir: &Path,
223 logging_config: &config::LoggingConfig,
224) -> Result<Option<tracing_appender::non_blocking::WorkerGuard>> {
225 let default_filter = "debug";
226 let hyper_filter = "hyper=warn,rustls=warn,reqwest=warn";
227 let h2_filter = "h2=warn";
228 let tower_filter = "tower=warn";
229 let tower_http = "tower_http=warn";
230 let rustls = "rustls=warn";
231 let tungstenite = "tungstenite=warn";
232 let tokio_postgres = "tokio_postgres=warn";
233
234 let env_filter = EnvFilter::new(format!(
235 "{default_filter},{hyper_filter},{h2_filter},{tower_filter},{tower_http},{rustls},{tungstenite},{tokio_postgres}"
236 ));
237
238 use config::LoggingOutput;
239 match logging_config.output {
240 LoggingOutput::Stderr => {
241 let console_level = logging_config
243 .console_level
244 .as_deref()
245 .unwrap_or("info")
246 .parse::<tracing::Level>()
247 .unwrap_or(tracing::Level::INFO);
248
249 let stderr = std::io::stderr.with_max_level(console_level);
250
251 tracing_subscriber::fmt()
252 .with_env_filter(env_filter)
253 .with_ansi(false)
254 .with_writer(stderr)
255 .init();
256
257 tracing::info!("Logging initialized: console only ({}+)", console_level);
258 Ok(None)
259 }
260 LoggingOutput::File => {
261 let file_level = logging_config
263 .file_level
264 .as_deref()
265 .unwrap_or("debug")
266 .parse::<tracing::Level>()
267 .unwrap_or(tracing::Level::DEBUG);
268
269 let logs_dir = work_dir.join("logs");
271 std::fs::create_dir_all(&logs_dir)?;
272
273 let file_appender = rolling::daily(&logs_dir, "cdk-mintd.log");
275 let (non_blocking_appender, guard) = non_blocking(file_appender);
276
277 let file_writer = non_blocking_appender.with_max_level(file_level);
278
279 tracing_subscriber::fmt()
280 .with_env_filter(env_filter)
281 .with_ansi(false)
282 .with_writer(file_writer)
283 .init();
284
285 tracing::info!(
286 "Logging initialized: file only at {}/cdk-mintd.log ({}+)",
287 logs_dir.display(),
288 file_level
289 );
290 Ok(Some(guard))
291 }
292 LoggingOutput::Both => {
293 let console_level = logging_config
295 .console_level
296 .as_deref()
297 .unwrap_or("info")
298 .parse::<tracing::Level>()
299 .unwrap_or(tracing::Level::INFO);
300 let file_level = logging_config
301 .file_level
302 .as_deref()
303 .unwrap_or("debug")
304 .parse::<tracing::Level>()
305 .unwrap_or(tracing::Level::DEBUG);
306
307 let logs_dir = work_dir.join("logs");
309 std::fs::create_dir_all(&logs_dir)?;
310
311 let file_appender = rolling::daily(&logs_dir, "cdk-mintd.log");
313 let (non_blocking_appender, guard) = non_blocking(file_appender);
314
315 let stderr = std::io::stderr.with_max_level(console_level);
317 let file_writer = non_blocking_appender.with_max_level(file_level);
318
319 tracing_subscriber::fmt()
320 .with_env_filter(env_filter)
321 .with_ansi(false)
322 .with_writer(stderr.and(file_writer))
323 .init();
324
325 tracing::info!(
326 "Logging initialized: console ({}+) and file at {}/cdk-mintd.log ({}+)",
327 console_level,
328 logs_dir.display(),
329 file_level
330 );
331 Ok(Some(guard))
332 }
333 }
334}
335
336pub async fn get_work_directory(args: &CLIArgs) -> Result<PathBuf> {
338 let work_dir = if let Some(work_dir) = &args.work_dir {
339 tracing::info!("Using work dir from cmd arg");
340 work_dir.clone()
341 } else if let Ok(env_work_dir) = env::var(ENV_WORK_DIR) {
342 tracing::info!("Using work dir from env var");
343 env_work_dir.into()
344 } else {
345 work_dir()?
346 };
347 tracing::info!("Using work dir: {}", work_dir.display());
348 Ok(work_dir)
349}
350
351pub fn load_settings(work_dir: &Path, config_path: Option<PathBuf>) -> Result<config::Settings> {
353 let config_file_arg = match config_path {
355 Some(c) => c,
356 None => work_dir.join("config.toml"),
357 };
358
359 let mut settings = if config_file_arg.exists() {
360 config::Settings::try_new(Some(config_file_arg.clone()))
361 .with_context(|| format!("Failed to read config file {}", config_file_arg.display()))?
362 } else {
363 tracing::info!("Config file does not exist. Attempting to read env vars");
364 config::Settings::default()
365 };
366 settings.from_env()
369}
370
371pub fn load_settings_from_args(work_dir: &Path, args: &CLIArgs) -> Result<config::Settings> {
373 let mut settings = load_settings(work_dir, args.config.clone())?;
374
375 if let Some(seed_file) = args.seed_file.as_deref() {
376 apply_seed_file(&mut settings, seed_file)?;
377 }
378
379 Ok(settings)
380}
381
382pub fn apply_seed_file(settings: &mut config::Settings, seed_file: &Path) -> Result<()> {
384 let mnemonic = std::fs::read_to_string(seed_file)
385 .with_context(|| format!("Failed to read seed file {}", seed_file.display()))?;
386 let mnemonic = mnemonic.trim();
387
388 if mnemonic.is_empty() {
389 bail!("Seed file {} is empty", seed_file.display());
390 }
391
392 Mnemonic::parse(mnemonic)
393 .with_context(|| format!("Invalid seed phrase in seed file {}", seed_file.display()))?;
394
395 settings.info.seed = None;
396 settings.info.mnemonic = Some(mnemonic.to_owned());
397
398 #[cfg(feature = "bdk")]
399 if settings
400 .onchain
401 .as_ref()
402 .is_some_and(|onchain| onchain.onchain_backend == config::OnchainBackend::Bdk)
403 {
404 let mut bdk = settings.bdk.clone().unwrap_or_default();
405 bdk.mnemonic = Some(mnemonic.to_owned());
406 settings.bdk = Some(bdk);
407 }
408
409 #[cfg(feature = "ldk-node")]
410 if settings
411 .ln
412 .iter()
413 .any(|ln| ln.ln_backend == LnBackend::LdkNode)
414 {
415 let mut ldk_node = settings.ldk_node.clone().unwrap_or_default();
416 ldk_node.ldk_node_mnemonic = Some(mnemonic.to_owned());
417 settings.ldk_node = Some(ldk_node);
418 }
419
420 Ok(())
421}
422
423async fn setup_database(
424 settings: &config::Settings,
425 _work_dir: &Path,
426 _db_password: Option<String>,
427) -> Result<(
428 DynMintDatabase,
429 Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
430 Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync>,
431)> {
432 tracing::info!("Using database engine: {:?}", settings.database.engine);
433 match settings.database.engine {
434 #[cfg(feature = "sqlite")]
435 DatabaseEngine::Sqlite => {
436 let db = setup_sqlite_database(_work_dir, _db_password).await?;
437 let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> = db.clone();
438 let kv: Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync> = db.clone();
439 let keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync> = db;
440 Ok((localstore, keystore, kv))
441 }
442 #[cfg(feature = "postgres")]
443 DatabaseEngine::Postgres => {
444 let pg_config = settings.database.postgres.as_ref().ok_or_else(|| {
446 anyhow!("PostgreSQL configuration is required when using PostgreSQL engine")
447 })?;
448
449 if pg_config.url.is_empty() {
450 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");
451 }
452
453 #[cfg(feature = "postgres")]
454 let db_config = PgConfig::new(
455 pg_config.url.as_str(),
456 pg_config.tls_mode.as_deref(),
457 pg_config.max_connections,
458 pg_config.connection_timeout_seconds,
459 );
460 #[cfg(feature = "postgres")]
461 let pg_db = Arc::new(MintPgDatabase::new(db_config).await?);
462 tracing::info!("PostgreSQL database connection established");
463 #[cfg(feature = "postgres")]
464 let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> =
465 pg_db.clone();
466 #[cfg(feature = "postgres")]
467 let kv: Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync> = pg_db.clone();
468 #[cfg(feature = "postgres")]
469 let keystore: Arc<
470 dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync,
471 > = pg_db;
472 #[cfg(feature = "postgres")]
473 return Ok((localstore, keystore, kv));
474
475 #[cfg(not(feature = "postgres"))]
476 bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
477 }
478 #[cfg(not(feature = "sqlite"))]
479 DatabaseEngine::Sqlite => {
480 bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
481 }
482 #[cfg(not(feature = "postgres"))]
483 DatabaseEngine::Postgres => {
484 bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
485 }
486 }
487}
488
489#[cfg(feature = "sqlite")]
490async fn setup_sqlite_database(
491 work_dir: &Path,
492 _password: Option<String>,
493) -> Result<Arc<MintSqliteDatabase>> {
494 let sql_db_path = work_dir.join("cdk-mintd.sqlite");
495 tracing::info!("SQLite database path: {}", sql_db_path.display());
496
497 #[cfg(not(feature = "sqlcipher"))]
498 let db = MintSqliteDatabase::new(&sql_db_path).await?;
499 #[cfg(feature = "sqlcipher")]
500 let db = {
501 let password = _password
503 .ok_or_else(|| anyhow!("Password required when sqlcipher feature is enabled"))?;
504 tracing::info!("Using SQLCipher encryption for SQLite database");
505 MintSqliteDatabase::new((sql_db_path, password)).await?
506 };
507
508 tracing::info!("SQLite database initialized successfully");
509 Ok(Arc::new(db))
510}
511
512async fn configure_mint_builder_with_wallet_info(
517 settings: &config::Settings,
518 mint_builder: MintBuilder,
519 runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
520 work_dir: &Path,
521 kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
522) -> Result<(MintBuilder, ConfiguredWalletInfoProvider)> {
523 settings
524 .validate_backend_pairing()
525 .map_err(anyhow::Error::msg)?;
526
527 let mint_builder = configure_basic_info(settings, mint_builder);
529
530 #[cfg(feature = "fakewallet")]
532 if settings
533 .ln
534 .iter()
535 .any(|ln| ln.ln_backend == LnBackend::FakeWallet)
536 {
537 if let Some(_onchain) = &settings.onchain {
538 #[cfg(feature = "bdk")]
539 if _onchain.onchain_backend == config::OnchainBackend::Bdk {
540 if let Some(bdk) = &settings.bdk {
541 if let Some(network) = &bdk.network {
542 let network = network.to_lowercase();
543 if network == "mainnet" || network == "bitcoin" {
544 bail!("Fake wallet cannot be used for Lightning when On-chain is configured for Mainnet");
545 }
546 }
547 }
548 }
549 }
550 }
551
552 let mint_builder = configure_lightning_backend(
554 settings,
555 mint_builder,
556 runtime.clone(),
557 work_dir,
558 kv_store.clone(),
559 )
560 .await?;
561
562 let (mint_builder, wallet_info_provider) = configure_onchain_backend_with_wallet_info(
564 settings,
565 mint_builder,
566 runtime,
567 work_dir,
568 kv_store,
569 )
570 .await?;
571
572 let mint_info = mint_builder.current_mint_info();
574 let payment_methods = extract_supported_payment_methods(&mint_info);
575
576 let mint_builder = mint_builder
578 .with_batch_minting(Some(DEFAULT_BATCH_MINT_SIZE), Some(payment_methods.clone()));
579
580 let mint_builder = configure_cache(settings, mint_builder, &payment_methods).await?;
582
583 let mint_builder =
585 mint_builder.with_limits(settings.limits.max_inputs, settings.limits.max_outputs);
586
587 if mint_builder
589 .current_mint_info()
590 .nuts
591 .nut04
592 .methods
593 .is_empty()
594 {
595 bail!("At least one payment backend (Lightning or On-chain) must be configured");
596 }
597
598 Ok((mint_builder, wallet_info_provider))
599}
600
601#[cfg(test)]
602async fn configure_mint_builder(
603 settings: &config::Settings,
604 mint_builder: MintBuilder,
605 runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
606 work_dir: &Path,
607 kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
608) -> Result<MintBuilder> {
609 Ok(
610 configure_mint_builder_with_wallet_info(
611 settings,
612 mint_builder,
613 runtime,
614 work_dir,
615 kv_store,
616 )
617 .await?
618 .0,
619 )
620}
621
622fn configure_basic_info(settings: &config::Settings, mint_builder: MintBuilder) -> MintBuilder {
624 let mut contacts = Vec::new();
626 if let Some(nostr_key) = &settings.mint_info.contact_nostr_public_key {
627 if !nostr_key.is_empty() {
628 contacts.push(ContactInfo::new("nostr".to_string(), nostr_key.to_string()));
629 }
630 }
631 if let Some(email) = &settings.mint_info.contact_email {
632 if !email.is_empty() {
633 contacts.push(ContactInfo::new("email".to_string(), email.to_string()));
634 }
635 }
636
637 let mint_version = MintVersion::new(
639 "cdk-mintd".to_string(),
640 CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
641 );
642
643 let mut builder = mint_builder.with_version(mint_version);
645
646 if !settings.mint_info.name.is_empty() {
648 builder = builder.with_name(settings.mint_info.name.clone());
649 }
650
651 if !settings.mint_info.description.is_empty() {
653 builder = builder.with_description(settings.mint_info.description.clone());
654 }
655
656 if let Some(long_description) = &settings.mint_info.description_long {
658 if !long_description.is_empty() {
659 builder = builder.with_long_description(long_description.to_string());
660 }
661 }
662
663 for contact in contacts {
664 builder = builder.with_contact_info(contact);
665 }
666
667 if let Some(pubkey) = settings.mint_info.pubkey {
668 builder = builder.with_pubkey(pubkey);
669 }
670
671 if let Some(icon_url) = &settings.mint_info.icon_url {
672 if !icon_url.is_empty() {
673 builder = builder.with_icon_url(icon_url.to_string());
674 }
675 }
676
677 if let Some(motd) = &settings.mint_info.motd {
678 if !motd.is_empty() {
679 builder = builder.with_motd(motd.to_string());
680 }
681 }
682
683 if let Some(tos_url) = &settings.mint_info.tos_url {
684 if !tos_url.is_empty() {
685 builder = builder.with_tos_url(tos_url.to_string());
686 }
687 }
688
689 builder = builder.with_keyset_v2(settings.info.use_keyset_v2);
690
691 builder
692}
693async fn configure_lightning_backend(
695 settings: &config::Settings,
696 mut mint_builder: MintBuilder,
697 _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
698 work_dir: &Path,
699 _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
700) -> Result<MintBuilder> {
701 if settings.ln.is_empty() {
702 tracing::info!("No Lightning backend configured");
703 return Ok(mint_builder);
704 }
705
706 #[cfg(feature = "fakewallet")]
707 let mut configure_fake_wallet_keyset_rotations = false;
708
709 for ln_entry in &settings.ln {
710 let mint_melt_limits = MintMeltLimits {
711 mint_min: ln_entry.min_mint,
712 mint_max: ln_entry.max_mint,
713 melt_min: ln_entry.min_melt,
714 melt_max: ln_entry.max_melt,
715 };
716
717 tracing::debug!(
718 "Ln backend: {:?} (unit: {:?})",
719 ln_entry.ln_backend,
720 ln_entry.unit
721 );
722
723 match ln_entry.ln_backend {
724 #[cfg(feature = "cln")]
725 LnBackend::Cln => {
726 let cln_settings = settings.cln.clone().ok_or_else(|| {
727 anyhow!("CLN backend selected but [cln] config section is missing")
728 })?;
729 let cln = cln_settings
730 .setup(
731 settings,
732 cdk::nuts::CurrencyUnit::Msat,
733 None,
734 work_dir,
735 _kv_store.clone(),
736 )
737 .await?;
738 #[cfg(feature = "prometheus")]
739 let cln = MetricsMintPayment::new(cln);
740
741 mint_builder = configure_backend_for_unit(
742 settings,
743 mint_builder,
744 ln_entry.unit.clone(),
745 mint_melt_limits,
746 Arc::new(cln),
747 )
748 .await?;
749 }
750 #[cfg(feature = "lnbits")]
751 LnBackend::LNbits => {
752 let lnbits_settings = settings.lnbits.clone().ok_or_else(|| {
753 anyhow!("LNbits backend selected but [lnbits] config section is missing")
754 })?;
755 let lnbits = lnbits_settings
756 .setup(settings, ln_entry.unit.clone(), None, work_dir, None)
757 .await?;
758 #[cfg(feature = "prometheus")]
759 let lnbits = MetricsMintPayment::new(lnbits);
760
761 mint_builder = configure_backend_for_unit(
762 settings,
763 mint_builder,
764 ln_entry.unit.clone(),
765 mint_melt_limits,
766 Arc::new(lnbits),
767 )
768 .await?;
769 }
770 #[cfg(feature = "lnd")]
771 LnBackend::Lnd => {
772 let lnd_settings = settings.lnd.clone().ok_or_else(|| {
773 anyhow!("LND backend selected but [lnd] config section is missing")
774 })?;
775 let lnd = lnd_settings
776 .setup(
777 settings,
778 cdk::nuts::CurrencyUnit::Msat,
779 None,
780 work_dir,
781 _kv_store.clone(),
782 )
783 .await?;
784 #[cfg(feature = "prometheus")]
785 let lnd = MetricsMintPayment::new(lnd);
786
787 mint_builder = configure_backend_for_unit(
788 settings,
789 mint_builder,
790 ln_entry.unit.clone(),
791 mint_melt_limits,
792 Arc::new(lnd),
793 )
794 .await?;
795 }
796 #[cfg(feature = "fakewallet")]
797 LnBackend::FakeWallet => {
798 let fake_wallet = settings.fake_wallet.clone().ok_or_else(|| {
799 anyhow!(
800 "Fake wallet backend selected but [fake_wallet] config section is missing"
801 )
802 })?;
803 tracing::info!("Using fake wallet: {:?}", fake_wallet);
804
805 let fake = fake_wallet
806 .setup(
807 settings,
808 ln_entry.unit.clone(),
809 None,
810 work_dir,
811 _kv_store.clone(),
812 )
813 .await?;
814 #[cfg(feature = "prometheus")]
815 let fake = MetricsMintPayment::new(fake);
816
817 mint_builder = configure_backend_for_unit(
818 settings,
819 mint_builder,
820 ln_entry.unit.clone(),
821 mint_melt_limits,
822 Arc::new(fake),
823 )
824 .await?;
825
826 configure_fake_wallet_keyset_rotations = true;
827 }
828 #[cfg(feature = "grpc-processor")]
829 LnBackend::GrpcProcessor => {
830 let grpc_processor = settings.grpc_processor.clone().ok_or_else(|| {
831 anyhow!(
832 "gRPC payment processor backend selected but [grpc_processor] config section is missing"
833 )
834 })?;
835
836 tracing::info!(
837 "Attempting to start with gRPC payment processor at {}:{}.",
838 grpc_processor.addr,
839 grpc_processor.port
840 );
841
842 let processor = grpc_processor
843 .setup(settings, ln_entry.unit.clone(), None, work_dir, None)
844 .await?;
845 #[cfg(feature = "prometheus")]
846 let processor = MetricsMintPayment::new(processor);
847
848 mint_builder = configure_backend_for_unit(
849 settings,
850 mint_builder,
851 ln_entry.unit.clone(),
852 mint_melt_limits,
853 Arc::new(processor),
854 )
855 .await?;
856 }
857 #[cfg(feature = "ldk-node")]
858 LnBackend::LdkNode => {
859 let ldk_node_settings = settings.ldk_node.clone().ok_or_else(|| {
860 anyhow!("LDK Node backend selected but [ldk_node] config section is missing")
861 })?;
862 tracing::info!("Using LDK Node backend: {:?}", ldk_node_settings);
863
864 let ldk_node = ldk_node_settings
865 .setup(
866 settings,
867 ln_entry.unit.clone(),
868 _runtime.clone(),
869 work_dir,
870 None,
871 )
872 .await?;
873
874 mint_builder = configure_backend_for_unit(
875 settings,
876 mint_builder,
877 ln_entry.unit.clone(),
878 mint_melt_limits,
879 Arc::new(ldk_node),
880 )
881 .await?;
882 }
883 LnBackend::None => {
884 tracing::info!(
885 "No Lightning backend configured for unit {:?}",
886 ln_entry.unit
887 );
888 }
889 };
890 }
891
892 #[cfg(feature = "fakewallet")]
893 if configure_fake_wallet_keyset_rotations {
894 let fake_wallet = settings.fake_wallet.as_ref().ok_or_else(|| {
895 anyhow!("Fake wallet backend selected but [fake_wallet] config section is missing")
896 })?;
897 mint_builder = configure_fake_wallet_keyset_rotations_once(mint_builder, fake_wallet);
898 }
899
900 Ok(mint_builder)
901}
902
903#[cfg(feature = "fakewallet")]
904fn configure_fake_wallet_keyset_rotations_once(
905 mut mint_builder: MintBuilder,
906 fake_wallet: &config::FakeWallet,
907) -> MintBuilder {
908 for rotation_cfg in &fake_wallet.keyset_rotations {
909 use cdk::mint::KeysetRotation;
910
911 let amounts = cdk::mint::UnitConfig::default().amounts;
912 let final_expiry = if rotation_cfg.expired {
913 Some(cdk::util::unix_time().saturating_sub(3600))
914 } else {
915 None
916 };
917
918 mint_builder = mint_builder.with_keyset_rotation(KeysetRotation {
919 unit: rotation_cfg.unit.clone(),
920 amounts,
921 input_fee_ppk: rotation_cfg.input_fee_ppk,
922 use_keyset_v2: rotation_cfg.version == "v2",
923 final_expiry,
924 });
925 }
926
927 mint_builder
928}
929
930async fn configure_onchain_backend_with_wallet_info(
932 settings: &config::Settings,
933 #[cfg_attr(not(feature = "bdk"), allow(unused_mut))] mut mint_builder: MintBuilder,
934 _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
935 _work_dir: &Path,
936 _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
937) -> Result<(MintBuilder, ConfiguredWalletInfoProvider)> {
938 use config::OnchainBackend;
939 #[cfg(feature = "bdk")]
940 use setup::OnchainBackendSetup;
941
942 #[cfg(all(feature = "management-rpc", feature = "bdk"))]
943 let mut wallet_info_provider = no_wallet_info_provider();
944 #[cfg(not(all(feature = "management-rpc", feature = "bdk")))]
945 let wallet_info_provider = no_wallet_info_provider();
946
947 if let Some(onchain_settings) = &settings.onchain {
948 match onchain_settings.onchain_backend {
949 #[cfg(feature = "bdk")]
950 OnchainBackend::Bdk => {
951 let mint_melt_limits = MintMeltLimits {
952 mint_min: onchain_settings.min_mint,
953 mint_max: onchain_settings.max_mint,
954 melt_min: onchain_settings.min_melt,
955 melt_max: onchain_settings.max_melt,
956 };
957
958 let bdk_settings = settings.bdk.clone().ok_or_else(|| {
959 anyhow!("BDK onchain backend selected but [bdk] config section is missing")
960 })?;
961 let bdk = bdk_settings
962 .setup(
963 settings,
964 cdk::nuts::CurrencyUnit::Sat,
965 None,
966 _work_dir,
967 _kv_store,
968 )
969 .await?;
970 let bdk = Arc::new(bdk);
971
972 #[cfg(feature = "management-rpc")]
973 {
974 wallet_info_provider = Some(Arc::new(BdkWalletInfoProvider {
975 bdk: Arc::clone(&bdk),
976 }));
977 }
978
979 mint_builder = configure_backend_for_unit(
980 settings,
981 mint_builder,
982 cdk::nuts::CurrencyUnit::Sat,
983 mint_melt_limits,
984 bdk,
985 )
986 .await?;
987 }
988 OnchainBackend::None => {}
989 #[cfg(feature = "fakewallet")]
990 OnchainBackend::FakeWallet => {
991 let has_lightning_backend = settings
992 .ln
993 .iter()
994 .any(|ln| ln.ln_backend != LnBackend::None);
995 let has_real_ln_backend = settings
996 .ln
997 .iter()
998 .any(|ln| !matches!(ln.ln_backend, LnBackend::None | LnBackend::FakeWallet));
999
1000 if !has_lightning_backend {
1001 let mint_melt_limits = MintMeltLimits {
1002 mint_min: onchain_settings.min_mint,
1003 mint_max: onchain_settings.max_mint,
1004 melt_min: onchain_settings.min_melt,
1005 melt_max: onchain_settings.max_melt,
1006 };
1007 let fake_wallet = settings
1008 .fake_wallet
1009 .clone()
1010 .ok_or_else(|| anyhow!("Fake wallet config section is missing"))?;
1011
1012 for unit in fake_wallet.clone().supported_units {
1013 let fake = fake_wallet
1014 .setup(settings, unit.clone(), None, _work_dir, _kv_store.clone())
1015 .await?;
1016 #[cfg(feature = "prometheus")]
1017 let fake = MetricsMintPayment::new(fake);
1018
1019 mint_builder = configure_backend_for_methods(
1020 settings,
1021 mint_builder,
1022 unit,
1023 mint_melt_limits,
1024 Arc::new(fake),
1025 vec![PaymentMethod::Known(KnownMethod::Onchain)],
1026 )
1027 .await?;
1028 }
1029 } else if has_real_ln_backend {
1030 bail!(
1031 "onchain_backend = \"fakewallet\" cannot be combined with a real Lightning backend"
1032 );
1033 }
1034 }
1035 }
1036 }
1037
1038 Ok((mint_builder, wallet_info_provider))
1039}
1040
1041#[cfg(test)]
1042async fn configure_onchain_backend(
1043 settings: &config::Settings,
1044 mint_builder: MintBuilder,
1045 runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1046 work_dir: &Path,
1047 kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
1048) -> Result<MintBuilder> {
1049 Ok(configure_onchain_backend_with_wallet_info(
1050 settings,
1051 mint_builder,
1052 runtime,
1053 work_dir,
1054 kv_store,
1055 )
1056 .await?
1057 .0)
1058}
1059
1060async fn configure_backend_for_unit(
1062 settings: &config::Settings,
1063 mint_builder: MintBuilder,
1064 unit: cdk::nuts::CurrencyUnit,
1065 mint_melt_limits: MintMeltLimits,
1066 backend: Arc<dyn MintPayment<Err = cdk_common::payment::Error> + Send + Sync>,
1067) -> Result<MintBuilder> {
1068 let payment_settings = backend.get_settings().await?;
1069 validate_backend_unit(&unit, &payment_settings.unit)?;
1070
1071 let mut methods = Vec::new();
1072
1073 if payment_settings.bolt11.is_some() {
1075 methods.push(PaymentMethod::Known(KnownMethod::Bolt11));
1076 }
1077
1078 if payment_settings.bolt12.is_some() {
1080 methods.push(PaymentMethod::Known(KnownMethod::Bolt12));
1081 }
1082
1083 if payment_settings.onchain.is_some() {
1085 methods.push(PaymentMethod::Known(KnownMethod::Onchain));
1086 }
1087
1088 for method_name in payment_settings.custom.keys() {
1090 methods.push(PaymentMethod::from(method_name.as_str()));
1091 }
1092
1093 configure_backend_for_methods(
1094 settings,
1095 mint_builder,
1096 unit,
1097 mint_melt_limits,
1098 backend,
1099 methods,
1100 )
1101 .await
1102}
1103
1104async fn configure_backend_for_methods(
1105 settings: &config::Settings,
1106 mut mint_builder: MintBuilder,
1107 unit: cdk::nuts::CurrencyUnit,
1108 mint_melt_limits: MintMeltLimits,
1109 backend: Arc<dyn MintPayment<Err = cdk_common::payment::Error> + Send + Sync>,
1110 methods: Vec<PaymentMethod>,
1111) -> Result<MintBuilder> {
1112 for method in &methods {
1114 mint_builder
1115 .add_payment_processor(
1116 unit.clone(),
1117 method.clone(),
1118 mint_melt_limits,
1119 backend.clone(),
1120 )
1121 .await?;
1122 }
1123
1124 for method in &methods {
1126 let method_str = method.to_string();
1127 let nut17_supported = match method_str.as_str() {
1128 "bolt11" => SupportedMethods::default_bolt11(unit.clone()),
1129 "bolt12" => SupportedMethods::default_bolt12(unit.clone()),
1130 _ => SupportedMethods::default_custom(method.clone(), unit.clone()),
1131 };
1132 mint_builder = mint_builder.with_supported_websockets(nut17_supported);
1133 }
1134
1135 if let Some(input_fee) = settings.info.input_fee_ppk {
1136 mint_builder.set_unit_fee(&unit, input_fee)?;
1137 }
1138
1139 Ok(mint_builder)
1140}
1141
1142fn validate_backend_unit(
1143 configured_unit: &cdk::nuts::CurrencyUnit,
1144 backend_unit: &str,
1145) -> Result<()> {
1146 let backend_unit = cdk::nuts::CurrencyUnit::from_str(backend_unit)
1147 .with_context(|| format!("Payment backend returned invalid unit `{backend_unit}`"))?;
1148
1149 if units_are_compatible(&backend_unit, configured_unit) {
1150 return Ok(());
1151 }
1152
1153 bail!(
1154 "Payment backend reports unit {} but config registers unit {}; only matching units or sat/msat conversions are supported",
1155 backend_unit,
1156 configured_unit
1157 )
1158}
1159
1160fn units_are_compatible(
1161 backend_unit: &cdk::nuts::CurrencyUnit,
1162 configured_unit: &cdk::nuts::CurrencyUnit,
1163) -> bool {
1164 backend_unit == configured_unit
1165 || matches!(
1166 (backend_unit, configured_unit),
1167 (cdk::nuts::CurrencyUnit::Sat, cdk::nuts::CurrencyUnit::Msat)
1168 | (cdk::nuts::CurrencyUnit::Msat, cdk::nuts::CurrencyUnit::Sat)
1169 )
1170}
1171
1172async fn configure_cache(
1174 settings: &config::Settings,
1175 mint_builder: MintBuilder,
1176 payment_methods: &[String],
1177) -> Result<MintBuilder> {
1178 let mut cached_endpoints = vec![
1179 CachedEndpoint::new(NUT19Method::Post, NUT19Path::Swap),
1181 ];
1182
1183 for method in payment_methods {
1185 cached_endpoints.push(CachedEndpoint::new(
1187 NUT19Method::Post,
1188 NUT19Path::custom_mint(method),
1189 ));
1190 cached_endpoints.push(CachedEndpoint::new(
1191 NUT19Method::Post,
1192 NUT19Path::custom_melt(method),
1193 ));
1194 }
1195
1196 let cache: HttpCache = HttpCache::from_config(settings.info.http_cache.clone()).await?;
1197 Ok(mint_builder.with_cache(Some(cache.ttl.as_secs()), cached_endpoints))
1198}
1199
1200async fn setup_authentication(
1201 settings: &config::Settings,
1202 _work_dir: &Path,
1203 mut mint_builder: MintBuilder,
1204 _password: Option<String>,
1205) -> Result<(
1206 MintBuilder,
1207 Option<cdk_common::database::DynMintAuthDatabase>,
1208)> {
1209 if let Some(auth_settings) = settings.auth.clone() {
1210 use cdk_common::database::DynMintAuthDatabase;
1211
1212 tracing::info!("Auth settings are defined. {:?}", auth_settings);
1213 let auth_localstore: DynMintAuthDatabase = match settings.database.engine {
1214 #[cfg(feature = "sqlite")]
1215 DatabaseEngine::Sqlite => {
1216 #[cfg(feature = "sqlite")]
1217 {
1218 let sql_db_path = _work_dir.join("cdk-mintd-auth.sqlite");
1219 #[cfg(not(feature = "sqlcipher"))]
1220 let sqlite_db = MintSqliteAuthDatabase::new(&sql_db_path).await?;
1221 #[cfg(feature = "sqlcipher")]
1222 let sqlite_db = {
1223 let password = _password.clone().ok_or_else(|| {
1225 anyhow!("Password required when sqlcipher feature is enabled")
1226 })?;
1227 MintSqliteAuthDatabase::new((sql_db_path, password)).await?
1228 };
1229
1230 Arc::new(sqlite_db)
1231 }
1232 #[cfg(not(feature = "sqlite"))]
1233 {
1234 bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
1235 }
1236 }
1237 #[cfg(feature = "postgres")]
1238 DatabaseEngine::Postgres => {
1239 #[cfg(feature = "postgres")]
1240 {
1241 let auth_db_config = settings.auth_database.as_ref().ok_or_else(|| {
1243 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")
1244 })?;
1245
1246 let auth_pg_config = auth_db_config.postgres.as_ref().ok_or_else(|| {
1247 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")
1248 })?;
1249
1250 if auth_pg_config.url.is_empty() {
1251 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");
1252 }
1253
1254 let auth_db_config = PgConfig::new(
1255 auth_pg_config.url.as_str(),
1256 auth_pg_config.tls_mode.as_deref(),
1257 auth_pg_config.max_connections,
1258 auth_pg_config.connection_timeout_seconds,
1259 );
1260 Arc::new(MintPgAuthDatabase::new(auth_db_config).await?)
1261 }
1262 #[cfg(not(feature = "postgres"))]
1263 {
1264 bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
1265 }
1266 }
1267 #[cfg(not(feature = "sqlite"))]
1268 DatabaseEngine::Sqlite => {
1269 bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
1270 }
1271 #[cfg(not(feature = "postgres"))]
1272 DatabaseEngine::Postgres => {
1273 bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
1274 }
1275 };
1276
1277 let mut protected_endpoints = HashMap::new();
1278 let mut blind_auth_endpoints = vec![];
1279 let mut clear_auth_endpoints = vec![];
1280 let mut unprotected_endpoints = vec![];
1281
1282 let mint_blind_auth_endpoint =
1283 ProtectedEndpoint::new(Method::Post, RoutePath::MintBlindAuth);
1284
1285 protected_endpoints.insert(mint_blind_auth_endpoint.clone(), AuthRequired::Clear);
1286
1287 clear_auth_endpoints.push(mint_blind_auth_endpoint);
1288
1289 let mut add_endpoint = |endpoint: ProtectedEndpoint, auth_type: &AuthType| {
1291 match auth_type {
1292 AuthType::Blind => {
1293 protected_endpoints.insert(endpoint.clone(), AuthRequired::Blind);
1294 blind_auth_endpoints.push(endpoint);
1295 }
1296 AuthType::Clear => {
1297 protected_endpoints.insert(endpoint.clone(), AuthRequired::Clear);
1298 clear_auth_endpoints.push(endpoint);
1299 }
1300 AuthType::None => {
1301 unprotected_endpoints.push(endpoint);
1302 }
1303 };
1304 };
1305
1306 {
1313 let swap_protected_endpoint = ProtectedEndpoint::new(Method::Post, RoutePath::Swap);
1314 add_endpoint(swap_protected_endpoint, &auth_settings.swap);
1315 }
1316
1317 {
1319 let restore_protected_endpoint =
1320 ProtectedEndpoint::new(Method::Post, RoutePath::Restore);
1321 add_endpoint(restore_protected_endpoint, &auth_settings.restore);
1322 }
1323
1324 {
1326 let state_protected_endpoint =
1327 ProtectedEndpoint::new(Method::Post, RoutePath::Checkstate);
1328 add_endpoint(state_protected_endpoint, &auth_settings.check_proof_state);
1329 }
1330
1331 {
1333 let ws_protected_endpoint = ProtectedEndpoint::new(Method::Get, RoutePath::Ws);
1334 add_endpoint(ws_protected_endpoint, &auth_settings.websocket_auth);
1335 }
1336
1337 mint_builder = mint_builder.with_auth(
1343 auth_localstore.clone(),
1344 auth_settings.openid_discovery,
1345 auth_settings.openid_client_id,
1346 clear_auth_endpoints,
1347 );
1348 mint_builder =
1349 mint_builder.with_blind_auth(auth_settings.mint_max_bat, blind_auth_endpoints);
1350
1351 let mut tx = auth_localstore.begin_transaction().await?;
1352
1353 if !unprotected_endpoints.is_empty() {
1354 tx.remove_protected_endpoints(unprotected_endpoints).await?;
1355 }
1356 if !protected_endpoints.is_empty() {
1357 tx.add_protected_endpoints(protected_endpoints).await?;
1358 }
1359 tx.commit().await?;
1360
1361 Ok((mint_builder, Some(auth_localstore)))
1362 } else {
1363 Ok((mint_builder, None))
1364 }
1365}
1366
1367async fn build_mint(
1369 settings: &config::Settings,
1370 keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
1371 mint_builder: MintBuilder,
1372) -> Result<Mint> {
1373 if let Some(signatory_url) = settings.info.signatory_url.clone() {
1374 tracing::info!(
1375 "Connecting to remote signatory to {} with certs {:?}",
1376 signatory_url,
1377 settings.info.signatory_certs.clone()
1378 );
1379
1380 Ok(mint_builder
1381 .build_with_signatory(Arc::new(
1382 cdk_signatory::SignatoryRpcClient::new(
1383 signatory_url,
1384 settings.info.signatory_certs.clone(),
1385 )
1386 .await?,
1387 ))
1388 .await?)
1389 } else if let Some(seed) = settings.info.seed.clone() {
1390 let seed_bytes: Vec<u8> = seed.into();
1391 Ok(mint_builder.build_with_seed(keystore, &seed_bytes).await?)
1392 } else if let Some(mnemonic) = settings
1393 .info
1394 .mnemonic
1395 .clone()
1396 .map(|s| Mnemonic::from_str(&s))
1397 .transpose()?
1398 {
1399 Ok(mint_builder
1400 .build_with_seed(keystore, &mnemonic.to_seed_normalized(""))
1401 .await?)
1402 } else {
1403 bail!("No seed nor remote signatory set");
1404 }
1405}
1406
1407#[allow(clippy::too_many_arguments)]
1408async fn start_services_with_shutdown(
1409 mint: Arc<cdk::mint::Mint>,
1410 settings: &config::Settings,
1411 _work_dir: &Path,
1412 _wallet_info_provider: ConfiguredWalletInfoProvider,
1413 mint_builder_info: cdk::nuts::MintInfo,
1414 shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
1415 routers: Vec<Router>,
1416 auth_localstore: Option<cdk_common::database::DynMintAuthDatabase>,
1417) -> Result<()> {
1418 let listen_addr = settings.info.listen_host.clone();
1419 let listen_port = settings.info.listen_port;
1420 let cache: HttpCache = HttpCache::from_config(settings.info.http_cache.clone()).await?;
1421
1422 #[cfg(feature = "management-rpc")]
1423 let mut rpc_enabled = false;
1424 #[cfg(not(feature = "management-rpc"))]
1425 let rpc_enabled = false;
1426
1427 #[cfg(feature = "management-rpc")]
1428 let mut rpc_server: Option<cdk_mint_rpc::MintRPCServer> = None;
1429
1430 #[cfg(feature = "management-rpc")]
1431 {
1432 if let Some(rpc_settings) = settings.mint_management_rpc.clone() {
1433 if rpc_settings.enabled {
1434 let addr = rpc_settings.address.unwrap_or("127.0.0.1".to_string());
1435 let port = rpc_settings.port.unwrap_or(8086);
1436 let mut mint_rpc = cdk_mint_rpc::MintRPCServer::new(&addr, port, mint.clone())?;
1437
1438 #[cfg(feature = "bdk")]
1439 if let Some(provider) = _wallet_info_provider.clone() {
1440 mint_rpc = mint_rpc.with_wallet_info_provider(provider);
1441 }
1442
1443 let tls_dir = rpc_settings.tls_dir_path.unwrap_or(_work_dir.join("tls"));
1444
1445 let tls_dir = if tls_dir.exists() {
1446 Some(tls_dir)
1447 } else {
1448 tracing::warn!(
1449 "TLS directory does not exist: {}. Starting RPC server in INSECURE mode without TLS encryption",
1450 tls_dir.display()
1451 );
1452 None
1453 };
1454
1455 mint_rpc.start(tls_dir).await?;
1456
1457 rpc_server = Some(mint_rpc);
1458
1459 rpc_enabled = true;
1460 }
1461 }
1462 }
1463
1464 let desired_quote_ttl: QuoteTTL = settings.info.quote_ttl.unwrap_or_default();
1466
1467 if rpc_enabled {
1468 if mint.mint_info().await.is_err() {
1469 tracing::info!("Mint info not set on mint, setting.");
1470 mint.set_mint_info(mint_builder_info).await?;
1472 mint.set_quote_ttl(desired_quote_ttl).await?;
1473 } else {
1474 if !mint.quote_ttl_is_persisted().await? {
1476 mint.set_quote_ttl(desired_quote_ttl).await?;
1477 }
1478 let mint_version = MintVersion::new(
1480 "cdk-mintd".to_string(),
1481 CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
1482 );
1483 let mut stored_mint_info = mint.mint_info().await?;
1484 stored_mint_info.version = Some(mint_version);
1485 mint.set_mint_info(stored_mint_info).await?;
1486
1487 tracing::info!("Mint info already set, not using config file settings.");
1488 }
1489 } else {
1490 tracing::info!("RPC not enabled, using mint info and quote TTL from config.");
1492 let mut mint_builder_info = mint_builder_info;
1493
1494 if let Ok(mint_info) = mint.mint_info().await {
1495 if mint_builder_info.pubkey.is_none() {
1496 mint_builder_info.pubkey = mint_info.pubkey;
1497 }
1498 }
1499
1500 mint.set_mint_info(mint_builder_info).await?;
1501 mint.set_quote_ttl(desired_quote_ttl).await?;
1502 }
1503
1504 let mint_info = mint.mint_info().await?;
1505 let nut04_methods = mint_info.nuts.nut04.supported_methods();
1506 let nut05_methods = mint_info.nuts.nut05.supported_methods();
1507
1508 let mut custom_methods = mint.get_custom_payment_methods().await?;
1510
1511 let bolt11_method = PaymentMethod::Known(KnownMethod::Bolt11);
1513 let bolt11_supported =
1514 nut04_methods.contains(&&bolt11_method) || nut05_methods.contains(&&bolt11_method);
1515 let bolt12_method = PaymentMethod::Known(KnownMethod::Bolt12);
1517 let bolt12_supported =
1518 nut04_methods.contains(&&bolt12_method) || nut05_methods.contains(&&bolt12_method);
1519
1520 let onchain_method = PaymentMethod::Known(KnownMethod::Onchain);
1522 let onchain_supported =
1523 nut04_methods.contains(&&onchain_method) || nut05_methods.contains(&&onchain_method);
1524
1525 if bolt11_supported
1526 && !custom_methods.contains(&PaymentMethod::Known(KnownMethod::Bolt11).to_string())
1527 {
1528 custom_methods.push(PaymentMethod::Known(KnownMethod::Bolt11).to_string());
1529 }
1530 if bolt12_supported
1531 && !custom_methods.contains(&PaymentMethod::Known(KnownMethod::Bolt12).to_string())
1532 {
1533 custom_methods.push(PaymentMethod::Known(KnownMethod::Bolt12).to_string());
1534 }
1535 if onchain_supported
1536 && !custom_methods.contains(&PaymentMethod::Known(KnownMethod::Onchain).to_string())
1537 {
1538 custom_methods.push(PaymentMethod::Known(KnownMethod::Onchain).to_string());
1539 }
1540
1541 tracing::info!("Payment methods: {:?}", custom_methods);
1542
1543 if let (Some(ref auth_settings), Some(auth_db)) = (&settings.auth, &auth_localstore) {
1545 if auth_settings.auth_enabled {
1546 use std::collections::HashMap;
1547
1548 use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
1549 use cdk::nuts::AuthRequired;
1550
1551 use crate::config::AuthType;
1552
1553 let existing_endpoints = auth_db.get_auth_for_endpoints().await?;
1556 let payment_method_endpoints_to_remove: Vec<ProtectedEndpoint> = existing_endpoints
1557 .keys()
1558 .filter(|endpoint| {
1559 matches!(
1560 endpoint.path,
1561 RoutePath::MintQuote(_)
1562 | RoutePath::Mint(_)
1563 | RoutePath::MeltQuote(_)
1564 | RoutePath::Melt(_)
1565 )
1566 })
1567 .cloned()
1568 .collect();
1569
1570 if !payment_method_endpoints_to_remove.is_empty() {
1571 tracing::debug!(
1572 "Removing {} old payment method endpoints from database",
1573 payment_method_endpoints_to_remove.len()
1574 );
1575 let mut tx = auth_db.begin_transaction().await?;
1576 tx.remove_protected_endpoints(payment_method_endpoints_to_remove)
1577 .await?;
1578 tx.commit().await?;
1579 }
1580
1581 if !custom_methods.is_empty() {
1583 let mut protected_endpoints = HashMap::new();
1584
1585 for method_name in &custom_methods {
1586 tracing::debug!("Adding auth endpoints for payment method: {}", method_name);
1587
1588 let mint_quote_auth = match auth_settings.get_mint_quote {
1590 AuthType::Clear => Some(AuthRequired::Clear),
1591 AuthType::Blind => Some(AuthRequired::Blind),
1592 AuthType::None => None,
1593 };
1594
1595 let check_mint_quote_auth = match auth_settings.check_mint_quote {
1596 AuthType::Clear => Some(AuthRequired::Clear),
1597 AuthType::Blind => Some(AuthRequired::Blind),
1598 AuthType::None => None,
1599 };
1600
1601 let mint_auth = match auth_settings.mint {
1602 AuthType::Clear => Some(AuthRequired::Clear),
1603 AuthType::Blind => Some(AuthRequired::Blind),
1604 AuthType::None => None,
1605 };
1606
1607 let melt_quote_auth = match auth_settings.get_melt_quote {
1608 AuthType::Clear => Some(AuthRequired::Clear),
1609 AuthType::Blind => Some(AuthRequired::Blind),
1610 AuthType::None => None,
1611 };
1612
1613 let check_melt_quote_auth = match auth_settings.check_melt_quote {
1614 AuthType::Clear => Some(AuthRequired::Clear),
1615 AuthType::Blind => Some(AuthRequired::Blind),
1616 AuthType::None => None,
1617 };
1618
1619 let melt_auth = match auth_settings.melt {
1620 AuthType::Clear => Some(AuthRequired::Clear),
1621 AuthType::Blind => Some(AuthRequired::Blind),
1622 AuthType::None => None,
1623 };
1624
1625 if let Some(auth) = mint_quote_auth {
1627 protected_endpoints.insert(
1628 ProtectedEndpoint::new(
1629 Method::Post,
1630 RoutePath::MintQuote(method_name.clone()),
1631 ),
1632 auth,
1633 );
1634 }
1635 if let Some(auth) = check_mint_quote_auth {
1636 protected_endpoints.insert(
1637 ProtectedEndpoint::new(
1638 Method::Get,
1639 RoutePath::MintQuote(method_name.clone()),
1640 ),
1641 auth,
1642 );
1643 }
1644 if let Some(auth) = mint_auth {
1645 protected_endpoints.insert(
1646 ProtectedEndpoint::new(
1647 Method::Post,
1648 RoutePath::Mint(method_name.clone()),
1649 ),
1650 auth,
1651 );
1652 }
1653 if let Some(auth) = melt_quote_auth {
1654 protected_endpoints.insert(
1655 ProtectedEndpoint::new(
1656 Method::Post,
1657 RoutePath::MeltQuote(method_name.clone()),
1658 ),
1659 auth,
1660 );
1661 }
1662 if let Some(auth) = check_melt_quote_auth {
1663 protected_endpoints.insert(
1664 ProtectedEndpoint::new(
1665 Method::Get,
1666 RoutePath::MeltQuote(method_name.clone()),
1667 ),
1668 auth,
1669 );
1670 }
1671 if let Some(auth) = melt_auth {
1672 protected_endpoints.insert(
1673 ProtectedEndpoint::new(
1674 Method::Post,
1675 RoutePath::Melt(method_name.clone()),
1676 ),
1677 auth,
1678 );
1679 }
1680 }
1681
1682 if !protected_endpoints.is_empty() {
1684 let mut tx = auth_db.begin_transaction().await?;
1685 tx.add_protected_endpoints(protected_endpoints).await?;
1686 tx.commit().await?;
1687 }
1688 }
1689 }
1690 }
1691
1692 let v1_service = cdk_axum::create_mint_router_with_custom_cache(
1693 Arc::clone(&mint),
1694 cache,
1695 custom_methods,
1696 settings.info.enable_info_page.unwrap_or(true),
1697 )
1698 .await?;
1699
1700 let mut mint_service = Router::new()
1701 .merge(v1_service)
1702 .layer(DefaultBodyLimit::max(REQUEST_BODY_LIMIT_BYTES))
1703 .layer(
1704 ServiceBuilder::new()
1705 .layer(RequestDecompressionLayer::new())
1706 .layer(CompressionLayer::new()),
1707 )
1708 .layer(TraceLayer::new_for_http());
1709
1710 for router in routers {
1711 mint_service = mint_service.merge(router);
1712 }
1713
1714 let (shutdown_tx, _) = tokio::sync::broadcast::channel::<()>(1);
1716
1717 #[cfg(feature = "prometheus")]
1719 let prometheus_handle = {
1720 if let Some(prometheus_settings) = &settings.prometheus {
1721 if prometheus_settings.enabled {
1722 let addr = prometheus_settings
1723 .address
1724 .clone()
1725 .unwrap_or("127.0.0.1".to_string());
1726 let port = prometheus_settings.port.unwrap_or(9000);
1727
1728 let address = format!("{}:{}", addr, port)
1729 .parse()
1730 .expect("Invalid prometheus address");
1731
1732 let server = cdk_prometheus::PrometheusBuilder::new()
1733 .bind_address(address)
1734 .build_with_cdk_metrics()?;
1735
1736 let mut shutdown_rx = shutdown_tx.subscribe();
1737 let prometheus_shutdown = async move {
1738 let _ = shutdown_rx.recv().await;
1739 };
1740
1741 Some(tokio::spawn(async move {
1742 if let Err(e) = server.start(prometheus_shutdown).await {
1743 tracing::error!("Failed to start prometheus server: {}", e);
1744 }
1745 }))
1746 } else {
1747 None
1748 }
1749 } else {
1750 None
1751 }
1752 };
1753
1754 mint.start().await?;
1755
1756 let socket_addr = SocketAddr::from_str(&format!("{listen_addr}:{listen_port}"))?;
1757
1758 let listener = tokio::net::TcpListener::bind(socket_addr).await?;
1759
1760 tracing::info!("listening on {}", listener.local_addr()?);
1761
1762 let shutdown_broadcast_task = {
1764 let shutdown_tx = shutdown_tx.clone();
1765 tokio::spawn(async move {
1766 shutdown_signal.await;
1767 tracing::info!("Shutdown signal received, broadcasting to all services");
1768 let _ = shutdown_tx.send(());
1769 })
1770 };
1771
1772 let mut axum_shutdown_rx = shutdown_tx.subscribe();
1774 let axum_shutdown = async move {
1775 let _ = axum_shutdown_rx.recv().await;
1776 };
1777
1778 let axum_result = axum::serve(listener, mint_service).with_graceful_shutdown(axum_shutdown);
1780
1781 match axum_result.await {
1782 Ok(_) => {
1783 tracing::info!("Axum server stopped with okay status");
1784 }
1785 Err(err) => {
1786 tracing::warn!("Axum server stopped with error");
1787 tracing::error!("{}", err);
1788 bail!("Axum exited with error")
1789 }
1790 }
1791
1792 let _ = shutdown_broadcast_task.await;
1794
1795 #[cfg(feature = "prometheus")]
1797 if let Some(handle) = prometheus_handle {
1798 if let Err(e) = handle.await {
1799 tracing::warn!("Prometheus server task failed: {}", e);
1800 }
1801 }
1802
1803 mint.stop().await?;
1804
1805 #[cfg(feature = "management-rpc")]
1806 {
1807 if let Some(rpc_server) = rpc_server {
1808 rpc_server.stop().await?;
1809 }
1810 }
1811
1812 Ok(())
1813}
1814
1815async fn shutdown_signal() {
1816 tokio::signal::ctrl_c()
1817 .await
1818 .expect("failed to install CTRL+C handler");
1819 tracing::info!("Shutdown signal received");
1820}
1821
1822fn work_dir() -> Result<PathBuf> {
1823 let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?;
1824 let dir = home_dir.join(".cdk-mintd");
1825
1826 std::fs::create_dir_all(&dir)?;
1827
1828 Ok(dir)
1829}
1830
1831pub async fn run_mintd(
1833 work_dir: &Path,
1834 settings: &config::Settings,
1835 db_password: Option<String>,
1836 enable_logging: bool,
1837 runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1838 routers: Vec<Router>,
1839) -> Result<()> {
1840 let _guard = if enable_logging {
1841 setup_tracing(work_dir, &settings.info.logging)?
1842 } else {
1843 None
1844 };
1845
1846 let result = run_mintd_with_shutdown(
1847 work_dir,
1848 settings,
1849 shutdown_signal(),
1850 db_password,
1851 runtime,
1852 routers,
1853 )
1854 .await;
1855
1856 if let Some(guard) = _guard {
1858 tracing::info!("Shutting down logging worker thread");
1859 drop(guard);
1860 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
1862 }
1863
1864 tracing::info!("Mintd shutdown");
1865
1866 result
1867}
1868
1869pub async fn run_mintd_with_shutdown(
1871 work_dir: &Path,
1872 settings: &config::Settings,
1873 shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
1874 db_password: Option<String>,
1875 runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
1876 routers: Vec<Router>,
1877) -> Result<()> {
1878 let (localstore, keystore, kv) = initial_setup(work_dir, settings, db_password.clone()).await?;
1879
1880 let mint_builder = MintBuilder::new(localstore);
1881
1882 let maybe_mint_builder = {
1885 #[cfg(feature = "management-rpc")]
1886 {
1887 if let Some(rpc_settings) = settings.mint_management_rpc.clone() {
1888 if rpc_settings.enabled {
1889 let mut tmp = mint_builder;
1891 if let Err(e) = tmp.init_from_db_if_present().await {
1892 tracing::warn!("Failed to init builder from DB: {}", e);
1893 }
1894 tmp
1895 } else {
1896 mint_builder
1897 }
1898 } else {
1899 mint_builder
1900 }
1901 }
1902 #[cfg(not(feature = "management-rpc"))]
1903 {
1904 mint_builder
1905 }
1906 };
1907
1908 let (mint_builder, wallet_info_provider) = configure_mint_builder_with_wallet_info(
1909 settings,
1910 maybe_mint_builder,
1911 runtime,
1912 work_dir,
1913 Some(kv),
1914 )
1915 .await?;
1916 let (mint_builder, auth_localstore) =
1917 setup_authentication(settings, work_dir, mint_builder, db_password).await?;
1918
1919 let config_mint_info = mint_builder.current_mint_info();
1920
1921 let mint = build_mint(settings, keystore, mint_builder).await?;
1922
1923 tracing::debug!("Mint built from builder.");
1924
1925 let mint = Arc::new(mint);
1926
1927 start_services_with_shutdown(
1928 mint.clone(),
1929 settings,
1930 work_dir,
1931 wallet_info_provider,
1932 config_mint_info,
1933 shutdown_signal,
1934 routers,
1935 auth_localstore,
1936 )
1937 .await
1938}
1939
1940#[cfg(test)]
1941mod tests {
1942 use std::fs;
1943
1944 use cdk::nuts::{CurrencyUnit, MintMethodSettings, PaymentMethod};
1945
1946 use super::*;
1947
1948 const TEST_MNEMONIC: &str =
1949 "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
1950
1951 fn temp_seed_file(name: &str) -> PathBuf {
1952 std::env::temp_dir().join(format!("cdk_mintd_{name}_{}", std::process::id()))
1953 }
1954
1955 #[test]
1956 fn apply_seed_file_sets_mint_mnemonic_from_trimmed_file_contents() {
1957 let seed_file = temp_seed_file("seed_file_sets_seed");
1958 fs::write(&seed_file, format!(" {TEST_MNEMONIC}\n")).expect("seed file should be written");
1959 let mut settings = config::Settings {
1960 info: config::Info {
1961 seed: Some("raw seed from config".to_string()),
1962 mnemonic: Some("mnemonic from config".to_string()),
1963 signatory_url: Some("http://127.0.0.1:50051".to_string()),
1964 signatory_certs: Some("/tmp/certs".to_string()),
1965 ..Default::default()
1966 },
1967 ..Default::default()
1968 };
1969
1970 apply_seed_file(&mut settings, &seed_file).expect("seed file should be applied");
1971
1972 assert_eq!(settings.info.seed, None);
1973 assert_eq!(settings.info.mnemonic, Some(TEST_MNEMONIC.to_string()));
1974 assert_eq!(
1975 settings.info.signatory_url,
1976 Some("http://127.0.0.1:50051".to_string())
1977 );
1978 assert_eq!(
1979 settings.info.signatory_certs,
1980 Some("/tmp/certs".to_string())
1981 );
1982
1983 let _ = fs::remove_file(&seed_file);
1984 }
1985
1986 #[cfg(feature = "bdk")]
1987 #[test]
1988 fn apply_seed_file_sets_active_bdk_mnemonic() {
1989 use crate::config::{Bdk, Onchain, OnchainBackend};
1990
1991 let seed_file = temp_seed_file("seed_file_sets_bdk_seed");
1992 fs::write(&seed_file, TEST_MNEMONIC).expect("seed file should be written");
1993 let mut settings = config::Settings {
1994 onchain: Some(Onchain {
1995 onchain_backend: OnchainBackend::Bdk,
1996 ..Default::default()
1997 }),
1998 bdk: Some(Bdk {
1999 mnemonic: Some("old bdk mnemonic".to_string()),
2000 ..Default::default()
2001 }),
2002 ..Default::default()
2003 };
2004
2005 apply_seed_file(&mut settings, &seed_file).expect("seed file should be applied");
2006
2007 assert_eq!(
2008 settings
2009 .bdk
2010 .expect("bdk settings should be present")
2011 .mnemonic,
2012 Some(TEST_MNEMONIC.to_string())
2013 );
2014
2015 let _ = fs::remove_file(&seed_file);
2016 }
2017
2018 #[cfg(feature = "ldk-node")]
2019 #[test]
2020 fn apply_seed_file_sets_active_ldk_node_mnemonic() {
2021 use crate::config::{LdkNode, Ln, LnBackend};
2022
2023 let seed_file = temp_seed_file("seed_file_sets_ldk_seed");
2024 fs::write(&seed_file, TEST_MNEMONIC).expect("seed file should be written");
2025 let mut settings = config::Settings {
2026 ln: vec![Ln {
2027 ln_backend: LnBackend::LdkNode,
2028 ..Default::default()
2029 }],
2030 ldk_node: Some(LdkNode {
2031 ldk_node_mnemonic: Some("old ldk mnemonic".to_string()),
2032 ..Default::default()
2033 }),
2034 ..Default::default()
2035 };
2036
2037 apply_seed_file(&mut settings, &seed_file).expect("seed file should be applied");
2038
2039 assert_eq!(
2040 settings
2041 .ldk_node
2042 .expect("ldk node settings should be present")
2043 .ldk_node_mnemonic,
2044 Some(TEST_MNEMONIC.to_string())
2045 );
2046
2047 let _ = fs::remove_file(&seed_file);
2048 }
2049
2050 #[test]
2051 fn apply_seed_file_rejects_empty_seed_file() {
2052 let seed_file = temp_seed_file("empty_seed_file");
2053 fs::write(&seed_file, "\n\t ").expect("seed file should be written");
2054 let mut settings = config::Settings::default();
2055
2056 let err = apply_seed_file(&mut settings, &seed_file)
2057 .expect_err("empty seed file should be rejected");
2058
2059 assert!(err.to_string().contains("is empty"));
2060 assert_eq!(settings.info.seed, None);
2061
2062 let _ = fs::remove_file(&seed_file);
2063 }
2064
2065 #[test]
2066 fn apply_seed_file_rejects_invalid_seed_phrase() {
2067 let seed_file = temp_seed_file("invalid_seed_file");
2068 fs::write(&seed_file, "not a valid seed phrase").expect("seed file should be written");
2069 let mut settings = config::Settings::default();
2070
2071 let err = apply_seed_file(&mut settings, &seed_file)
2072 .expect_err("invalid seed phrase should be rejected");
2073
2074 assert!(err.to_string().contains("Invalid seed phrase"));
2075 assert_eq!(settings.info.mnemonic, None);
2076
2077 let _ = fs::remove_file(&seed_file);
2078 }
2079
2080 #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
2081 #[tokio::test]
2082 async fn fakewallet_dispatcher_uses_ln_entry_unit() {
2083 use cdk::mint::MintBuilder;
2084 use cdk_sqlite::mint::memory;
2085
2086 use crate::config::{FakeWallet, Ln, LnBackend};
2087
2088 let settings = config::Settings {
2089 ln: vec![Ln {
2090 ln_backend: LnBackend::FakeWallet,
2091 unit: CurrencyUnit::Eur,
2092 ..Default::default()
2093 }],
2094 fake_wallet: Some(FakeWallet::default()),
2095 ..Default::default()
2096 };
2097
2098 let localstore = Arc::new(memory::empty().await.unwrap());
2099 let builder = MintBuilder::new(localstore);
2100 let builder =
2101 configure_lightning_backend(&settings, builder, None, &std::env::temp_dir(), None)
2102 .await
2103 .expect("dispatcher should succeed");
2104
2105 let mint_info = builder.current_mint_info();
2106 let units: Vec<_> = mint_info
2107 .nuts
2108 .nut04
2109 .methods
2110 .iter()
2111 .map(|m| m.unit.clone())
2112 .collect();
2113 assert!(
2114 units.contains(&CurrencyUnit::Eur),
2115 "expected Eur, got {units:?}"
2116 );
2117 assert!(
2118 !units.contains(&CurrencyUnit::Sat),
2119 "Sat would only appear if supported_units leaked through; got {units:?}"
2120 );
2121 }
2122
2123 #[test]
2124 fn backend_unit_validation_allows_matching_units() {
2125 validate_backend_unit(&CurrencyUnit::Eur, "EUR").expect("matching units should pass");
2126 }
2127
2128 #[test]
2129 fn backend_unit_validation_allows_sat_msat_pair() {
2130 validate_backend_unit(&CurrencyUnit::Sat, "MSAT")
2131 .expect("sat/msat compatible units should pass");
2132 validate_backend_unit(&CurrencyUnit::Msat, "SAT")
2133 .expect("msat/sat compatible units should pass");
2134 }
2135
2136 #[test]
2137 fn backend_unit_validation_rejects_unsupported_conversion() {
2138 let err = validate_backend_unit(&CurrencyUnit::Eur, "SAT")
2139 .expect_err("sat backend should not advertise eur");
2140
2141 assert!(
2142 err.to_string().contains("only matching units"),
2143 "error should explain the supported conversions: {err}"
2144 );
2145 }
2146
2147 #[cfg(feature = "cln")]
2148 #[test]
2149 fn expand_path_expands_bare_tilde_without_panic() {
2150 let expanded = expand_path("~");
2151
2152 assert_eq!(expanded, home::home_dir());
2153 }
2154
2155 #[cfg(feature = "cln")]
2156 #[test]
2157 fn expand_path_keeps_named_tilde_paths_literal() {
2158 let expanded = expand_path("~foo").expect("path should be returned");
2159
2160 assert_eq!(expanded, PathBuf::from("~foo"));
2161 }
2162
2163 #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
2164 #[tokio::test]
2165 async fn duplicate_ln_unit_method_pair_is_rejected() {
2166 use cdk::mint::MintBuilder;
2167 use cdk_sqlite::mint::memory;
2168
2169 use crate::config::{FakeWallet, Ln, LnBackend};
2170
2171 let settings = config::Settings {
2172 ln: vec![
2173 Ln {
2174 ln_backend: LnBackend::FakeWallet,
2175 unit: CurrencyUnit::Sat,
2176 ..Default::default()
2177 },
2178 Ln {
2179 ln_backend: LnBackend::FakeWallet,
2180 unit: CurrencyUnit::Sat,
2181 ..Default::default()
2182 },
2183 ],
2184 fake_wallet: Some(FakeWallet::default()),
2185 ..Default::default()
2186 };
2187
2188 let localstore = Arc::new(memory::empty().await.unwrap());
2189 let builder = MintBuilder::new(localstore);
2190 let err =
2191 configure_lightning_backend(&settings, builder, None, &std::env::temp_dir(), None)
2192 .await
2193 .expect_err("duplicate unit/method pair should be rejected");
2194
2195 assert!(err.to_string().contains("Duplicate payment processor"));
2196 }
2197
2198 #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
2199 #[tokio::test]
2200 async fn empty_ln_vec_returns_unchanged_builder() {
2201 use cdk::mint::MintBuilder;
2202 use cdk_sqlite::mint::memory;
2203
2204 let settings = config::Settings {
2205 ln: vec![],
2206 ..Default::default()
2207 };
2208
2209 let localstore = Arc::new(memory::empty().await.unwrap());
2210 let builder = MintBuilder::new(localstore);
2211 let builder =
2212 configure_lightning_backend(&settings, builder, None, &std::env::temp_dir(), None)
2213 .await
2214 .expect("empty ln should succeed");
2215
2216 let mint_info = builder.current_mint_info();
2217 assert!(
2218 mint_info.nuts.nut04.methods.is_empty(),
2219 "no backends should be registered"
2220 );
2221 }
2222
2223 #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
2224 #[tokio::test]
2225 async fn ln_backend_none_logs_and_continues() {
2226 use cdk::mint::MintBuilder;
2227 use cdk_sqlite::mint::memory;
2228
2229 use crate::config::{Ln, LnBackend};
2230
2231 let settings = config::Settings {
2232 ln: vec![Ln {
2233 ln_backend: LnBackend::None,
2234 unit: CurrencyUnit::Sat,
2235 ..Default::default()
2236 }],
2237 ..Default::default()
2238 };
2239
2240 let localstore = Arc::new(memory::empty().await.unwrap());
2241 let builder = MintBuilder::new(localstore);
2242 let builder =
2243 configure_lightning_backend(&settings, builder, None, &std::env::temp_dir(), None)
2244 .await
2245 .expect("LnBackend::None should succeed");
2246
2247 let mint_info = builder.current_mint_info();
2248 assert!(
2249 mint_info.nuts.nut04.methods.is_empty(),
2250 "LnBackend::None should not register any methods"
2251 );
2252 }
2253
2254 #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
2255 #[tokio::test]
2256 async fn onchain_backend_none_returns_unchanged() {
2257 use cdk::mint::MintBuilder;
2258 use cdk_sqlite::mint::memory;
2259
2260 use crate::config::{Onchain, OnchainBackend};
2261
2262 let settings = config::Settings {
2263 onchain: Some(Onchain {
2264 onchain_backend: OnchainBackend::None,
2265 ..Default::default()
2266 }),
2267 ..Default::default()
2268 };
2269
2270 let localstore = Arc::new(memory::empty().await.unwrap());
2271 let builder = MintBuilder::new(localstore);
2272 let builder =
2273 configure_onchain_backend(&settings, builder, None, &std::env::temp_dir(), None)
2274 .await
2275 .expect("OnchainBackend::None should succeed");
2276
2277 let mint_info = builder.current_mint_info();
2278 assert!(
2279 mint_info.nuts.nut04.methods.is_empty(),
2280 "OnchainBackend::None should not register any methods"
2281 );
2282 }
2283
2284 #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
2285 #[tokio::test]
2286 async fn fakewallet_onchain_no_lightning_configures_onchain_methods() {
2287 use cdk::mint::MintBuilder;
2288 use cdk_sqlite::mint::memory;
2289
2290 use crate::config::{FakeWallet, Ln, LnBackend, Onchain, OnchainBackend};
2291
2292 let settings = config::Settings {
2293 ln: vec![Ln {
2294 ln_backend: LnBackend::None,
2295 ..Default::default()
2296 }],
2297 onchain: Some(Onchain {
2298 onchain_backend: OnchainBackend::FakeWallet,
2299 ..Default::default()
2300 }),
2301 fake_wallet: Some(FakeWallet::default()),
2302 ..Default::default()
2303 };
2304
2305 let localstore = Arc::new(memory::empty().await.unwrap());
2306 let builder = MintBuilder::new(localstore);
2307 let builder =
2308 configure_onchain_backend(&settings, builder, None, &std::env::temp_dir(), None)
2309 .await
2310 .expect("fakewallet onchain should succeed");
2311
2312 let mint_info = builder.current_mint_info();
2313 let methods: Vec<_> = mint_info
2314 .nuts
2315 .nut04
2316 .methods
2317 .iter()
2318 .map(|m| m.method.clone())
2319 .collect();
2320 assert!(
2321 methods.contains(&PaymentMethod::Known(KnownMethod::Onchain)),
2322 "expected onchain method, got {methods:?}"
2323 );
2324 }
2325
2326 #[cfg(all(feature = "fakewallet", feature = "cln", feature = "sqlite"))]
2327 #[tokio::test]
2328 async fn fakewallet_onchain_with_real_ln_bails() {
2329 use cdk::mint::MintBuilder;
2330 use cdk_sqlite::mint::memory;
2331
2332 use crate::config::{Ln, LnBackend, Onchain, OnchainBackend};
2333
2334 let settings = config::Settings {
2335 ln: vec![Ln {
2336 ln_backend: LnBackend::Cln,
2337 unit: CurrencyUnit::Sat,
2338 ..Default::default()
2339 }],
2340 onchain: Some(Onchain {
2341 onchain_backend: OnchainBackend::FakeWallet,
2342 ..Default::default()
2343 }),
2344 ..Default::default()
2345 };
2346
2347 let localstore = Arc::new(memory::empty().await.unwrap());
2348 let builder = MintBuilder::new(localstore);
2349 let err = configure_onchain_backend(&settings, builder, None, &std::env::temp_dir(), None)
2350 .await
2351 .expect_err("fakewallet onchain with real LN should bail");
2352
2353 assert!(
2354 err.to_string().contains("fakewallet"),
2355 "error should mention fakewallet: {err}"
2356 );
2357 }
2358
2359 #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
2360 #[tokio::test]
2361 async fn configure_mint_builder_no_backends_bails() {
2362 use cdk::mint::MintBuilder;
2363 use cdk_sqlite::mint::memory;
2364
2365 use crate::config::{Ln, LnBackend};
2366
2367 let settings = config::Settings {
2368 ln: vec![Ln {
2369 ln_backend: LnBackend::None,
2370 ..Default::default()
2371 }],
2372 ..Default::default()
2373 };
2374
2375 let localstore = Arc::new(memory::empty().await.unwrap());
2376 let builder = MintBuilder::new(localstore);
2377 let err = configure_mint_builder(&settings, builder, None, &std::env::temp_dir(), None)
2378 .await
2379 .expect_err("no payment backends should bail");
2380
2381 assert!(
2382 err.to_string().contains("At least one payment backend"),
2383 "error should mention missing backends: {err}"
2384 );
2385 }
2386
2387 #[cfg(all(feature = "fakewallet", feature = "sqlite", feature = "bdk"))]
2388 #[tokio::test]
2389 async fn configure_mint_builder_fake_wallet_with_bdk_onchain_bails() {
2390 use cdk::mint::MintBuilder;
2391 use cdk_sqlite::mint::memory;
2392
2393 use crate::config::{Bdk, FakeWallet, Ln, LnBackend, Onchain, OnchainBackend};
2394
2395 let settings = config::Settings {
2396 ln: vec![Ln {
2397 ln_backend: LnBackend::FakeWallet,
2398 ..Default::default()
2399 }],
2400 onchain: Some(Onchain {
2401 onchain_backend: OnchainBackend::Bdk,
2402 ..Default::default()
2403 }),
2404 fake_wallet: Some(FakeWallet::default()),
2405 bdk: Some(Bdk {
2406 network: Some("mainnet".to_string()),
2407 ..Default::default()
2408 }),
2409 ..Default::default()
2410 };
2411
2412 let localstore = Arc::new(memory::empty().await.unwrap());
2413 let builder = MintBuilder::new(localstore);
2414 let err = configure_mint_builder(&settings, builder, None, &std::env::temp_dir(), None)
2415 .await
2416 .expect_err("fake wallet with BDK onchain should bail");
2417
2418 assert!(
2419 err.to_string().contains("fakewallet") && err.to_string().contains("bdk"),
2420 "error should mention backend pairing validation: {err}"
2421 );
2422 }
2423
2424 #[cfg(all(feature = "management-rpc", feature = "bdk", feature = "sqlite"))]
2425 #[tokio::test]
2426 async fn bdk_onchain_exposes_wallet_info_provider() {
2427 use cdk::mint::MintBuilder;
2428 use cdk_sqlite::mint::memory;
2429
2430 use crate::config::{Bdk, Onchain, OnchainBackend};
2431
2432 let work_dir = temp_seed_file("wallet_info_provider");
2433 let settings = config::Settings {
2434 onchain: Some(Onchain {
2435 onchain_backend: OnchainBackend::Bdk,
2436 ..Default::default()
2437 }),
2438 bdk: Some(Bdk {
2439 network: Some("regtest".to_string()),
2440 chain_source_type: Some("esplora".to_string()),
2441 esplora_url: Some("http://127.0.0.1:1".to_string()),
2442 mnemonic: Some(
2443 "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
2444 .to_string(),
2445 ),
2446 ..Default::default()
2447 }),
2448 ..Default::default()
2449 };
2450
2451 let localstore = Arc::new(memory::empty().await.expect("in-memory database"));
2452 let builder = MintBuilder::new(localstore.clone());
2453 let (builder, provider) = configure_onchain_backend_with_wallet_info(
2454 &settings,
2455 builder,
2456 None,
2457 &work_dir,
2458 Some(localstore),
2459 )
2460 .await
2461 .expect("configure BDK backend");
2462
2463 assert!(builder
2464 .current_mint_info()
2465 .nuts
2466 .nut04
2467 .methods
2468 .iter()
2469 .any(|method| method.method == PaymentMethod::Known(KnownMethod::Onchain)));
2470
2471 let balance = provider
2472 .expect("wallet info provider")
2473 .get_balance()
2474 .await
2475 .expect("get wallet balance");
2476 assert_eq!(balance.network, "regtest");
2477 assert_eq!(balance.total_sat, 0);
2478
2479 drop(builder);
2480 let _ = std::fs::remove_dir_all(work_dir);
2481 }
2482
2483 #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
2484 #[tokio::test]
2485 async fn configure_backend_for_methods_registers_websockets_and_fee() {
2486 use cdk::mint::MintBuilder;
2487 use cdk_sqlite::mint::memory;
2488
2489 use crate::config::{FakeWallet, Ln, LnBackend};
2490
2491 let settings = config::Settings {
2492 ln: vec![Ln {
2493 ln_backend: LnBackend::FakeWallet,
2494 unit: CurrencyUnit::Sat,
2495 ..Default::default()
2496 }],
2497 fake_wallet: Some(FakeWallet::default()),
2498 info: config::Info {
2499 input_fee_ppk: Some(100),
2500 ..Default::default()
2501 },
2502 ..Default::default()
2503 };
2504
2505 let localstore = Arc::new(memory::empty().await.unwrap());
2506 let builder = MintBuilder::new(localstore);
2507
2508 let fake_wallet = settings.fake_wallet.clone().expect("fake wallet config");
2509 let fake = fake_wallet
2510 .setup(
2511 &settings,
2512 CurrencyUnit::Sat,
2513 None,
2514 &std::env::temp_dir(),
2515 None,
2516 )
2517 .await
2518 .expect("fake wallet setup");
2519
2520 let mint_melt_limits = cdk::mint::MintMeltLimits {
2521 mint_min: 1.into(),
2522 mint_max: 500_000.into(),
2523 melt_min: 1.into(),
2524 melt_max: 500_000.into(),
2525 };
2526
2527 let builder = configure_backend_for_methods(
2528 &settings,
2529 builder,
2530 CurrencyUnit::Sat,
2531 mint_melt_limits,
2532 Arc::new(fake),
2533 vec![PaymentMethod::Known(KnownMethod::Bolt11)],
2534 )
2535 .await
2536 .expect("configure_backend_for_methods should succeed");
2537
2538 let mint_info = builder.current_mint_info();
2539 assert!(
2540 !mint_info.nuts.nut04.methods.is_empty(),
2541 "bolt11 method should be registered"
2542 );
2543 assert!(
2544 !mint_info.nuts.nut17.supported.is_empty(),
2545 "websocket support should be configured"
2546 );
2547 }
2548
2549 #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
2550 #[tokio::test]
2551 async fn fakewallet_onchain_with_fake_ln_does_not_duplicate() {
2552 use cdk::mint::MintBuilder;
2553 use cdk_sqlite::mint::memory;
2554
2555 use crate::config::{FakeWallet, Ln, LnBackend, Onchain, OnchainBackend};
2556
2557 let settings = config::Settings {
2558 ln: vec![Ln {
2559 ln_backend: LnBackend::FakeWallet,
2560 unit: CurrencyUnit::Sat,
2561 ..Default::default()
2562 }],
2563 onchain: Some(Onchain {
2564 onchain_backend: OnchainBackend::FakeWallet,
2565 ..Default::default()
2566 }),
2567 fake_wallet: Some(FakeWallet::default()),
2568 ..Default::default()
2569 };
2570
2571 let localstore = Arc::new(memory::empty().await.unwrap());
2572 let builder = MintBuilder::new(localstore);
2573 let builder =
2574 configure_onchain_backend(&settings, builder, None, &std::env::temp_dir(), None)
2575 .await
2576 .expect("fakewallet onchain with fake LN should succeed without duplicating");
2577
2578 let mint_info = builder.current_mint_info();
2579 assert!(
2580 mint_info.nuts.nut04.methods.is_empty(),
2581 "when has_lightning_backend is true and no real LN, fakewallet onchain should skip; got {:?}",
2582 mint_info.nuts.nut04.methods
2583 );
2584 }
2585
2586 #[cfg(all(feature = "fakewallet", feature = "sqlite"))]
2587 #[tokio::test]
2588 async fn fakewallet_onchain_missing_fake_wallet_config_bails() {
2589 use cdk::mint::MintBuilder;
2590 use cdk_sqlite::mint::memory;
2591
2592 use crate::config::{Ln, LnBackend, Onchain, OnchainBackend};
2593
2594 let settings = config::Settings {
2595 ln: vec![Ln {
2596 ln_backend: LnBackend::None,
2597 ..Default::default()
2598 }],
2599 onchain: Some(Onchain {
2600 onchain_backend: OnchainBackend::FakeWallet,
2601 ..Default::default()
2602 }),
2603 fake_wallet: None,
2604 ..Default::default()
2605 };
2606
2607 let localstore = Arc::new(memory::empty().await.unwrap());
2608 let builder = MintBuilder::new(localstore);
2609 let err = configure_onchain_backend(&settings, builder, None, &std::env::temp_dir(), None)
2610 .await
2611 .expect_err("missing fake_wallet config should bail");
2612
2613 assert!(
2614 err.to_string().contains("Fake wallet config"),
2615 "error should mention missing config: {err}"
2616 );
2617 }
2618
2619 #[test]
2620 fn test_postgres_auth_url_validation() {
2621 let auth_config = config::PostgresAuthConfig {
2625 url: "".to_string(),
2626 ..Default::default()
2627 };
2628 assert!(auth_config.url.is_empty());
2629
2630 let auth_config = config::PostgresAuthConfig {
2632 url: "postgresql://user:password@localhost:5432/auth_db".to_string(),
2633 ..Default::default()
2634 };
2635 assert!(!auth_config.url.is_empty());
2636 }
2637
2638 #[test]
2639 fn test_extract_supported_payment_methods_unique_ordered() {
2640 let mut mint_info = cdk::nuts::MintInfo::default();
2641 mint_info.nuts.nut04.methods = vec![
2642 MintMethodSettings {
2643 method: PaymentMethod::Known(KnownMethod::Bolt11),
2644 unit: CurrencyUnit::Sat,
2645 min_amount: None,
2646 max_amount: None,
2647 options: None,
2648 },
2649 MintMethodSettings {
2650 method: PaymentMethod::Known(KnownMethod::Bolt12),
2651 unit: CurrencyUnit::Sat,
2652 min_amount: None,
2653 max_amount: None,
2654 options: None,
2655 },
2656 MintMethodSettings {
2657 method: PaymentMethod::Known(KnownMethod::Bolt11),
2658 unit: CurrencyUnit::Msat,
2659 min_amount: None,
2660 max_amount: None,
2661 options: None,
2662 },
2663 MintMethodSettings {
2664 method: PaymentMethod::Custom("paypal".to_string()),
2665 unit: CurrencyUnit::Usd,
2666 min_amount: None,
2667 max_amount: None,
2668 options: None,
2669 },
2670 MintMethodSettings {
2671 method: PaymentMethod::Custom("paypal".to_string()),
2672 unit: CurrencyUnit::Eur,
2673 min_amount: None,
2674 max_amount: None,
2675 options: None,
2676 },
2677 ];
2678
2679 let methods = extract_supported_payment_methods(&mint_info);
2680
2681 assert_eq!(methods, vec!["bolt11", "bolt12", "paypal"]);
2682 }
2683}