lib.rs 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224
  1. //! Cdk mintd lib
  2. // std
  3. #[cfg(feature = "auth")]
  4. use std::collections::HashMap;
  5. use std::env::{self};
  6. use std::net::SocketAddr;
  7. use std::path::{Path, PathBuf};
  8. use std::str::FromStr;
  9. use std::sync::Arc;
  10. // external crates
  11. use anyhow::{anyhow, bail, Result};
  12. use axum::Router;
  13. use bip39::Mnemonic;
  14. use cdk::cdk_database::{self, MintDatabase, MintKVStore, MintKeysDatabase};
  15. use cdk::mint::{Mint, MintBuilder, MintMeltLimits};
  16. #[cfg(any(
  17. feature = "cln",
  18. feature = "lnbits",
  19. feature = "lnd",
  20. feature = "ldk-node",
  21. feature = "fakewallet",
  22. feature = "grpc-processor"
  23. ))]
  24. use cdk::nuts::nut17::SupportedMethods;
  25. use cdk::nuts::nut19::{CachedEndpoint, Method as NUT19Method, Path as NUT19Path};
  26. #[cfg(any(
  27. feature = "cln",
  28. feature = "lnbits",
  29. feature = "lnd",
  30. feature = "ldk-node",
  31. feature = "fakewallet"
  32. ))]
  33. use cdk::nuts::CurrencyUnit;
  34. #[cfg(feature = "auth")]
  35. use cdk::nuts::{AuthRequired, Method, ProtectedEndpoint, RoutePath};
  36. use cdk::nuts::{ContactInfo, MintVersion, PaymentMethod};
  37. use cdk_axum::cache::HttpCache;
  38. use cdk_common::common::QuoteTTL;
  39. use cdk_common::database::DynMintDatabase;
  40. // internal crate modules
  41. #[cfg(feature = "prometheus")]
  42. use cdk_common::payment::MetricsMintPayment;
  43. use cdk_common::payment::MintPayment;
  44. #[cfg(all(feature = "auth", feature = "postgres"))]
  45. use cdk_postgres::MintPgAuthDatabase;
  46. #[cfg(feature = "postgres")]
  47. use cdk_postgres::MintPgDatabase;
  48. #[cfg(all(feature = "auth", feature = "sqlite"))]
  49. use cdk_sqlite::mint::MintSqliteAuthDatabase;
  50. #[cfg(feature = "sqlite")]
  51. use cdk_sqlite::MintSqliteDatabase;
  52. use cli::CLIArgs;
  53. #[cfg(feature = "auth")]
  54. use config::AuthType;
  55. use config::{DatabaseEngine, LnBackend};
  56. use env_vars::ENV_WORK_DIR;
  57. use setup::LnBackendSetup;
  58. use tower::ServiceBuilder;
  59. use tower_http::compression::CompressionLayer;
  60. use tower_http::decompression::RequestDecompressionLayer;
  61. use tower_http::trace::TraceLayer;
  62. use tracing_appender::{non_blocking, rolling};
  63. use tracing_subscriber::fmt::writer::MakeWriterExt;
  64. use tracing_subscriber::EnvFilter;
  65. #[cfg(feature = "swagger")]
  66. use utoipa::OpenApi;
  67. pub mod cli;
  68. pub mod config;
  69. pub mod env_vars;
  70. pub mod setup;
  71. const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
  72. #[cfg(feature = "cln")]
  73. fn expand_path(path: &str) -> Option<PathBuf> {
  74. if path.starts_with('~') {
  75. if let Some(home_dir) = home::home_dir().as_mut() {
  76. let remainder = &path[2..];
  77. home_dir.push(remainder);
  78. let expanded_path = home_dir;
  79. Some(expanded_path.clone())
  80. } else {
  81. None
  82. }
  83. } else {
  84. Some(PathBuf::from(path))
  85. }
  86. }
  87. /// Performs the initial setup for the application, including configuring tracing,
  88. /// parsing CLI arguments, setting up the working directory, loading settings,
  89. /// and initializing the database connection.
  90. async fn initial_setup(
  91. work_dir: &Path,
  92. settings: &config::Settings,
  93. db_password: Option<String>,
  94. ) -> Result<(
  95. DynMintDatabase,
  96. Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
  97. Arc<dyn MintKVStore<Err = cdk_database::Error> + Send + Sync>,
  98. )> {
  99. let (localstore, keystore, kv) = setup_database(settings, work_dir, db_password).await?;
  100. Ok((localstore, keystore, kv))
  101. }
  102. /// Sets up and initializes a tracing subscriber with custom log filtering.
  103. /// Logs can be configured to output to stdout only, file only, or both.
  104. /// Returns a guard that must be kept alive and properly dropped on shutdown.
  105. pub fn setup_tracing(
  106. work_dir: &Path,
  107. logging_config: &config::LoggingConfig,
  108. ) -> Result<Option<tracing_appender::non_blocking::WorkerGuard>> {
  109. let default_filter = "debug";
  110. let hyper_filter = "hyper=warn,rustls=warn,reqwest=warn";
  111. let h2_filter = "h2=warn";
  112. let tower_http = "tower_http=warn";
  113. let rustls = "rustls=warn";
  114. let env_filter = EnvFilter::new(format!(
  115. "{default_filter},{hyper_filter},{h2_filter},{tower_http},{rustls}"
  116. ));
  117. use config::LoggingOutput;
  118. match logging_config.output {
  119. LoggingOutput::Stderr => {
  120. // Console output only (stderr)
  121. let console_level = logging_config
  122. .console_level
  123. .as_deref()
  124. .unwrap_or("info")
  125. .parse::<tracing::Level>()
  126. .unwrap_or(tracing::Level::INFO);
  127. let stderr = std::io::stderr.with_max_level(console_level);
  128. tracing_subscriber::fmt()
  129. .with_env_filter(env_filter)
  130. .with_writer(stderr)
  131. .init();
  132. tracing::info!("Logging initialized: console only ({}+)", console_level);
  133. Ok(None)
  134. }
  135. LoggingOutput::File => {
  136. // File output only
  137. let file_level = logging_config
  138. .file_level
  139. .as_deref()
  140. .unwrap_or("debug")
  141. .parse::<tracing::Level>()
  142. .unwrap_or(tracing::Level::DEBUG);
  143. // Create logs directory in work_dir if it doesn't exist
  144. let logs_dir = work_dir.join("logs");
  145. std::fs::create_dir_all(&logs_dir)?;
  146. // Set up file appender with daily rotation
  147. let file_appender = rolling::daily(&logs_dir, "cdk-mintd.log");
  148. let (non_blocking_appender, guard) = non_blocking(file_appender);
  149. let file_writer = non_blocking_appender.with_max_level(file_level);
  150. tracing_subscriber::fmt()
  151. .with_env_filter(env_filter)
  152. .with_writer(file_writer)
  153. .init();
  154. tracing::info!(
  155. "Logging initialized: file only at {}/cdk-mintd.log ({}+)",
  156. logs_dir.display(),
  157. file_level
  158. );
  159. Ok(Some(guard))
  160. }
  161. LoggingOutput::Both => {
  162. // Both console and file output (stderr + file)
  163. let console_level = logging_config
  164. .console_level
  165. .as_deref()
  166. .unwrap_or("info")
  167. .parse::<tracing::Level>()
  168. .unwrap_or(tracing::Level::INFO);
  169. let file_level = logging_config
  170. .file_level
  171. .as_deref()
  172. .unwrap_or("debug")
  173. .parse::<tracing::Level>()
  174. .unwrap_or(tracing::Level::DEBUG);
  175. // Create logs directory in work_dir if it doesn't exist
  176. let logs_dir = work_dir.join("logs");
  177. std::fs::create_dir_all(&logs_dir)?;
  178. // Set up file appender with daily rotation
  179. let file_appender = rolling::daily(&logs_dir, "cdk-mintd.log");
  180. let (non_blocking_appender, guard) = non_blocking(file_appender);
  181. // Combine console output (stderr) and file output
  182. let stderr = std::io::stderr.with_max_level(console_level);
  183. let file_writer = non_blocking_appender.with_max_level(file_level);
  184. tracing_subscriber::fmt()
  185. .with_env_filter(env_filter)
  186. .with_writer(stderr.and(file_writer))
  187. .init();
  188. tracing::info!(
  189. "Logging initialized: console ({}+) and file at {}/cdk-mintd.log ({}+)",
  190. console_level,
  191. logs_dir.display(),
  192. file_level
  193. );
  194. Ok(Some(guard))
  195. }
  196. }
  197. }
  198. /// Retrieves the work directory based on command-line arguments, environment variables, or system defaults.
  199. pub async fn get_work_directory(args: &CLIArgs) -> Result<PathBuf> {
  200. let work_dir = if let Some(work_dir) = &args.work_dir {
  201. tracing::info!("Using work dir from cmd arg");
  202. work_dir.clone()
  203. } else if let Ok(env_work_dir) = env::var(ENV_WORK_DIR) {
  204. tracing::info!("Using work dir from env var");
  205. env_work_dir.into()
  206. } else {
  207. work_dir()?
  208. };
  209. tracing::info!("Using work dir: {}", work_dir.display());
  210. Ok(work_dir)
  211. }
  212. /// Loads the application settings based on a configuration file and environment variables.
  213. pub fn load_settings(work_dir: &Path, config_path: Option<PathBuf>) -> Result<config::Settings> {
  214. // get config file name from args
  215. let config_file_arg = match config_path {
  216. Some(c) => c,
  217. None => work_dir.join("config.toml"),
  218. };
  219. let mut settings = if config_file_arg.exists() {
  220. config::Settings::new(Some(config_file_arg))
  221. } else {
  222. tracing::info!("Config file does not exist. Attempting to read env vars");
  223. config::Settings::default()
  224. };
  225. // This check for any settings defined in ENV VARs
  226. // ENV VARS will take **priority** over those in the config
  227. settings.from_env()
  228. }
  229. async fn setup_database(
  230. settings: &config::Settings,
  231. _work_dir: &Path,
  232. _db_password: Option<String>,
  233. ) -> Result<(
  234. DynMintDatabase,
  235. Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
  236. Arc<dyn MintKVStore<Err = cdk_database::Error> + Send + Sync>,
  237. )> {
  238. match settings.database.engine {
  239. #[cfg(feature = "sqlite")]
  240. DatabaseEngine::Sqlite => {
  241. let db = setup_sqlite_database(_work_dir, _db_password).await?;
  242. let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> = db.clone();
  243. let kv: Arc<dyn MintKVStore<Err = cdk_database::Error> + Send + Sync> = db.clone();
  244. let keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync> = db;
  245. Ok((localstore, keystore, kv))
  246. }
  247. #[cfg(feature = "postgres")]
  248. DatabaseEngine::Postgres => {
  249. // Get the PostgreSQL configuration, ensuring it exists
  250. let pg_config = settings.database.postgres.as_ref().ok_or_else(|| {
  251. anyhow!("PostgreSQL configuration is required when using PostgreSQL engine")
  252. })?;
  253. if pg_config.url.is_empty() {
  254. 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");
  255. }
  256. #[cfg(feature = "postgres")]
  257. let pg_db = Arc::new(MintPgDatabase::new(pg_config.url.as_str()).await?);
  258. #[cfg(feature = "postgres")]
  259. let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> =
  260. pg_db.clone();
  261. #[cfg(feature = "postgres")]
  262. let kv: Arc<dyn MintKVStore<Err = cdk_database::Error> + Send + Sync> = pg_db.clone();
  263. #[cfg(feature = "postgres")]
  264. let keystore: Arc<
  265. dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync,
  266. > = pg_db;
  267. #[cfg(feature = "postgres")]
  268. return Ok((localstore, keystore, kv));
  269. #[cfg(not(feature = "postgres"))]
  270. bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
  271. }
  272. #[cfg(not(feature = "sqlite"))]
  273. DatabaseEngine::Sqlite => {
  274. bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
  275. }
  276. #[cfg(not(feature = "postgres"))]
  277. DatabaseEngine::Postgres => {
  278. bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
  279. }
  280. }
  281. }
  282. #[cfg(feature = "sqlite")]
  283. async fn setup_sqlite_database(
  284. work_dir: &Path,
  285. _password: Option<String>,
  286. ) -> Result<Arc<MintSqliteDatabase>> {
  287. let sql_db_path = work_dir.join("cdk-mintd.sqlite");
  288. #[cfg(not(feature = "sqlcipher"))]
  289. let db = MintSqliteDatabase::new(&sql_db_path).await?;
  290. #[cfg(feature = "sqlcipher")]
  291. let db = {
  292. // Get password from command line arguments for sqlcipher
  293. MintSqliteDatabase::new((sql_db_path, _password.unwrap())).await?
  294. };
  295. Ok(Arc::new(db))
  296. }
  297. /**
  298. * Configures a `MintBuilder` instance with provided settings and initializes
  299. * routers for Lightning Network backends.
  300. */
  301. async fn configure_mint_builder(
  302. settings: &config::Settings,
  303. mint_builder: MintBuilder,
  304. runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
  305. work_dir: &Path,
  306. kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
  307. ) -> Result<MintBuilder> {
  308. // Configure basic mint information
  309. let mint_builder = configure_basic_info(settings, mint_builder);
  310. // Configure lightning backend
  311. let mint_builder =
  312. configure_lightning_backend(settings, mint_builder, runtime, work_dir, kv_store).await?;
  313. // Configure caching
  314. let mint_builder = configure_cache(settings, mint_builder);
  315. Ok(mint_builder)
  316. }
  317. /// Configures basic mint information (name, contact info, descriptions, etc.)
  318. fn configure_basic_info(settings: &config::Settings, mint_builder: MintBuilder) -> MintBuilder {
  319. // Add contact information
  320. let mut contacts = Vec::new();
  321. if let Some(nostr_key) = &settings.mint_info.contact_nostr_public_key {
  322. contacts.push(ContactInfo::new("nostr".to_string(), nostr_key.to_string()));
  323. }
  324. if let Some(email) = &settings.mint_info.contact_email {
  325. contacts.push(ContactInfo::new("email".to_string(), email.to_string()));
  326. }
  327. // Add version information
  328. let mint_version = MintVersion::new(
  329. "cdk-mintd".to_string(),
  330. CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
  331. );
  332. // Configure mint builder with basic info
  333. let mut builder = mint_builder
  334. .with_name(settings.mint_info.name.clone())
  335. .with_version(mint_version)
  336. .with_description(settings.mint_info.description.clone());
  337. // Add optional information
  338. if let Some(long_description) = &settings.mint_info.description_long {
  339. builder = builder.with_long_description(long_description.to_string());
  340. }
  341. for contact in contacts {
  342. builder = builder.with_contact_info(contact);
  343. }
  344. if let Some(pubkey) = settings.mint_info.pubkey {
  345. builder = builder.with_pubkey(pubkey);
  346. }
  347. if let Some(icon_url) = &settings.mint_info.icon_url {
  348. builder = builder.with_icon_url(icon_url.to_string());
  349. }
  350. if let Some(motd) = &settings.mint_info.motd {
  351. builder = builder.with_motd(motd.to_string());
  352. }
  353. if let Some(tos_url) = &settings.mint_info.tos_url {
  354. builder = builder.with_tos_url(tos_url.to_string());
  355. }
  356. builder
  357. }
  358. /// Configures Lightning Network backend based on the specified backend type
  359. async fn configure_lightning_backend(
  360. settings: &config::Settings,
  361. mut mint_builder: MintBuilder,
  362. _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
  363. work_dir: &Path,
  364. _kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
  365. ) -> Result<MintBuilder> {
  366. let mint_melt_limits = MintMeltLimits {
  367. mint_min: settings.ln.min_mint,
  368. mint_max: settings.ln.max_mint,
  369. melt_min: settings.ln.min_melt,
  370. melt_max: settings.ln.max_melt,
  371. };
  372. tracing::debug!("Ln backend: {:?}", settings.ln.ln_backend);
  373. match settings.ln.ln_backend {
  374. #[cfg(feature = "cln")]
  375. LnBackend::Cln => {
  376. let cln_settings = settings
  377. .cln
  378. .clone()
  379. .expect("Config checked at load that cln is some");
  380. let cln = cln_settings
  381. .setup(settings, CurrencyUnit::Msat, None, work_dir, _kv_store)
  382. .await?;
  383. #[cfg(feature = "prometheus")]
  384. let cln = MetricsMintPayment::new(cln);
  385. mint_builder = configure_backend_for_unit(
  386. settings,
  387. mint_builder,
  388. CurrencyUnit::Sat,
  389. mint_melt_limits,
  390. Arc::new(cln),
  391. )
  392. .await?;
  393. }
  394. #[cfg(feature = "lnbits")]
  395. LnBackend::LNbits => {
  396. let lnbits_settings = settings.clone().lnbits.expect("Checked on config load");
  397. let lnbits = lnbits_settings
  398. .setup(settings, CurrencyUnit::Sat, None, work_dir, None)
  399. .await?;
  400. #[cfg(feature = "prometheus")]
  401. let lnbits = MetricsMintPayment::new(lnbits);
  402. mint_builder = configure_backend_for_unit(
  403. settings,
  404. mint_builder,
  405. CurrencyUnit::Sat,
  406. mint_melt_limits,
  407. Arc::new(lnbits),
  408. )
  409. .await?;
  410. }
  411. #[cfg(feature = "lnd")]
  412. LnBackend::Lnd => {
  413. let lnd_settings = settings.clone().lnd.expect("Checked at config load");
  414. let lnd = lnd_settings
  415. .setup(settings, CurrencyUnit::Msat, None, work_dir, _kv_store)
  416. .await?;
  417. #[cfg(feature = "prometheus")]
  418. let lnd = MetricsMintPayment::new(lnd);
  419. mint_builder = configure_backend_for_unit(
  420. settings,
  421. mint_builder,
  422. CurrencyUnit::Sat,
  423. mint_melt_limits,
  424. Arc::new(lnd),
  425. )
  426. .await?;
  427. }
  428. #[cfg(feature = "fakewallet")]
  429. LnBackend::FakeWallet => {
  430. let fake_wallet = settings.clone().fake_wallet.expect("Fake wallet defined");
  431. tracing::info!("Using fake wallet: {:?}", fake_wallet);
  432. for unit in fake_wallet.clone().supported_units {
  433. let fake = fake_wallet
  434. .setup(settings, unit.clone(), None, work_dir, _kv_store.clone())
  435. .await?;
  436. #[cfg(feature = "prometheus")]
  437. let fake = MetricsMintPayment::new(fake);
  438. mint_builder = configure_backend_for_unit(
  439. settings,
  440. mint_builder,
  441. unit.clone(),
  442. mint_melt_limits,
  443. Arc::new(fake),
  444. )
  445. .await?;
  446. }
  447. }
  448. #[cfg(feature = "grpc-processor")]
  449. LnBackend::GrpcProcessor => {
  450. let grpc_processor = settings
  451. .clone()
  452. .grpc_processor
  453. .expect("grpc processor config defined");
  454. tracing::info!(
  455. "Attempting to start with gRPC payment processor at {}:{}.",
  456. grpc_processor.addr,
  457. grpc_processor.port
  458. );
  459. for unit in grpc_processor.clone().supported_units {
  460. tracing::debug!("Adding unit: {:?}", unit);
  461. let processor = grpc_processor
  462. .setup(settings, unit.clone(), None, work_dir, None)
  463. .await?;
  464. #[cfg(feature = "prometheus")]
  465. let processor = MetricsMintPayment::new(processor);
  466. mint_builder = configure_backend_for_unit(
  467. settings,
  468. mint_builder,
  469. unit.clone(),
  470. mint_melt_limits,
  471. Arc::new(processor),
  472. )
  473. .await?;
  474. }
  475. }
  476. #[cfg(feature = "ldk-node")]
  477. LnBackend::LdkNode => {
  478. let ldk_node_settings = settings.clone().ldk_node.expect("Checked at config load");
  479. tracing::info!("Using LDK Node backend: {:?}", ldk_node_settings);
  480. let ldk_node = ldk_node_settings
  481. .setup(settings, CurrencyUnit::Sat, _runtime, work_dir, None)
  482. .await?;
  483. mint_builder = configure_backend_for_unit(
  484. settings,
  485. mint_builder,
  486. CurrencyUnit::Sat,
  487. mint_melt_limits,
  488. Arc::new(ldk_node),
  489. )
  490. .await?;
  491. }
  492. LnBackend::None => {
  493. tracing::error!(
  494. "Payment backend was not set or feature disabled. {:?}",
  495. settings.ln.ln_backend
  496. );
  497. bail!("Lightning backend must be configured");
  498. }
  499. };
  500. Ok(mint_builder)
  501. }
  502. /// Helper function to configure a mint builder with a lightning backend for a specific currency unit
  503. async fn configure_backend_for_unit(
  504. settings: &config::Settings,
  505. mut mint_builder: MintBuilder,
  506. unit: cdk::nuts::CurrencyUnit,
  507. mint_melt_limits: MintMeltLimits,
  508. backend: Arc<dyn MintPayment<Err = cdk_common::payment::Error> + Send + Sync>,
  509. ) -> Result<MintBuilder> {
  510. let payment_settings = backend.get_settings().await?;
  511. if let Some(bolt12) = payment_settings.get("bolt12") {
  512. if bolt12.as_bool().unwrap_or_default() {
  513. mint_builder
  514. .add_payment_processor(
  515. unit.clone(),
  516. PaymentMethod::Bolt12,
  517. mint_melt_limits,
  518. Arc::clone(&backend),
  519. )
  520. .await?;
  521. let nut17_supported = SupportedMethods::default_bolt12(unit.clone());
  522. mint_builder = mint_builder.with_supported_websockets(nut17_supported);
  523. }
  524. }
  525. mint_builder
  526. .add_payment_processor(
  527. unit.clone(),
  528. PaymentMethod::Bolt11,
  529. mint_melt_limits,
  530. backend,
  531. )
  532. .await?;
  533. if let Some(input_fee) = settings.info.input_fee_ppk {
  534. mint_builder.set_unit_fee(&unit, input_fee)?;
  535. }
  536. #[cfg(any(
  537. feature = "cln",
  538. feature = "lnbits",
  539. feature = "lnd",
  540. feature = "fakewallet",
  541. feature = "grpc-processor",
  542. feature = "ldk-node"
  543. ))]
  544. {
  545. let nut17_supported = SupportedMethods::default_bolt11(unit);
  546. mint_builder = mint_builder.with_supported_websockets(nut17_supported);
  547. }
  548. Ok(mint_builder)
  549. }
  550. /// Configures cache settings
  551. fn configure_cache(settings: &config::Settings, mint_builder: MintBuilder) -> MintBuilder {
  552. let cached_endpoints = vec![
  553. CachedEndpoint::new(NUT19Method::Post, NUT19Path::MintBolt11),
  554. CachedEndpoint::new(NUT19Method::Post, NUT19Path::MeltBolt11),
  555. CachedEndpoint::new(NUT19Method::Post, NUT19Path::Swap),
  556. ];
  557. let cache: HttpCache = settings.info.http_cache.clone().into();
  558. mint_builder.with_cache(Some(cache.ttl.as_secs()), cached_endpoints)
  559. }
  560. #[cfg(feature = "auth")]
  561. async fn setup_authentication(
  562. settings: &config::Settings,
  563. _work_dir: &Path,
  564. mut mint_builder: MintBuilder,
  565. _password: Option<String>,
  566. ) -> Result<MintBuilder> {
  567. if let Some(auth_settings) = settings.auth.clone() {
  568. use cdk_common::database::DynMintAuthDatabase;
  569. tracing::info!("Auth settings are defined. {:?}", auth_settings);
  570. let auth_localstore: DynMintAuthDatabase = match settings.database.engine {
  571. #[cfg(feature = "sqlite")]
  572. DatabaseEngine::Sqlite => {
  573. #[cfg(feature = "sqlite")]
  574. {
  575. let sql_db_path = _work_dir.join("cdk-mintd-auth.sqlite");
  576. #[cfg(not(feature = "sqlcipher"))]
  577. let sqlite_db = MintSqliteAuthDatabase::new(&sql_db_path).await?;
  578. #[cfg(feature = "sqlcipher")]
  579. let sqlite_db = {
  580. // Get password from command line arguments for sqlcipher
  581. MintSqliteAuthDatabase::new((sql_db_path, _password.unwrap())).await?
  582. };
  583. Arc::new(sqlite_db)
  584. }
  585. #[cfg(not(feature = "sqlite"))]
  586. {
  587. bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
  588. }
  589. }
  590. #[cfg(feature = "postgres")]
  591. DatabaseEngine::Postgres => {
  592. #[cfg(feature = "postgres")]
  593. {
  594. // Require dedicated auth database configuration - no fallback to main database
  595. let auth_db_config = settings.auth_database.as_ref().ok_or_else(|| {
  596. 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")
  597. })?;
  598. let auth_pg_config = auth_db_config.postgres.as_ref().ok_or_else(|| {
  599. 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")
  600. })?;
  601. if auth_pg_config.url.is_empty() {
  602. 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");
  603. }
  604. Arc::new(MintPgAuthDatabase::new(auth_pg_config.url.as_str()).await?)
  605. }
  606. #[cfg(not(feature = "postgres"))]
  607. {
  608. bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
  609. }
  610. }
  611. #[cfg(not(feature = "sqlite"))]
  612. DatabaseEngine::Sqlite => {
  613. bail!("SQLite support not compiled in. Enable the 'sqlite' feature to use SQLite database.")
  614. }
  615. #[cfg(not(feature = "postgres"))]
  616. DatabaseEngine::Postgres => {
  617. bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
  618. }
  619. };
  620. let mut protected_endpoints = HashMap::new();
  621. let mut blind_auth_endpoints = vec![];
  622. let mut clear_auth_endpoints = vec![];
  623. let mut unprotected_endpoints = vec![];
  624. let mint_blind_auth_endpoint =
  625. ProtectedEndpoint::new(Method::Post, RoutePath::MintBlindAuth);
  626. protected_endpoints.insert(mint_blind_auth_endpoint, AuthRequired::Clear);
  627. clear_auth_endpoints.push(mint_blind_auth_endpoint);
  628. // Helper function to add endpoint based on auth type
  629. let mut add_endpoint = |endpoint: ProtectedEndpoint, auth_type: &AuthType| {
  630. match auth_type {
  631. AuthType::Blind => {
  632. protected_endpoints.insert(endpoint, AuthRequired::Blind);
  633. blind_auth_endpoints.push(endpoint);
  634. }
  635. AuthType::Clear => {
  636. protected_endpoints.insert(endpoint, AuthRequired::Clear);
  637. clear_auth_endpoints.push(endpoint);
  638. }
  639. AuthType::None => {
  640. unprotected_endpoints.push(endpoint);
  641. }
  642. };
  643. };
  644. // Get mint quote endpoint
  645. {
  646. let mint_quote_protected_endpoint =
  647. ProtectedEndpoint::new(cdk::nuts::Method::Post, RoutePath::MintQuoteBolt11);
  648. add_endpoint(mint_quote_protected_endpoint, &auth_settings.get_mint_quote);
  649. }
  650. // Check mint quote endpoint
  651. {
  652. let check_mint_protected_endpoint =
  653. ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11);
  654. add_endpoint(
  655. check_mint_protected_endpoint,
  656. &auth_settings.check_mint_quote,
  657. );
  658. }
  659. // Mint endpoint
  660. {
  661. let mint_protected_endpoint =
  662. ProtectedEndpoint::new(cdk::nuts::Method::Post, RoutePath::MintBolt11);
  663. add_endpoint(mint_protected_endpoint, &auth_settings.mint);
  664. }
  665. // Get melt quote endpoint
  666. {
  667. let melt_quote_protected_endpoint = ProtectedEndpoint::new(
  668. cdk::nuts::Method::Post,
  669. cdk::nuts::RoutePath::MeltQuoteBolt11,
  670. );
  671. add_endpoint(melt_quote_protected_endpoint, &auth_settings.get_melt_quote);
  672. }
  673. // Check melt quote endpoint
  674. {
  675. let check_melt_protected_endpoint =
  676. ProtectedEndpoint::new(Method::Get, RoutePath::MeltQuoteBolt11);
  677. add_endpoint(
  678. check_melt_protected_endpoint,
  679. &auth_settings.check_melt_quote,
  680. );
  681. }
  682. // Melt endpoint
  683. {
  684. let melt_protected_endpoint =
  685. ProtectedEndpoint::new(Method::Post, RoutePath::MeltBolt11);
  686. add_endpoint(melt_protected_endpoint, &auth_settings.melt);
  687. }
  688. // Swap endpoint
  689. {
  690. let swap_protected_endpoint = ProtectedEndpoint::new(Method::Post, RoutePath::Swap);
  691. add_endpoint(swap_protected_endpoint, &auth_settings.swap);
  692. }
  693. // Restore endpoint
  694. {
  695. let restore_protected_endpoint =
  696. ProtectedEndpoint::new(Method::Post, RoutePath::Restore);
  697. add_endpoint(restore_protected_endpoint, &auth_settings.restore);
  698. }
  699. // Check proof state endpoint
  700. {
  701. let state_protected_endpoint =
  702. ProtectedEndpoint::new(Method::Post, RoutePath::Checkstate);
  703. add_endpoint(state_protected_endpoint, &auth_settings.check_proof_state);
  704. }
  705. // Ws endpoint
  706. {
  707. let ws_protected_endpoint = ProtectedEndpoint::new(Method::Get, RoutePath::Ws);
  708. add_endpoint(ws_protected_endpoint, &auth_settings.websocket_auth);
  709. }
  710. mint_builder = mint_builder.with_auth(
  711. auth_localstore.clone(),
  712. auth_settings.openid_discovery,
  713. auth_settings.openid_client_id,
  714. clear_auth_endpoints,
  715. );
  716. mint_builder =
  717. mint_builder.with_blind_auth(auth_settings.mint_max_bat, blind_auth_endpoints);
  718. let mut tx = auth_localstore.begin_transaction().await?;
  719. tx.remove_protected_endpoints(unprotected_endpoints).await?;
  720. tx.add_protected_endpoints(protected_endpoints).await?;
  721. tx.commit().await?;
  722. }
  723. Ok(mint_builder)
  724. }
  725. /// Build mints with the configured the signing method (remote signatory or local seed)
  726. async fn build_mint(
  727. settings: &config::Settings,
  728. keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
  729. mint_builder: MintBuilder,
  730. ) -> Result<Mint> {
  731. if let Some(signatory_url) = settings.info.signatory_url.clone() {
  732. tracing::info!(
  733. "Connecting to remote signatory to {} with certs {:?}",
  734. signatory_url,
  735. settings.info.signatory_certs.clone()
  736. );
  737. Ok(mint_builder
  738. .build_with_signatory(Arc::new(
  739. cdk_signatory::SignatoryRpcClient::new(
  740. signatory_url,
  741. settings.info.signatory_certs.clone(),
  742. )
  743. .await?,
  744. ))
  745. .await?)
  746. } else if let Some(seed) = settings.info.seed.clone() {
  747. let seed_bytes: Vec<u8> = seed.into();
  748. Ok(mint_builder.build_with_seed(keystore, &seed_bytes).await?)
  749. } else if let Some(mnemonic) = settings
  750. .info
  751. .mnemonic
  752. .clone()
  753. .map(|s| Mnemonic::from_str(&s))
  754. .transpose()?
  755. {
  756. Ok(mint_builder
  757. .build_with_seed(keystore, &mnemonic.to_seed_normalized(""))
  758. .await?)
  759. } else {
  760. bail!("No seed nor remote signatory set");
  761. }
  762. }
  763. async fn start_services_with_shutdown(
  764. mint: Arc<cdk::mint::Mint>,
  765. settings: &config::Settings,
  766. work_dir: &Path,
  767. mint_builder_info: cdk::nuts::MintInfo,
  768. shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
  769. routers: Vec<Router>,
  770. ) -> Result<()> {
  771. let listen_addr = settings.info.listen_host.clone();
  772. let listen_port = settings.info.listen_port;
  773. let cache: HttpCache = settings.info.http_cache.clone().into();
  774. #[cfg(feature = "management-rpc")]
  775. let mut rpc_enabled = false;
  776. #[cfg(not(feature = "management-rpc"))]
  777. let rpc_enabled = false;
  778. #[cfg(feature = "management-rpc")]
  779. let mut rpc_server: Option<cdk_mint_rpc::MintRPCServer> = None;
  780. #[cfg(feature = "management-rpc")]
  781. {
  782. if let Some(rpc_settings) = settings.mint_management_rpc.clone() {
  783. if rpc_settings.enabled {
  784. let addr = rpc_settings.address.unwrap_or("127.0.0.1".to_string());
  785. let port = rpc_settings.port.unwrap_or(8086);
  786. let mut mint_rpc = cdk_mint_rpc::MintRPCServer::new(&addr, port, mint.clone())?;
  787. let tls_dir = rpc_settings.tls_dir_path.unwrap_or(work_dir.join("tls"));
  788. if !tls_dir.exists() {
  789. tracing::error!("TLS directory does not exist: {}", tls_dir.display());
  790. bail!("Cannot start RPC server: TLS directory does not exist");
  791. }
  792. mint_rpc.start(Some(tls_dir)).await?;
  793. rpc_server = Some(mint_rpc);
  794. rpc_enabled = true;
  795. }
  796. }
  797. }
  798. // Determine the desired QuoteTTL from config/env or fall back to defaults
  799. let desired_quote_ttl: QuoteTTL = settings.info.quote_ttl.unwrap_or_default();
  800. if rpc_enabled {
  801. if mint.mint_info().await.is_err() {
  802. tracing::info!("Mint info not set on mint, setting.");
  803. // First boot with RPC enabled: seed from config
  804. mint.set_mint_info(mint_builder_info).await?;
  805. mint.set_quote_ttl(desired_quote_ttl).await?;
  806. } else {
  807. // If QuoteTTL has never been persisted, seed it now from config
  808. if !mint.quote_ttl_is_persisted().await? {
  809. mint.set_quote_ttl(desired_quote_ttl).await?;
  810. }
  811. // Add/refresh version information without altering stored mint_info fields
  812. let mint_version = MintVersion::new(
  813. "cdk-mintd".to_string(),
  814. CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
  815. );
  816. let mut stored_mint_info = mint.mint_info().await?;
  817. stored_mint_info.version = Some(mint_version);
  818. mint.set_mint_info(stored_mint_info).await?;
  819. tracing::info!("Mint info already set, not using config file settings.");
  820. }
  821. } else {
  822. // RPC disabled: config is source of truth on every boot
  823. tracing::info!("RPC not enabled, using mint info and quote TTL from config.");
  824. let mut mint_builder_info = mint_builder_info;
  825. if let Ok(mint_info) = mint.mint_info().await {
  826. if mint_builder_info.pubkey.is_none() {
  827. mint_builder_info.pubkey = mint_info.pubkey;
  828. }
  829. }
  830. mint.set_mint_info(mint_builder_info).await?;
  831. mint.set_quote_ttl(desired_quote_ttl).await?;
  832. }
  833. let mint_info = mint.mint_info().await?;
  834. let nut04_methods = mint_info.nuts.nut04.supported_methods();
  835. let nut05_methods = mint_info.nuts.nut05.supported_methods();
  836. let bolt12_supported = nut04_methods.contains(&&PaymentMethod::Bolt12)
  837. || nut05_methods.contains(&&PaymentMethod::Bolt12);
  838. let v1_service =
  839. cdk_axum::create_mint_router_with_custom_cache(Arc::clone(&mint), cache, bolt12_supported)
  840. .await?;
  841. let mut mint_service = Router::new()
  842. .merge(v1_service)
  843. .layer(
  844. ServiceBuilder::new()
  845. .layer(RequestDecompressionLayer::new())
  846. .layer(CompressionLayer::new()),
  847. )
  848. .layer(TraceLayer::new_for_http());
  849. for router in routers {
  850. mint_service = mint_service.merge(router);
  851. }
  852. #[cfg(feature = "swagger")]
  853. {
  854. if settings.info.enable_swagger_ui.unwrap_or(false) {
  855. mint_service = mint_service.merge(
  856. utoipa_swagger_ui::SwaggerUi::new("/swagger-ui")
  857. .url("/api-docs/openapi.json", cdk_axum::ApiDoc::openapi()),
  858. );
  859. }
  860. }
  861. // Create a broadcast channel to share shutdown signal between services
  862. let (shutdown_tx, _) = tokio::sync::broadcast::channel::<()>(1);
  863. // Start Prometheus server if enabled
  864. #[cfg(feature = "prometheus")]
  865. let prometheus_handle = {
  866. if let Some(prometheus_settings) = &settings.prometheus {
  867. if prometheus_settings.enabled {
  868. let addr = prometheus_settings
  869. .address
  870. .clone()
  871. .unwrap_or("127.0.0.1".to_string());
  872. let port = prometheus_settings.port.unwrap_or(9000);
  873. let address = format!("{}:{}", addr, port)
  874. .parse()
  875. .expect("Invalid prometheus address");
  876. let server = cdk_prometheus::PrometheusBuilder::new()
  877. .bind_address(address)
  878. .build_with_cdk_metrics()?;
  879. let mut shutdown_rx = shutdown_tx.subscribe();
  880. let prometheus_shutdown = async move {
  881. let _ = shutdown_rx.recv().await;
  882. };
  883. Some(tokio::spawn(async move {
  884. if let Err(e) = server.start(prometheus_shutdown).await {
  885. tracing::error!("Failed to start prometheus server: {}", e);
  886. }
  887. }))
  888. } else {
  889. None
  890. }
  891. } else {
  892. None
  893. }
  894. };
  895. #[cfg(not(feature = "prometheus"))]
  896. let prometheus_handle: Option<tokio::task::JoinHandle<()>> = None;
  897. mint.start().await?;
  898. let socket_addr = SocketAddr::from_str(&format!("{listen_addr}:{listen_port}"))?;
  899. let listener = tokio::net::TcpListener::bind(socket_addr).await?;
  900. tracing::info!("listening on {}", listener.local_addr().unwrap());
  901. // Create a task to wait for the shutdown signal and broadcast it
  902. let shutdown_broadcast_task = {
  903. let shutdown_tx = shutdown_tx.clone();
  904. tokio::spawn(async move {
  905. shutdown_signal.await;
  906. tracing::info!("Shutdown signal received, broadcasting to all services");
  907. let _ = shutdown_tx.send(());
  908. })
  909. };
  910. // Create shutdown future for axum server
  911. let mut axum_shutdown_rx = shutdown_tx.subscribe();
  912. let axum_shutdown = async move {
  913. let _ = axum_shutdown_rx.recv().await;
  914. };
  915. // Wait for axum server to complete with custom shutdown signal
  916. let axum_result = axum::serve(listener, mint_service).with_graceful_shutdown(axum_shutdown);
  917. match axum_result.await {
  918. Ok(_) => {
  919. tracing::info!("Axum server stopped with okay status");
  920. }
  921. Err(err) => {
  922. tracing::warn!("Axum server stopped with error");
  923. tracing::error!("{}", err);
  924. bail!("Axum exited with error")
  925. }
  926. }
  927. // Wait for the shutdown broadcast task to complete
  928. let _ = shutdown_broadcast_task.await;
  929. // Wait for prometheus server to shutdown if it was started
  930. #[cfg(feature = "prometheus")]
  931. if let Some(handle) = prometheus_handle {
  932. if let Err(e) = handle.await {
  933. tracing::warn!("Prometheus server task failed: {}", e);
  934. }
  935. }
  936. mint.stop().await?;
  937. #[cfg(feature = "management-rpc")]
  938. {
  939. if let Some(rpc_server) = rpc_server {
  940. rpc_server.stop().await?;
  941. }
  942. }
  943. Ok(())
  944. }
  945. async fn shutdown_signal() {
  946. tokio::signal::ctrl_c()
  947. .await
  948. .expect("failed to install CTRL+C handler");
  949. tracing::info!("Shutdown signal received");
  950. }
  951. fn work_dir() -> Result<PathBuf> {
  952. let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?;
  953. let dir = home_dir.join(".cdk-mintd");
  954. std::fs::create_dir_all(&dir)?;
  955. Ok(dir)
  956. }
  957. /// The main entry point for the application when used as a library
  958. pub async fn run_mintd(
  959. work_dir: &Path,
  960. settings: &config::Settings,
  961. db_password: Option<String>,
  962. enable_logging: bool,
  963. runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
  964. routers: Vec<Router>,
  965. ) -> Result<()> {
  966. let _guard = if enable_logging {
  967. setup_tracing(work_dir, &settings.info.logging)?
  968. } else {
  969. None
  970. };
  971. let result = run_mintd_with_shutdown(
  972. work_dir,
  973. settings,
  974. shutdown_signal(),
  975. db_password,
  976. runtime,
  977. routers,
  978. )
  979. .await;
  980. // Explicitly drop the guard to ensure proper cleanup
  981. if let Some(guard) = _guard {
  982. tracing::info!("Shutting down logging worker thread");
  983. drop(guard);
  984. // Give the worker thread a moment to flush any remaining logs
  985. tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
  986. }
  987. tracing::info!("Mintd shutdown");
  988. result
  989. }
  990. /// Run mintd with a custom shutdown signal
  991. pub async fn run_mintd_with_shutdown(
  992. work_dir: &Path,
  993. settings: &config::Settings,
  994. shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
  995. db_password: Option<String>,
  996. runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
  997. routers: Vec<Router>,
  998. ) -> Result<()> {
  999. let (localstore, keystore, kv) = initial_setup(work_dir, settings, db_password.clone()).await?;
  1000. let mint_builder = MintBuilder::new(localstore);
  1001. // If RPC is enabled and DB contains mint_info already, initialize the builder from DB.
  1002. // This ensures subsequent builder modifications (like version injection) can respect stored values.
  1003. let maybe_mint_builder = {
  1004. #[cfg(feature = "management-rpc")]
  1005. {
  1006. if let Some(rpc_settings) = settings.mint_management_rpc.clone() {
  1007. if rpc_settings.enabled {
  1008. // Best-effort: pull DB state into builder if present
  1009. let mut tmp = mint_builder;
  1010. if let Err(e) = tmp.init_from_db_if_present().await {
  1011. tracing::warn!("Failed to init builder from DB: {}", e);
  1012. }
  1013. tmp
  1014. } else {
  1015. mint_builder
  1016. }
  1017. } else {
  1018. mint_builder
  1019. }
  1020. }
  1021. #[cfg(not(feature = "management-rpc"))]
  1022. {
  1023. mint_builder
  1024. }
  1025. };
  1026. let mint_builder =
  1027. configure_mint_builder(settings, maybe_mint_builder, runtime, work_dir, Some(kv)).await?;
  1028. #[cfg(feature = "auth")]
  1029. let mint_builder = setup_authentication(settings, work_dir, mint_builder, db_password).await?;
  1030. let config_mint_info = mint_builder.current_mint_info();
  1031. let mint = build_mint(settings, keystore, mint_builder).await?;
  1032. tracing::debug!("Mint built from builder.");
  1033. let mint = Arc::new(mint);
  1034. // Checks the status of all pending melt quotes
  1035. // Pending melt quotes where the payment has gone through inputs are burnt
  1036. // Pending melt quotes where the payment has **failed** inputs are reset to unspent
  1037. mint.check_pending_melt_quotes().await?;
  1038. start_services_with_shutdown(
  1039. mint.clone(),
  1040. settings,
  1041. work_dir,
  1042. config_mint_info,
  1043. shutdown_signal,
  1044. routers,
  1045. )
  1046. .await
  1047. }
  1048. #[cfg(test)]
  1049. mod tests {
  1050. use super::*;
  1051. #[test]
  1052. fn test_postgres_auth_url_validation() {
  1053. // Test that the auth database config requires explicit configuration
  1054. // Test empty URL
  1055. let auth_config = config::PostgresAuthConfig {
  1056. url: "".to_string(),
  1057. ..Default::default()
  1058. };
  1059. assert!(auth_config.url.is_empty());
  1060. // Test non-empty URL
  1061. let auth_config = config::PostgresAuthConfig {
  1062. url: "postgresql://user:password@localhost:5432/auth_db".to_string(),
  1063. ..Default::default()
  1064. };
  1065. assert!(!auth_config.url.is_empty());
  1066. }
  1067. }