lib.rs 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965
  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. // internal crate modules
  15. use cdk::cdk_database::{self, MintDatabase, MintKeysDatabase};
  16. use cdk::cdk_payment;
  17. use cdk::cdk_payment::MintPayment;
  18. use cdk::mint::{Mint, MintBuilder, MintMeltLimits};
  19. #[cfg(any(
  20. feature = "cln",
  21. feature = "lnbits",
  22. feature = "lnd",
  23. feature = "fakewallet",
  24. feature = "grpc-processor"
  25. ))]
  26. use cdk::nuts::nut17::SupportedMethods;
  27. use cdk::nuts::nut19::{CachedEndpoint, Method as NUT19Method, Path as NUT19Path};
  28. #[cfg(any(
  29. feature = "cln",
  30. feature = "lnbits",
  31. feature = "lnd",
  32. feature = "fakewallet"
  33. ))]
  34. use cdk::nuts::CurrencyUnit;
  35. #[cfg(feature = "auth")]
  36. use cdk::nuts::{AuthRequired, Method, ProtectedEndpoint, RoutePath};
  37. use cdk::nuts::{ContactInfo, MintVersion, PaymentMethod};
  38. use cdk::types::QuoteTTL;
  39. use cdk_axum::cache::HttpCache;
  40. use cdk_postgres::{MintPgAuthDatabase, MintPgDatabase};
  41. #[cfg(feature = "auth")]
  42. use cdk_sqlite::mint::MintSqliteAuthDatabase;
  43. use cdk_sqlite::MintSqliteDatabase;
  44. use cli::CLIArgs;
  45. use config::{AuthType, DatabaseEngine, LnBackend};
  46. use env_vars::ENV_WORK_DIR;
  47. use setup::LnBackendSetup;
  48. use tower::ServiceBuilder;
  49. use tower_http::compression::CompressionLayer;
  50. use tower_http::decompression::RequestDecompressionLayer;
  51. use tower_http::trace::TraceLayer;
  52. use tracing_appender::{non_blocking, rolling};
  53. use tracing_subscriber::fmt::writer::MakeWriterExt;
  54. use tracing_subscriber::EnvFilter;
  55. #[cfg(feature = "swagger")]
  56. use utoipa::OpenApi;
  57. pub mod cli;
  58. pub mod config;
  59. pub mod env_vars;
  60. pub mod setup;
  61. const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
  62. #[cfg(feature = "cln")]
  63. fn expand_path(path: &str) -> Option<PathBuf> {
  64. if path.starts_with('~') {
  65. if let Some(home_dir) = home::home_dir().as_mut() {
  66. let remainder = &path[2..];
  67. home_dir.push(remainder);
  68. let expanded_path = home_dir;
  69. Some(expanded_path.clone())
  70. } else {
  71. None
  72. }
  73. } else {
  74. Some(PathBuf::from(path))
  75. }
  76. }
  77. /// Performs the initial setup for the application, including configuring tracing,
  78. /// parsing CLI arguments, setting up the working directory, loading settings,
  79. /// and initializing the database connection.
  80. async fn initial_setup(
  81. work_dir: &Path,
  82. settings: &config::Settings,
  83. db_password: Option<String>,
  84. ) -> Result<(
  85. Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync>,
  86. Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
  87. )> {
  88. let (localstore, keystore) = setup_database(settings, work_dir, db_password).await?;
  89. Ok((localstore, keystore))
  90. }
  91. /// Sets up and initializes a tracing subscriber with custom log filtering.
  92. /// Logs can be configured to output to stdout only, file only, or both.
  93. /// Returns a guard that must be kept alive and properly dropped on shutdown.
  94. pub fn setup_tracing(
  95. work_dir: &Path,
  96. logging_config: &config::LoggingConfig,
  97. ) -> Result<Option<tracing_appender::non_blocking::WorkerGuard>> {
  98. let default_filter = "debug";
  99. let hyper_filter = "hyper=warn,rustls=warn,reqwest=warn";
  100. let h2_filter = "h2=warn";
  101. let tower_http = "tower_http=warn";
  102. let env_filter = EnvFilter::new(format!(
  103. "{default_filter},{hyper_filter},{h2_filter},{tower_http}"
  104. ));
  105. use config::LoggingOutput;
  106. match logging_config.output {
  107. LoggingOutput::Stderr => {
  108. // Console output only (stderr)
  109. let console_level = logging_config
  110. .console_level
  111. .as_deref()
  112. .unwrap_or("info")
  113. .parse::<tracing::Level>()
  114. .unwrap_or(tracing::Level::INFO);
  115. let stderr = std::io::stderr.with_max_level(console_level);
  116. tracing_subscriber::fmt()
  117. .with_env_filter(env_filter)
  118. .with_writer(stderr)
  119. .init();
  120. tracing::info!("Logging initialized: console only ({}+)", console_level);
  121. Ok(None)
  122. }
  123. LoggingOutput::File => {
  124. // File output only
  125. let file_level = logging_config
  126. .file_level
  127. .as_deref()
  128. .unwrap_or("debug")
  129. .parse::<tracing::Level>()
  130. .unwrap_or(tracing::Level::DEBUG);
  131. // Create logs directory in work_dir if it doesn't exist
  132. let logs_dir = work_dir.join("logs");
  133. std::fs::create_dir_all(&logs_dir)?;
  134. // Set up file appender with daily rotation
  135. let file_appender = rolling::daily(&logs_dir, "cdk-mintd.log");
  136. let (non_blocking_appender, guard) = non_blocking(file_appender);
  137. let file_writer = non_blocking_appender.with_max_level(file_level);
  138. tracing_subscriber::fmt()
  139. .with_env_filter(env_filter)
  140. .with_writer(file_writer)
  141. .init();
  142. tracing::info!(
  143. "Logging initialized: file only at {}/cdk-mintd.log ({}+)",
  144. logs_dir.display(),
  145. file_level
  146. );
  147. Ok(Some(guard))
  148. }
  149. LoggingOutput::Both => {
  150. // Both console and file output (stderr + file)
  151. let console_level = logging_config
  152. .console_level
  153. .as_deref()
  154. .unwrap_or("info")
  155. .parse::<tracing::Level>()
  156. .unwrap_or(tracing::Level::INFO);
  157. let file_level = logging_config
  158. .file_level
  159. .as_deref()
  160. .unwrap_or("debug")
  161. .parse::<tracing::Level>()
  162. .unwrap_or(tracing::Level::DEBUG);
  163. // Create logs directory in work_dir if it doesn't exist
  164. let logs_dir = work_dir.join("logs");
  165. std::fs::create_dir_all(&logs_dir)?;
  166. // Set up file appender with daily rotation
  167. let file_appender = rolling::daily(&logs_dir, "cdk-mintd.log");
  168. let (non_blocking_appender, guard) = non_blocking(file_appender);
  169. // Combine console output (stderr) and file output
  170. let stderr = std::io::stderr.with_max_level(console_level);
  171. let file_writer = non_blocking_appender.with_max_level(file_level);
  172. tracing_subscriber::fmt()
  173. .with_env_filter(env_filter)
  174. .with_writer(stderr.and(file_writer))
  175. .init();
  176. tracing::info!(
  177. "Logging initialized: console ({}+) and file at {}/cdk-mintd.log ({}+)",
  178. console_level,
  179. logs_dir.display(),
  180. file_level
  181. );
  182. Ok(Some(guard))
  183. }
  184. }
  185. }
  186. /// Retrieves the work directory based on command-line arguments, environment variables, or system defaults.
  187. pub async fn get_work_directory(args: &CLIArgs) -> Result<PathBuf> {
  188. let work_dir = if let Some(work_dir) = &args.work_dir {
  189. tracing::info!("Using work dir from cmd arg");
  190. work_dir.clone()
  191. } else if let Ok(env_work_dir) = env::var(ENV_WORK_DIR) {
  192. tracing::info!("Using work dir from env var");
  193. env_work_dir.into()
  194. } else {
  195. work_dir()?
  196. };
  197. tracing::info!("Using work dir: {}", work_dir.display());
  198. Ok(work_dir)
  199. }
  200. /// Loads the application settings based on a configuration file and environment variables.
  201. pub fn load_settings(work_dir: &Path, config_path: Option<PathBuf>) -> Result<config::Settings> {
  202. // get config file name from args
  203. let config_file_arg = match config_path {
  204. Some(c) => c,
  205. None => work_dir.join("config.toml"),
  206. };
  207. let mut settings = if config_file_arg.exists() {
  208. config::Settings::new(Some(config_file_arg))
  209. } else {
  210. tracing::info!("Config file does not exist. Attempting to read env vars");
  211. config::Settings::default()
  212. };
  213. // This check for any settings defined in ENV VARs
  214. // ENV VARS will take **priority** over those in the config
  215. settings.from_env()
  216. }
  217. async fn setup_database(
  218. settings: &config::Settings,
  219. work_dir: &Path,
  220. db_password: Option<String>,
  221. ) -> Result<(
  222. Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync>,
  223. Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
  224. )> {
  225. match settings.database.engine {
  226. DatabaseEngine::Sqlite => {
  227. let db = setup_sqlite_database(work_dir, db_password).await?;
  228. let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> = db.clone();
  229. let keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync> = db;
  230. Ok((localstore, keystore))
  231. }
  232. DatabaseEngine::Postgres => {
  233. // Get the PostgreSQL configuration, ensuring it exists
  234. let pg_config = settings.database.postgres.as_ref().ok_or_else(|| {
  235. anyhow!("PostgreSQL configuration is required when using PostgreSQL engine")
  236. })?;
  237. if pg_config.url.is_empty() {
  238. 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");
  239. }
  240. let pg_db = Arc::new(MintPgDatabase::new(pg_config.url.as_str()).await?);
  241. let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> =
  242. pg_db.clone();
  243. let keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync> =
  244. pg_db;
  245. Ok((localstore, keystore))
  246. }
  247. }
  248. }
  249. async fn setup_sqlite_database(
  250. work_dir: &Path,
  251. _password: Option<String>,
  252. ) -> Result<Arc<MintSqliteDatabase>> {
  253. let sql_db_path = work_dir.join("cdk-mintd.sqlite");
  254. #[cfg(not(feature = "sqlcipher"))]
  255. let db = MintSqliteDatabase::new(&sql_db_path).await?;
  256. #[cfg(feature = "sqlcipher")]
  257. let db = {
  258. // Get password from command line arguments for sqlcipher
  259. MintSqliteDatabase::new((sql_db_path, _password.unwrap())).await?
  260. };
  261. Ok(Arc::new(db))
  262. }
  263. /**
  264. * Configures a `MintBuilder` instance with provided settings and initializes
  265. * routers for Lightning Network backends.
  266. */
  267. async fn configure_mint_builder(
  268. settings: &config::Settings,
  269. mint_builder: MintBuilder,
  270. ) -> Result<(MintBuilder, Vec<Router>)> {
  271. let mut ln_routers = vec![];
  272. // Configure basic mint information
  273. let mint_builder = configure_basic_info(settings, mint_builder);
  274. // Configure lightning backend
  275. let mint_builder = configure_lightning_backend(settings, mint_builder, &mut ln_routers).await?;
  276. // Configure caching
  277. let mint_builder = configure_cache(settings, mint_builder);
  278. Ok((mint_builder, ln_routers))
  279. }
  280. /// Configures basic mint information (name, contact info, descriptions, etc.)
  281. fn configure_basic_info(settings: &config::Settings, mint_builder: MintBuilder) -> MintBuilder {
  282. // Add contact information
  283. let mut contacts = Vec::new();
  284. if let Some(nostr_key) = &settings.mint_info.contact_nostr_public_key {
  285. contacts.push(ContactInfo::new("nostr".to_string(), nostr_key.to_string()));
  286. }
  287. if let Some(email) = &settings.mint_info.contact_email {
  288. contacts.push(ContactInfo::new("email".to_string(), email.to_string()));
  289. }
  290. // Add version information
  291. let mint_version = MintVersion::new(
  292. "cdk-mintd".to_string(),
  293. CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
  294. );
  295. // Configure mint builder with basic info
  296. let mut builder = mint_builder
  297. .with_name(settings.mint_info.name.clone())
  298. .with_version(mint_version)
  299. .with_description(settings.mint_info.description.clone());
  300. // Add optional information
  301. if let Some(long_description) = &settings.mint_info.description_long {
  302. builder = builder.with_long_description(long_description.to_string());
  303. }
  304. for contact in contacts {
  305. builder = builder.with_contact_info(contact);
  306. }
  307. if let Some(pubkey) = settings.mint_info.pubkey {
  308. builder = builder.with_pubkey(pubkey);
  309. }
  310. if let Some(icon_url) = &settings.mint_info.icon_url {
  311. builder = builder.with_icon_url(icon_url.to_string());
  312. }
  313. if let Some(motd) = &settings.mint_info.motd {
  314. builder = builder.with_motd(motd.to_string());
  315. }
  316. if let Some(tos_url) = &settings.mint_info.tos_url {
  317. builder = builder.with_tos_url(tos_url.to_string());
  318. }
  319. builder
  320. }
  321. /// Configures Lightning Network backend based on the specified backend type
  322. async fn configure_lightning_backend(
  323. settings: &config::Settings,
  324. mut mint_builder: MintBuilder,
  325. ln_routers: &mut Vec<Router>,
  326. ) -> Result<MintBuilder> {
  327. let mint_melt_limits = MintMeltLimits {
  328. mint_min: settings.ln.min_mint,
  329. mint_max: settings.ln.max_mint,
  330. melt_min: settings.ln.min_melt,
  331. melt_max: settings.ln.max_melt,
  332. };
  333. tracing::debug!("Ln backend: {:?}", settings.ln.ln_backend);
  334. match settings.ln.ln_backend {
  335. #[cfg(feature = "cln")]
  336. LnBackend::Cln => {
  337. let cln_settings = settings
  338. .cln
  339. .clone()
  340. .expect("Config checked at load that cln is some");
  341. let cln = cln_settings
  342. .setup(ln_routers, settings, CurrencyUnit::Msat)
  343. .await?;
  344. mint_builder = configure_backend_for_unit(
  345. settings,
  346. mint_builder,
  347. CurrencyUnit::Sat,
  348. mint_melt_limits,
  349. Arc::new(cln),
  350. )
  351. .await?;
  352. }
  353. #[cfg(feature = "lnbits")]
  354. LnBackend::LNbits => {
  355. let lnbits_settings = settings.clone().lnbits.expect("Checked on config load");
  356. let lnbits = lnbits_settings
  357. .setup(ln_routers, settings, CurrencyUnit::Sat)
  358. .await?;
  359. mint_builder = configure_backend_for_unit(
  360. settings,
  361. mint_builder,
  362. CurrencyUnit::Sat,
  363. mint_melt_limits,
  364. Arc::new(lnbits),
  365. )
  366. .await?;
  367. }
  368. #[cfg(feature = "lnd")]
  369. LnBackend::Lnd => {
  370. let lnd_settings = settings.clone().lnd.expect("Checked at config load");
  371. let lnd = lnd_settings
  372. .setup(ln_routers, settings, CurrencyUnit::Msat)
  373. .await?;
  374. mint_builder = configure_backend_for_unit(
  375. settings,
  376. mint_builder,
  377. CurrencyUnit::Sat,
  378. mint_melt_limits,
  379. Arc::new(lnd),
  380. )
  381. .await?;
  382. }
  383. #[cfg(feature = "fakewallet")]
  384. LnBackend::FakeWallet => {
  385. let fake_wallet = settings.clone().fake_wallet.expect("Fake wallet defined");
  386. tracing::info!("Using fake wallet: {:?}", fake_wallet);
  387. for unit in fake_wallet.clone().supported_units {
  388. let fake = fake_wallet
  389. .setup(ln_routers, settings, unit.clone())
  390. .await?;
  391. mint_builder = configure_backend_for_unit(
  392. settings,
  393. mint_builder,
  394. unit.clone(),
  395. mint_melt_limits,
  396. Arc::new(fake),
  397. )
  398. .await?;
  399. }
  400. }
  401. #[cfg(feature = "grpc-processor")]
  402. LnBackend::GrpcProcessor => {
  403. let grpc_processor = settings
  404. .clone()
  405. .grpc_processor
  406. .expect("grpc processor config defined");
  407. tracing::info!(
  408. "Attempting to start with gRPC payment processor at {}:{}.",
  409. grpc_processor.addr,
  410. grpc_processor.port
  411. );
  412. for unit in grpc_processor.clone().supported_units {
  413. tracing::debug!("Adding unit: {:?}", unit);
  414. let processor = grpc_processor
  415. .setup(ln_routers, settings, unit.clone())
  416. .await?;
  417. mint_builder = configure_backend_for_unit(
  418. settings,
  419. mint_builder,
  420. unit.clone(),
  421. mint_melt_limits,
  422. Arc::new(processor),
  423. )
  424. .await?;
  425. }
  426. }
  427. LnBackend::None => {
  428. tracing::error!(
  429. "Payment backend was not set or feature disabled. {:?}",
  430. settings.ln.ln_backend
  431. );
  432. bail!("Lightning backend must be configured");
  433. }
  434. };
  435. Ok(mint_builder)
  436. }
  437. /// Helper function to configure a mint builder with a lightning backend for a specific currency unit
  438. async fn configure_backend_for_unit(
  439. settings: &config::Settings,
  440. mut mint_builder: MintBuilder,
  441. unit: cdk::nuts::CurrencyUnit,
  442. mint_melt_limits: MintMeltLimits,
  443. backend: Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>,
  444. ) -> Result<MintBuilder> {
  445. let payment_settings = backend.get_settings().await?;
  446. if let Some(bolt12) = payment_settings.get("bolt12") {
  447. if bolt12.as_bool().unwrap_or_default() {
  448. mint_builder
  449. .add_payment_processor(
  450. unit.clone(),
  451. PaymentMethod::Bolt12,
  452. mint_melt_limits,
  453. Arc::clone(&backend),
  454. )
  455. .await?;
  456. }
  457. }
  458. mint_builder
  459. .add_payment_processor(
  460. unit.clone(),
  461. PaymentMethod::Bolt11,
  462. mint_melt_limits,
  463. backend,
  464. )
  465. .await?;
  466. if let Some(input_fee) = settings.info.input_fee_ppk {
  467. mint_builder.set_unit_fee(&unit, input_fee)?;
  468. }
  469. let nut17_supported = SupportedMethods::default_bolt11(unit);
  470. mint_builder = mint_builder.with_supported_websockets(nut17_supported);
  471. Ok(mint_builder)
  472. }
  473. /// Configures cache settings
  474. fn configure_cache(settings: &config::Settings, mint_builder: MintBuilder) -> MintBuilder {
  475. let cached_endpoints = vec![
  476. CachedEndpoint::new(NUT19Method::Post, NUT19Path::MintBolt11),
  477. CachedEndpoint::new(NUT19Method::Post, NUT19Path::MeltBolt11),
  478. CachedEndpoint::new(NUT19Method::Post, NUT19Path::Swap),
  479. ];
  480. let cache: HttpCache = settings.info.http_cache.clone().into();
  481. mint_builder.with_cache(Some(cache.ttl.as_secs()), cached_endpoints)
  482. }
  483. #[cfg(feature = "auth")]
  484. async fn setup_authentication(
  485. settings: &config::Settings,
  486. work_dir: &Path,
  487. mut mint_builder: MintBuilder,
  488. _password: Option<String>,
  489. ) -> Result<MintBuilder> {
  490. if let Some(auth_settings) = settings.auth.clone() {
  491. tracing::info!("Auth settings are defined. {:?}", auth_settings);
  492. let auth_localstore: Arc<
  493. dyn cdk_database::MintAuthDatabase<Err = cdk_database::Error> + Send + Sync,
  494. > = match settings.database.engine {
  495. DatabaseEngine::Sqlite => {
  496. let sql_db_path = work_dir.join("cdk-mintd-auth.sqlite");
  497. #[cfg(not(feature = "sqlcipher"))]
  498. let sqlite_db = MintSqliteAuthDatabase::new(&sql_db_path).await?;
  499. #[cfg(feature = "sqlcipher")]
  500. let sqlite_db = {
  501. // Get password from command line arguments for sqlcipher
  502. MintSqliteAuthDatabase::new((sql_db_path, _password.unwrap())).await?
  503. };
  504. Arc::new(sqlite_db)
  505. }
  506. DatabaseEngine::Postgres => {
  507. // Get the PostgreSQL configuration, ensuring it exists
  508. let pg_config = settings.database.postgres.as_ref().ok_or_else(|| {
  509. anyhow!("PostgreSQL configuration is required when using PostgreSQL engine")
  510. })?;
  511. if pg_config.url.is_empty() {
  512. bail!("PostgreSQL URL is required for auth database. Set it in config file [database.postgres] section or via CDK_MINTD_POSTGRES_URL/CDK_MINTD_DATABASE_URL environment variable");
  513. }
  514. Arc::new(MintPgAuthDatabase::new(pg_config.url.as_str()).await?)
  515. }
  516. };
  517. let mut protected_endpoints = HashMap::new();
  518. let mut blind_auth_endpoints = vec![];
  519. let mut clear_auth_endpoints = vec![];
  520. let mut unprotected_endpoints = vec![];
  521. let mint_blind_auth_endpoint =
  522. ProtectedEndpoint::new(Method::Post, RoutePath::MintBlindAuth);
  523. protected_endpoints.insert(mint_blind_auth_endpoint, AuthRequired::Clear);
  524. clear_auth_endpoints.push(mint_blind_auth_endpoint);
  525. // Helper function to add endpoint based on auth type
  526. let mut add_endpoint = |endpoint: ProtectedEndpoint, auth_type: &AuthType| {
  527. match auth_type {
  528. AuthType::Blind => {
  529. protected_endpoints.insert(endpoint, AuthRequired::Blind);
  530. blind_auth_endpoints.push(endpoint);
  531. }
  532. AuthType::Clear => {
  533. protected_endpoints.insert(endpoint, AuthRequired::Clear);
  534. clear_auth_endpoints.push(endpoint);
  535. }
  536. AuthType::None => {
  537. unprotected_endpoints.push(endpoint);
  538. }
  539. };
  540. };
  541. // Get mint quote endpoint
  542. {
  543. let mint_quote_protected_endpoint =
  544. ProtectedEndpoint::new(cdk::nuts::Method::Post, RoutePath::MintQuoteBolt11);
  545. add_endpoint(mint_quote_protected_endpoint, &auth_settings.get_mint_quote);
  546. }
  547. // Check mint quote endpoint
  548. {
  549. let check_mint_protected_endpoint =
  550. ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11);
  551. add_endpoint(
  552. check_mint_protected_endpoint,
  553. &auth_settings.check_mint_quote,
  554. );
  555. }
  556. // Mint endpoint
  557. {
  558. let mint_protected_endpoint =
  559. ProtectedEndpoint::new(cdk::nuts::Method::Post, RoutePath::MintBolt11);
  560. add_endpoint(mint_protected_endpoint, &auth_settings.mint);
  561. }
  562. // Get melt quote endpoint
  563. {
  564. let melt_quote_protected_endpoint = ProtectedEndpoint::new(
  565. cdk::nuts::Method::Post,
  566. cdk::nuts::RoutePath::MeltQuoteBolt11,
  567. );
  568. add_endpoint(melt_quote_protected_endpoint, &auth_settings.get_melt_quote);
  569. }
  570. // Check melt quote endpoint
  571. {
  572. let check_melt_protected_endpoint =
  573. ProtectedEndpoint::new(Method::Get, RoutePath::MeltQuoteBolt11);
  574. add_endpoint(
  575. check_melt_protected_endpoint,
  576. &auth_settings.check_melt_quote,
  577. );
  578. }
  579. // Melt endpoint
  580. {
  581. let melt_protected_endpoint =
  582. ProtectedEndpoint::new(Method::Post, RoutePath::MeltBolt11);
  583. add_endpoint(melt_protected_endpoint, &auth_settings.melt);
  584. }
  585. // Swap endpoint
  586. {
  587. let swap_protected_endpoint = ProtectedEndpoint::new(Method::Post, RoutePath::Swap);
  588. add_endpoint(swap_protected_endpoint, &auth_settings.swap);
  589. }
  590. // Restore endpoint
  591. {
  592. let restore_protected_endpoint =
  593. ProtectedEndpoint::new(Method::Post, RoutePath::Restore);
  594. add_endpoint(restore_protected_endpoint, &auth_settings.restore);
  595. }
  596. // Check proof state endpoint
  597. {
  598. let state_protected_endpoint =
  599. ProtectedEndpoint::new(Method::Post, RoutePath::Checkstate);
  600. add_endpoint(state_protected_endpoint, &auth_settings.check_proof_state);
  601. }
  602. mint_builder = mint_builder.with_auth(
  603. auth_localstore.clone(),
  604. auth_settings.openid_discovery,
  605. auth_settings.openid_client_id,
  606. clear_auth_endpoints,
  607. );
  608. mint_builder =
  609. mint_builder.with_blind_auth(auth_settings.mint_max_bat, blind_auth_endpoints);
  610. let mut tx = auth_localstore.begin_transaction().await?;
  611. tx.remove_protected_endpoints(unprotected_endpoints).await?;
  612. tx.add_protected_endpoints(protected_endpoints).await?;
  613. tx.commit().await?;
  614. }
  615. Ok(mint_builder)
  616. }
  617. /// Build mints with the configured the signing method (remote signatory or local seed)
  618. async fn build_mint(
  619. settings: &config::Settings,
  620. keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
  621. mint_builder: MintBuilder,
  622. ) -> Result<Mint> {
  623. if let Some(signatory_url) = settings.info.signatory_url.clone() {
  624. tracing::info!(
  625. "Connecting to remote signatory to {} with certs {:?}",
  626. signatory_url,
  627. settings.info.signatory_certs.clone()
  628. );
  629. Ok(mint_builder
  630. .build_with_signatory(Arc::new(
  631. cdk_signatory::SignatoryRpcClient::new(
  632. signatory_url,
  633. settings.info.signatory_certs.clone(),
  634. )
  635. .await?,
  636. ))
  637. .await?)
  638. } else if let Some(mnemonic) = settings
  639. .info
  640. .mnemonic
  641. .clone()
  642. .map(|s| Mnemonic::from_str(&s))
  643. .transpose()?
  644. {
  645. Ok(mint_builder
  646. .build_with_seed(keystore, &mnemonic.to_seed_normalized(""))
  647. .await?)
  648. } else {
  649. bail!("No seed nor remote signatory set");
  650. }
  651. }
  652. async fn start_services_with_shutdown(
  653. mint: Arc<cdk::mint::Mint>,
  654. settings: &config::Settings,
  655. ln_routers: Vec<Router>,
  656. work_dir: &Path,
  657. mint_builder_info: cdk::nuts::MintInfo,
  658. shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
  659. ) -> Result<()> {
  660. let listen_addr = settings.info.listen_host.clone();
  661. let listen_port = settings.info.listen_port;
  662. let cache: HttpCache = settings.info.http_cache.clone().into();
  663. #[cfg(feature = "management-rpc")]
  664. let mut rpc_enabled = false;
  665. #[cfg(not(feature = "management-rpc"))]
  666. let rpc_enabled = false;
  667. #[cfg(feature = "management-rpc")]
  668. let mut rpc_server: Option<cdk_mint_rpc::MintRPCServer> = None;
  669. #[cfg(feature = "management-rpc")]
  670. {
  671. if let Some(rpc_settings) = settings.mint_management_rpc.clone() {
  672. if rpc_settings.enabled {
  673. let addr = rpc_settings.address.unwrap_or("127.0.0.1".to_string());
  674. let port = rpc_settings.port.unwrap_or(8086);
  675. let mut mint_rpc = cdk_mint_rpc::MintRPCServer::new(&addr, port, mint.clone())?;
  676. let tls_dir = rpc_settings.tls_dir_path.unwrap_or(work_dir.join("tls"));
  677. if !tls_dir.exists() {
  678. tracing::error!("TLS directory does not exist: {}", tls_dir.display());
  679. bail!("Cannot start RPC server: TLS directory does not exist");
  680. }
  681. mint_rpc.start(Some(tls_dir)).await?;
  682. rpc_server = Some(mint_rpc);
  683. rpc_enabled = true;
  684. }
  685. }
  686. }
  687. if rpc_enabled {
  688. if mint.mint_info().await.is_err() {
  689. tracing::info!("Mint info not set on mint, setting.");
  690. mint.set_mint_info(mint_builder_info).await?;
  691. mint.set_quote_ttl(QuoteTTL::new(10_000, 10_000)).await?;
  692. } else {
  693. if mint.localstore().get_quote_ttl().await.is_err() {
  694. mint.set_quote_ttl(QuoteTTL::new(10_000, 10_000)).await?;
  695. }
  696. // Add version information
  697. let mint_version = MintVersion::new(
  698. "cdk-mintd".to_string(),
  699. CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
  700. );
  701. let mut stored_mint_info = mint.mint_info().await?;
  702. stored_mint_info.version = Some(mint_version);
  703. mint.set_mint_info(stored_mint_info).await?;
  704. tracing::info!("Mint info already set, not using config file settings.");
  705. }
  706. } else {
  707. tracing::info!("RPC not enabled, using mint info from config.");
  708. mint.set_mint_info(mint_builder_info).await?;
  709. mint.set_quote_ttl(QuoteTTL::new(10_000, 10_000)).await?;
  710. }
  711. let mint_info = mint.mint_info().await?;
  712. let nut04_methods = mint_info.nuts.nut04.supported_methods();
  713. let nut05_methods = mint_info.nuts.nut05.supported_methods();
  714. let bolt12_supported = nut04_methods.contains(&&PaymentMethod::Bolt12)
  715. || nut05_methods.contains(&&PaymentMethod::Bolt12);
  716. let v1_service =
  717. cdk_axum::create_mint_router_with_custom_cache(Arc::clone(&mint), cache, bolt12_supported)
  718. .await?;
  719. let mut mint_service = Router::new()
  720. .merge(v1_service)
  721. .layer(
  722. ServiceBuilder::new()
  723. .layer(RequestDecompressionLayer::new())
  724. .layer(CompressionLayer::new()),
  725. )
  726. .layer(TraceLayer::new_for_http());
  727. #[cfg(feature = "swagger")]
  728. {
  729. if settings.info.enable_swagger_ui.unwrap_or(false) {
  730. mint_service = mint_service.merge(
  731. utoipa_swagger_ui::SwaggerUi::new("/swagger-ui")
  732. .url("/api-docs/openapi.json", cdk_axum::ApiDoc::openapi()),
  733. );
  734. }
  735. }
  736. for router in ln_routers {
  737. mint_service = mint_service.merge(router);
  738. }
  739. mint.start().await?;
  740. let socket_addr = SocketAddr::from_str(&format!("{listen_addr}:{listen_port}"))?;
  741. let listener = tokio::net::TcpListener::bind(socket_addr).await?;
  742. tracing::info!("listening on {}", listener.local_addr().unwrap());
  743. // Wait for axum server to complete with custom shutdown signal
  744. let axum_result = axum::serve(listener, mint_service).with_graceful_shutdown(shutdown_signal);
  745. match axum_result.await {
  746. Ok(_) => {
  747. tracing::info!("Axum server stopped with okay status");
  748. }
  749. Err(err) => {
  750. tracing::warn!("Axum server stopped with error");
  751. tracing::error!("{}", err);
  752. bail!("Axum exited with error")
  753. }
  754. }
  755. mint.stop().await?;
  756. #[cfg(feature = "management-rpc")]
  757. {
  758. if let Some(rpc_server) = rpc_server {
  759. rpc_server.stop().await?;
  760. }
  761. }
  762. Ok(())
  763. }
  764. async fn shutdown_signal() {
  765. tokio::signal::ctrl_c()
  766. .await
  767. .expect("failed to install CTRL+C handler");
  768. tracing::info!("Shutdown signal received");
  769. }
  770. fn work_dir() -> Result<PathBuf> {
  771. let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?;
  772. let dir = home_dir.join(".cdk-mintd");
  773. std::fs::create_dir_all(&dir)?;
  774. Ok(dir)
  775. }
  776. /// The main entry point for the application when used as a library
  777. pub async fn run_mintd(
  778. work_dir: &Path,
  779. settings: &config::Settings,
  780. db_password: Option<String>,
  781. enable_logging: bool,
  782. ) -> Result<()> {
  783. let _guard = if enable_logging {
  784. setup_tracing(work_dir, &settings.info.logging)?
  785. } else {
  786. None
  787. };
  788. let result = run_mintd_with_shutdown(work_dir, settings, shutdown_signal(), db_password).await;
  789. // Explicitly drop the guard to ensure proper cleanup
  790. if let Some(guard) = _guard {
  791. tracing::info!("Shutting down logging worker thread");
  792. drop(guard);
  793. // Give the worker thread a moment to flush any remaining logs
  794. tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
  795. }
  796. tracing::info!("Mintd shutdown");
  797. result
  798. }
  799. /// Run mintd with a custom shutdown signal
  800. pub async fn run_mintd_with_shutdown(
  801. work_dir: &Path,
  802. settings: &config::Settings,
  803. shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
  804. db_password: Option<String>,
  805. ) -> Result<()> {
  806. let (localstore, keystore) = initial_setup(work_dir, settings, db_password.clone()).await?;
  807. let mint_builder = MintBuilder::new(localstore);
  808. let (mint_builder, ln_routers) = configure_mint_builder(settings, mint_builder).await?;
  809. #[cfg(feature = "auth")]
  810. let mint_builder = setup_authentication(settings, work_dir, mint_builder, db_password).await?;
  811. let mint = build_mint(settings, keystore, mint_builder).await?;
  812. tracing::debug!("Mint built from builder.");
  813. let mint = Arc::new(mint);
  814. // Checks the status of all pending melt quotes
  815. // Pending melt quotes where the payment has gone through inputs are burnt
  816. // Pending melt quotes where the payment has **failed** inputs are reset to unspent
  817. mint.check_pending_melt_quotes().await?;
  818. let result = start_services_with_shutdown(
  819. mint.clone(),
  820. settings,
  821. ln_routers,
  822. work_dir,
  823. mint.mint_info().await?,
  824. shutdown_signal,
  825. )
  826. .await;
  827. // Ensure any remaining tracing data is flushed
  828. // This is particularly important for file-based logging
  829. tracing::debug!("Flushing remaining trace data");
  830. result
  831. }