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