main.rs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. //! CDK Mint Server
  2. #![warn(missing_docs)]
  3. #![warn(rustdoc::bare_urls)]
  4. use std::env;
  5. use std::net::SocketAddr;
  6. use std::path::PathBuf;
  7. use std::str::FromStr;
  8. use std::sync::Arc;
  9. use anyhow::{anyhow, bail, Result};
  10. use axum::Router;
  11. use bip39::Mnemonic;
  12. use cdk::cdk_database::{self, MintDatabase};
  13. use cdk::mint::{MintBuilder, MintMeltLimits};
  14. // Feature-gated imports
  15. #[cfg(any(
  16. feature = "cln",
  17. feature = "lnbits",
  18. feature = "lnd",
  19. feature = "fakewallet",
  20. feature = "grpc-processor"
  21. ))]
  22. use cdk::nuts::nut17::SupportedMethods;
  23. use cdk::nuts::nut19::{CachedEndpoint, Method as NUT19Method, Path as NUT19Path};
  24. #[cfg(any(
  25. feature = "cln",
  26. feature = "lnbits",
  27. feature = "lnd",
  28. feature = "fakewallet"
  29. ))]
  30. use cdk::nuts::CurrencyUnit;
  31. use cdk::nuts::{ContactInfo, MintVersion, PaymentMethod};
  32. use cdk::types::QuoteTTL;
  33. use cdk_axum::cache::HttpCache;
  34. #[cfg(feature = "management-rpc")]
  35. use cdk_mint_rpc::MintRPCServer;
  36. use cdk_mintd::cli::CLIArgs;
  37. use cdk_mintd::config::{self, DatabaseEngine, LnBackend};
  38. use cdk_mintd::env_vars::ENV_WORK_DIR;
  39. use cdk_mintd::setup::LnBackendSetup;
  40. #[cfg(feature = "redb")]
  41. use cdk_redb::MintRedbDatabase;
  42. use cdk_sqlite::MintSqliteDatabase;
  43. use clap::Parser;
  44. use tokio::sync::Notify;
  45. use tower::ServiceBuilder;
  46. use tower_http::compression::CompressionLayer;
  47. use tower_http::decompression::RequestDecompressionLayer;
  48. use tower_http::trace::TraceLayer;
  49. use tracing_subscriber::EnvFilter;
  50. #[cfg(feature = "swagger")]
  51. use utoipa::OpenApi;
  52. const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
  53. // Ensure at least one lightning backend is enabled at compile time
  54. #[cfg(not(any(
  55. feature = "cln",
  56. feature = "lnbits",
  57. feature = "lnd",
  58. feature = "fakewallet",
  59. feature = "grpc-processor"
  60. )))]
  61. compile_error!(
  62. "At least one lightning backend feature must be enabled: cln, lnbits, lnd, fakewallet, or grpc-processor"
  63. );
  64. #[tokio::main]
  65. async fn main() -> anyhow::Result<()> {
  66. let default_filter = "debug";
  67. let sqlx_filter = "sqlx=warn";
  68. let hyper_filter = "hyper=warn";
  69. let h2_filter = "h2=warn";
  70. let tower_http = "tower_http=warn";
  71. let env_filter = EnvFilter::new(format!(
  72. "{},{},{},{},{}",
  73. default_filter, sqlx_filter, hyper_filter, h2_filter, tower_http
  74. ));
  75. tracing_subscriber::fmt().with_env_filter(env_filter).init();
  76. let args = CLIArgs::parse();
  77. let work_dir = if let Some(work_dir) = args.work_dir {
  78. tracing::info!("Using work dir from cmd arg");
  79. work_dir
  80. } else if let Ok(env_work_dir) = env::var(ENV_WORK_DIR) {
  81. tracing::info!("Using work dir from env var");
  82. env_work_dir.into()
  83. } else {
  84. work_dir()?
  85. };
  86. tracing::info!("Using work dir: {}", work_dir.display());
  87. // get config file name from args
  88. let config_file_arg = match args.config {
  89. Some(c) => c,
  90. None => work_dir.join("config.toml"),
  91. };
  92. let mut mint_builder = MintBuilder::new();
  93. let mut settings = if config_file_arg.exists() {
  94. config::Settings::new(Some(config_file_arg))
  95. } else {
  96. tracing::info!("Config file does not exist. Attempting to read env vars");
  97. config::Settings::default()
  98. };
  99. // This check for any settings defined in ENV VARs
  100. // ENV VARS will take **priority** over those in the config
  101. let settings = settings.from_env()?;
  102. let localstore: Arc<dyn MintDatabase<Err = cdk_database::Error> + Send + Sync> =
  103. match settings.database.engine {
  104. DatabaseEngine::Sqlite => {
  105. let sql_db_path = work_dir.join("cdk-mintd.sqlite");
  106. #[cfg(not(feature = "sqlcipher"))]
  107. let sqlite_db = MintSqliteDatabase::new(&sql_db_path).await?;
  108. #[cfg(feature = "sqlcipher")]
  109. let sqlite_db = MintSqliteDatabase::new(&sql_db_path, args.password).await?;
  110. sqlite_db.migrate().await;
  111. Arc::new(sqlite_db)
  112. }
  113. #[cfg(feature = "redb")]
  114. DatabaseEngine::Redb => {
  115. let redb_path = work_dir.join("cdk-mintd.redb");
  116. Arc::new(MintRedbDatabase::new(&redb_path)?)
  117. }
  118. };
  119. mint_builder = mint_builder.with_localstore(localstore);
  120. let mut contact_info: Option<Vec<ContactInfo>> = None;
  121. if let Some(nostr_contact) = &settings.mint_info.contact_nostr_public_key {
  122. let nostr_contact = ContactInfo::new("nostr".to_string(), nostr_contact.to_string());
  123. contact_info = match contact_info {
  124. Some(mut vec) => {
  125. vec.push(nostr_contact);
  126. Some(vec)
  127. }
  128. None => Some(vec![nostr_contact]),
  129. };
  130. }
  131. if let Some(email_contact) = &settings.mint_info.contact_email {
  132. let email_contact = ContactInfo::new("email".to_string(), email_contact.to_string());
  133. contact_info = match contact_info {
  134. Some(mut vec) => {
  135. vec.push(email_contact);
  136. Some(vec)
  137. }
  138. None => Some(vec![email_contact]),
  139. };
  140. }
  141. let mint_version = MintVersion::new(
  142. "cdk-mintd".to_string(),
  143. CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
  144. );
  145. let mut ln_routers = vec![];
  146. let mint_melt_limits = MintMeltLimits {
  147. mint_min: settings.ln.min_mint,
  148. mint_max: settings.ln.max_mint,
  149. melt_min: settings.ln.min_melt,
  150. melt_max: settings.ln.max_melt,
  151. };
  152. tracing::debug!("Ln backendd: {:?}", settings.ln.ln_backend);
  153. match settings.ln.ln_backend {
  154. #[cfg(feature = "cln")]
  155. LnBackend::Cln => {
  156. let cln_settings = settings
  157. .cln
  158. .clone()
  159. .expect("Config checked at load that cln is some");
  160. let cln = cln_settings
  161. .setup(&mut ln_routers, &settings, CurrencyUnit::Msat)
  162. .await?;
  163. let cln = Arc::new(cln);
  164. mint_builder = mint_builder
  165. .add_ln_backend(
  166. CurrencyUnit::Sat,
  167. PaymentMethod::Bolt11,
  168. mint_melt_limits,
  169. cln.clone(),
  170. )
  171. .await?;
  172. let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, CurrencyUnit::Sat);
  173. mint_builder = mint_builder.add_supported_websockets(nut17_supported);
  174. }
  175. #[cfg(feature = "lnbits")]
  176. LnBackend::LNbits => {
  177. let lnbits_settings = settings.clone().lnbits.expect("Checked on config load");
  178. let lnbits = lnbits_settings
  179. .setup(&mut ln_routers, &settings, CurrencyUnit::Sat)
  180. .await?;
  181. mint_builder = mint_builder
  182. .add_ln_backend(
  183. CurrencyUnit::Sat,
  184. PaymentMethod::Bolt11,
  185. mint_melt_limits,
  186. Arc::new(lnbits),
  187. )
  188. .await?;
  189. let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, CurrencyUnit::Sat);
  190. mint_builder = mint_builder.add_supported_websockets(nut17_supported);
  191. }
  192. #[cfg(feature = "lnd")]
  193. LnBackend::Lnd => {
  194. let lnd_settings = settings.clone().lnd.expect("Checked at config load");
  195. let lnd = lnd_settings
  196. .setup(&mut ln_routers, &settings, CurrencyUnit::Msat)
  197. .await?;
  198. mint_builder = mint_builder
  199. .add_ln_backend(
  200. CurrencyUnit::Sat,
  201. PaymentMethod::Bolt11,
  202. mint_melt_limits,
  203. Arc::new(lnd),
  204. )
  205. .await?;
  206. let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, CurrencyUnit::Sat);
  207. mint_builder = mint_builder.add_supported_websockets(nut17_supported);
  208. }
  209. #[cfg(feature = "fakewallet")]
  210. LnBackend::FakeWallet => {
  211. let fake_wallet = settings.clone().fake_wallet.expect("Fake wallet defined");
  212. tracing::info!("Using fake wallet: {:?}", fake_wallet);
  213. for unit in fake_wallet.clone().supported_units {
  214. let fake = fake_wallet
  215. .setup(&mut ln_routers, &settings, CurrencyUnit::Sat)
  216. .await
  217. .expect("hhh");
  218. let fake = Arc::new(fake);
  219. mint_builder = mint_builder
  220. .add_ln_backend(
  221. unit.clone(),
  222. PaymentMethod::Bolt11,
  223. mint_melt_limits,
  224. fake.clone(),
  225. )
  226. .await?;
  227. let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, unit);
  228. mint_builder = mint_builder.add_supported_websockets(nut17_supported);
  229. }
  230. }
  231. #[cfg(feature = "grpc-processor")]
  232. LnBackend::GrpcProcessor => {
  233. let grpc_processor = settings
  234. .clone()
  235. .grpc_processor
  236. .expect("grpc processor config defined");
  237. tracing::info!(
  238. "Attempting to start with gRPC payment processor at {}:{}.",
  239. grpc_processor.addr,
  240. grpc_processor.port
  241. );
  242. tracing::info!("{:?}", grpc_processor);
  243. for unit in grpc_processor.clone().supported_units {
  244. tracing::debug!("Adding unit: {:?}", unit);
  245. let processor = grpc_processor
  246. .setup(&mut ln_routers, &settings, unit.clone())
  247. .await?;
  248. mint_builder = mint_builder
  249. .add_ln_backend(
  250. unit.clone(),
  251. PaymentMethod::Bolt11,
  252. mint_melt_limits,
  253. Arc::new(processor),
  254. )
  255. .await?;
  256. let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, unit);
  257. mint_builder = mint_builder.add_supported_websockets(nut17_supported);
  258. }
  259. }
  260. LnBackend::None => {
  261. tracing::error!(
  262. "Pyament backend was not set or feature disabled. {:?}",
  263. settings.ln.ln_backend
  264. );
  265. bail!("Ln backend must be")
  266. }
  267. };
  268. if let Some(long_description) = &settings.mint_info.description_long {
  269. mint_builder = mint_builder.with_long_description(long_description.to_string());
  270. }
  271. if let Some(contact_info) = contact_info {
  272. for info in contact_info {
  273. mint_builder = mint_builder.add_contact_info(info);
  274. }
  275. }
  276. if let Some(pubkey) = settings.mint_info.pubkey {
  277. mint_builder = mint_builder.with_pubkey(pubkey);
  278. }
  279. if let Some(icon_url) = &settings.mint_info.icon_url {
  280. mint_builder = mint_builder.with_icon_url(icon_url.to_string());
  281. }
  282. if let Some(motd) = settings.mint_info.motd {
  283. mint_builder = mint_builder.with_motd(motd);
  284. }
  285. if let Some(tos_url) = &settings.mint_info.tos_url {
  286. mint_builder = mint_builder.with_tos_url(tos_url.to_string());
  287. }
  288. let mnemonic = Mnemonic::from_str(&settings.info.mnemonic)?;
  289. mint_builder = mint_builder
  290. .with_name(settings.mint_info.name)
  291. .with_version(mint_version)
  292. .with_description(settings.mint_info.description)
  293. .with_seed(mnemonic.to_seed_normalized("").to_vec());
  294. let cached_endpoints = vec![
  295. CachedEndpoint::new(NUT19Method::Post, NUT19Path::MintBolt11),
  296. CachedEndpoint::new(NUT19Method::Post, NUT19Path::MeltBolt11),
  297. CachedEndpoint::new(NUT19Method::Post, NUT19Path::Swap),
  298. ];
  299. let cache: HttpCache = settings.info.http_cache.into();
  300. mint_builder = mint_builder.add_cache(Some(cache.ttl.as_secs()), cached_endpoints);
  301. let mint = mint_builder.build().await?;
  302. tracing::debug!("Mint built from builder.");
  303. let mint = Arc::new(mint);
  304. // Check the status of any mint quotes that are pending
  305. // In the event that the mint server is down but the ln node is not
  306. // it is possible that a mint quote was paid but the mint has not been updated
  307. // this will check and update the mint state of those quotes
  308. mint.check_pending_mint_quotes().await?;
  309. // Checks the status of all pending melt quotes
  310. // Pending melt quotes where the payment has gone through inputs are burnt
  311. // Pending melt quotes where the payment has **failed** inputs are reset to unspent
  312. mint.check_pending_melt_quotes().await?;
  313. let listen_addr = settings.info.listen_host;
  314. let listen_port = settings.info.listen_port;
  315. let v1_service =
  316. cdk_axum::create_mint_router_with_custom_cache(Arc::clone(&mint), cache).await?;
  317. let mut mint_service = Router::new()
  318. .merge(v1_service)
  319. .layer(
  320. ServiceBuilder::new()
  321. .layer(RequestDecompressionLayer::new())
  322. .layer(CompressionLayer::new()),
  323. )
  324. .layer(TraceLayer::new_for_http());
  325. #[cfg(feature = "swagger")]
  326. {
  327. if settings.info.enable_swagger_ui.unwrap_or(false) {
  328. mint_service = mint_service.merge(
  329. utoipa_swagger_ui::SwaggerUi::new("/swagger-ui")
  330. .url("/api-docs/openapi.json", cdk_axum::ApiDocV1::openapi()),
  331. );
  332. }
  333. }
  334. for router in ln_routers {
  335. mint_service = mint_service.merge(router);
  336. }
  337. let shutdown = Arc::new(Notify::new());
  338. let mint_clone = Arc::clone(&mint);
  339. tokio::spawn({
  340. let shutdown = Arc::clone(&shutdown);
  341. async move { mint_clone.wait_for_paid_invoices(shutdown).await }
  342. });
  343. #[cfg(feature = "management-rpc")]
  344. let mut rpc_enabled = false;
  345. #[cfg(not(feature = "management-rpc"))]
  346. let rpc_enabled = false;
  347. #[cfg(feature = "management-rpc")]
  348. let mut rpc_server: Option<cdk_mint_rpc::MintRPCServer> = None;
  349. #[cfg(feature = "management-rpc")]
  350. {
  351. if let Some(rpc_settings) = settings.mint_management_rpc {
  352. if rpc_settings.enabled {
  353. let addr = rpc_settings.address.unwrap_or("127.0.0.1".to_string());
  354. let port = rpc_settings.port.unwrap_or(8086);
  355. let mut mint_rpc = MintRPCServer::new(&addr, port, mint.clone())?;
  356. let tls_dir = rpc_settings.tls_dir_path.unwrap_or(work_dir.join("tls"));
  357. if !tls_dir.exists() {
  358. tracing::error!("TLS directory does not exist: {}", tls_dir.display());
  359. bail!("Cannot start RPC server: TLS directory does not exist");
  360. }
  361. mint_rpc.start(Some(tls_dir)).await?;
  362. rpc_server = Some(mint_rpc);
  363. rpc_enabled = true;
  364. }
  365. }
  366. }
  367. if rpc_enabled {
  368. if mint.mint_info().await.is_err() {
  369. tracing::info!("Mint info not set on mint, setting.");
  370. mint.set_mint_info(mint_builder.mint_info).await?;
  371. } else {
  372. tracing::info!("Mint info already set, not using config file settings.");
  373. }
  374. } else {
  375. tracing::warn!("RPC not enabled, using mint info from config.");
  376. mint.set_mint_info(mint_builder.mint_info).await?;
  377. mint.set_quote_ttl(QuoteTTL::new(10_000, 10_000)).await?;
  378. }
  379. let socket_addr = SocketAddr::from_str(&format!("{}:{}", listen_addr, listen_port))?;
  380. let listener = tokio::net::TcpListener::bind(socket_addr).await?;
  381. tracing::debug!("listening on {}", listener.local_addr().unwrap());
  382. let axum_result = axum::serve(listener, mint_service).with_graceful_shutdown(shutdown_signal());
  383. match axum_result.await {
  384. Ok(_) => {
  385. tracing::info!("Axum server stopped with okay status");
  386. }
  387. Err(err) => {
  388. tracing::warn!("Axum server stopped with error");
  389. tracing::error!("{}", err);
  390. bail!("Axum exited with error")
  391. }
  392. }
  393. shutdown.notify_waiters();
  394. #[cfg(feature = "management-rpc")]
  395. {
  396. if let Some(rpc_server) = rpc_server {
  397. rpc_server.stop().await?;
  398. }
  399. }
  400. Ok(())
  401. }
  402. async fn shutdown_signal() {
  403. tokio::signal::ctrl_c()
  404. .await
  405. .expect("failed to install CTRL+C handler");
  406. tracing::info!("Shutdown signal received");
  407. }
  408. fn work_dir() -> Result<PathBuf> {
  409. let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?;
  410. let dir = home_dir.join(".cdk-mintd");
  411. std::fs::create_dir_all(&dir)?;
  412. Ok(dir)
  413. }