config.rs 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106
  1. use std::path::PathBuf;
  2. use bitcoin::hashes::{sha256, Hash};
  3. use cdk::nuts::{CurrencyUnit, PublicKey};
  4. use cdk::Amount;
  5. use cdk_axum::cache;
  6. use cdk_common::common::QuoteTTL;
  7. use config::{Config, ConfigError, File};
  8. use serde::{Deserialize, Serialize};
  9. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
  10. #[serde(rename_all = "lowercase")]
  11. pub enum LoggingOutput {
  12. /// Log to stderr only
  13. Stderr,
  14. /// Log to file only
  15. File,
  16. /// Log to both stderr and file (default)
  17. #[default]
  18. Both,
  19. }
  20. impl std::str::FromStr for LoggingOutput {
  21. type Err = String;
  22. fn from_str(s: &str) -> Result<Self, Self::Err> {
  23. match s.to_lowercase().as_str() {
  24. "stderr" => Ok(LoggingOutput::Stderr),
  25. "file" => Ok(LoggingOutput::File),
  26. "both" => Ok(LoggingOutput::Both),
  27. _ => Err(format!(
  28. "Unknown logging output: {s}. Valid options: stdout, file, both"
  29. )),
  30. }
  31. }
  32. }
  33. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  34. pub struct LoggingConfig {
  35. /// Where to output logs: stdout, file, or both
  36. #[serde(default)]
  37. pub output: LoggingOutput,
  38. /// Log level for console output (when stdout or both)
  39. pub console_level: Option<String>,
  40. /// Log level for file output (when file or both)
  41. pub file_level: Option<String>,
  42. }
  43. #[derive(Clone, Serialize, Deserialize)]
  44. pub struct Info {
  45. pub url: String,
  46. pub listen_host: String,
  47. pub listen_port: u16,
  48. /// Overrides mnemonic
  49. pub seed: Option<String>,
  50. pub mnemonic: Option<String>,
  51. pub signatory_url: Option<String>,
  52. pub signatory_certs: Option<String>,
  53. pub input_fee_ppk: Option<u64>,
  54. pub http_cache: cache::Config,
  55. /// Logging configuration
  56. #[serde(default)]
  57. pub logging: LoggingConfig,
  58. /// When this is set to true, the mint exposes a Swagger UI for it's API at
  59. /// `[listen_host]:[listen_port]/swagger-ui`
  60. ///
  61. /// This requires `mintd` was built with the `swagger` feature flag.
  62. pub enable_swagger_ui: Option<bool>,
  63. /// Optional persisted quote TTL values (seconds) to initialize the database with
  64. /// when RPC is disabled or on first-run when RPC is enabled.
  65. /// If not provided, defaults are used.
  66. #[serde(skip_serializing_if = "Option::is_none")]
  67. pub quote_ttl: Option<QuoteTTL>,
  68. }
  69. impl Default for Info {
  70. fn default() -> Self {
  71. Info {
  72. url: String::new(),
  73. listen_host: "127.0.0.1".to_string(),
  74. listen_port: 8091, // Default to port 8091 instead of 0
  75. seed: None,
  76. mnemonic: None,
  77. signatory_url: None,
  78. signatory_certs: None,
  79. input_fee_ppk: None,
  80. http_cache: cache::Config::default(),
  81. enable_swagger_ui: None,
  82. logging: LoggingConfig::default(),
  83. quote_ttl: None,
  84. }
  85. }
  86. }
  87. impl std::fmt::Debug for Info {
  88. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  89. // Use a fallback approach that won't panic
  90. let mnemonic_display: String = {
  91. if let Some(mnemonic) = self.mnemonic.as_ref() {
  92. let hash = sha256::Hash::hash(mnemonic.as_bytes());
  93. format!("<hashed: {hash}>")
  94. } else {
  95. format!("<url: {}>", self.signatory_url.clone().unwrap_or_default())
  96. }
  97. };
  98. f.debug_struct("Info")
  99. .field("url", &self.url)
  100. .field("listen_host", &self.listen_host)
  101. .field("listen_port", &self.listen_port)
  102. .field("mnemonic", &mnemonic_display)
  103. .field("input_fee_ppk", &self.input_fee_ppk)
  104. .field("http_cache", &self.http_cache)
  105. .field("logging", &self.logging)
  106. .field("enable_swagger_ui", &self.enable_swagger_ui)
  107. .finish()
  108. }
  109. }
  110. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
  111. #[serde(rename_all = "lowercase")]
  112. pub enum LnBackend {
  113. #[default]
  114. None,
  115. #[cfg(feature = "cln")]
  116. Cln,
  117. #[cfg(feature = "lnbits")]
  118. LNbits,
  119. #[cfg(feature = "fakewallet")]
  120. FakeWallet,
  121. #[cfg(feature = "lnd")]
  122. Lnd,
  123. #[cfg(feature = "ldk-node")]
  124. LdkNode,
  125. #[cfg(feature = "grpc-processor")]
  126. GrpcProcessor,
  127. }
  128. impl std::str::FromStr for LnBackend {
  129. type Err = String;
  130. fn from_str(s: &str) -> Result<Self, Self::Err> {
  131. match s.to_lowercase().as_str() {
  132. #[cfg(feature = "cln")]
  133. "cln" => Ok(LnBackend::Cln),
  134. #[cfg(feature = "lnbits")]
  135. "lnbits" => Ok(LnBackend::LNbits),
  136. #[cfg(feature = "fakewallet")]
  137. "fakewallet" => Ok(LnBackend::FakeWallet),
  138. #[cfg(feature = "lnd")]
  139. "lnd" => Ok(LnBackend::Lnd),
  140. #[cfg(feature = "ldk-node")]
  141. "ldk-node" | "ldknode" => Ok(LnBackend::LdkNode),
  142. #[cfg(feature = "grpc-processor")]
  143. "grpcprocessor" => Ok(LnBackend::GrpcProcessor),
  144. _ => Err(format!("Unknown Lightning backend: {s}")),
  145. }
  146. }
  147. }
  148. #[derive(Debug, Clone, Serialize, Deserialize)]
  149. pub struct Ln {
  150. pub ln_backend: LnBackend,
  151. pub invoice_description: Option<String>,
  152. pub min_mint: Amount,
  153. pub max_mint: Amount,
  154. pub min_melt: Amount,
  155. pub max_melt: Amount,
  156. }
  157. impl Default for Ln {
  158. fn default() -> Self {
  159. Ln {
  160. ln_backend: LnBackend::default(),
  161. invoice_description: None,
  162. min_mint: 1.into(),
  163. max_mint: 500_000.into(),
  164. min_melt: 1.into(),
  165. max_melt: 500_000.into(),
  166. }
  167. }
  168. }
  169. #[cfg(feature = "lnbits")]
  170. #[derive(Debug, Clone, Serialize, Deserialize)]
  171. pub struct LNbits {
  172. pub admin_api_key: String,
  173. pub invoice_api_key: String,
  174. pub lnbits_api: String,
  175. #[serde(default = "default_fee_percent")]
  176. pub fee_percent: f32,
  177. #[serde(default = "default_reserve_fee_min")]
  178. pub reserve_fee_min: Amount,
  179. }
  180. #[cfg(feature = "lnbits")]
  181. impl Default for LNbits {
  182. fn default() -> Self {
  183. Self {
  184. admin_api_key: String::new(),
  185. invoice_api_key: String::new(),
  186. lnbits_api: String::new(),
  187. fee_percent: 0.02,
  188. reserve_fee_min: 2.into(),
  189. }
  190. }
  191. }
  192. #[cfg(feature = "cln")]
  193. #[derive(Debug, Clone, Serialize, Deserialize)]
  194. pub struct Cln {
  195. pub rpc_path: PathBuf,
  196. #[serde(default = "default_cln_bolt12")]
  197. pub bolt12: bool,
  198. #[serde(default = "default_fee_percent")]
  199. pub fee_percent: f32,
  200. #[serde(default = "default_reserve_fee_min")]
  201. pub reserve_fee_min: Amount,
  202. }
  203. #[cfg(feature = "cln")]
  204. impl Default for Cln {
  205. fn default() -> Self {
  206. Self {
  207. rpc_path: PathBuf::new(),
  208. bolt12: true,
  209. fee_percent: 0.02,
  210. reserve_fee_min: 2.into(),
  211. }
  212. }
  213. }
  214. #[cfg(feature = "cln")]
  215. fn default_cln_bolt12() -> bool {
  216. true
  217. }
  218. #[cfg(feature = "lnd")]
  219. #[derive(Debug, Clone, Serialize, Deserialize)]
  220. pub struct Lnd {
  221. pub address: String,
  222. pub cert_file: PathBuf,
  223. pub macaroon_file: PathBuf,
  224. #[serde(default = "default_fee_percent")]
  225. pub fee_percent: f32,
  226. #[serde(default = "default_reserve_fee_min")]
  227. pub reserve_fee_min: Amount,
  228. }
  229. #[cfg(feature = "lnd")]
  230. impl Default for Lnd {
  231. fn default() -> Self {
  232. Self {
  233. address: String::new(),
  234. cert_file: PathBuf::new(),
  235. macaroon_file: PathBuf::new(),
  236. fee_percent: 0.02,
  237. reserve_fee_min: 2.into(),
  238. }
  239. }
  240. }
  241. #[cfg(feature = "ldk-node")]
  242. #[derive(Debug, Clone, Serialize, Deserialize)]
  243. pub struct LdkNode {
  244. /// Fee percentage (e.g., 0.02 for 2%)
  245. #[serde(default = "default_ldk_fee_percent")]
  246. pub fee_percent: f32,
  247. /// Minimum reserve fee
  248. #[serde(default = "default_ldk_reserve_fee_min")]
  249. pub reserve_fee_min: Amount,
  250. /// Bitcoin network (mainnet, testnet, signet, regtest)
  251. pub bitcoin_network: Option<String>,
  252. /// Chain source type (esplora or bitcoinrpc)
  253. pub chain_source_type: Option<String>,
  254. /// Esplora URL (when chain_source_type = "esplora")
  255. pub esplora_url: Option<String>,
  256. /// Bitcoin RPC configuration (when chain_source_type = "bitcoinrpc")
  257. pub bitcoind_rpc_host: Option<String>,
  258. pub bitcoind_rpc_port: Option<u16>,
  259. pub bitcoind_rpc_user: Option<String>,
  260. pub bitcoind_rpc_password: Option<String>,
  261. /// Storage directory path
  262. pub storage_dir_path: Option<String>,
  263. /// LDK node listening host
  264. pub ldk_node_host: Option<String>,
  265. /// LDK node listening port
  266. pub ldk_node_port: Option<u16>,
  267. /// Gossip source type (p2p or rgs)
  268. pub gossip_source_type: Option<String>,
  269. /// Rapid Gossip Sync URL (when gossip_source_type = "rgs")
  270. pub rgs_url: Option<String>,
  271. /// Webserver host (defaults to 127.0.0.1)
  272. #[serde(default = "default_webserver_host")]
  273. pub webserver_host: Option<String>,
  274. /// Webserver port
  275. #[serde(default = "default_webserver_port")]
  276. pub webserver_port: Option<u16>,
  277. }
  278. #[cfg(feature = "ldk-node")]
  279. impl Default for LdkNode {
  280. fn default() -> Self {
  281. Self {
  282. fee_percent: default_ldk_fee_percent(),
  283. reserve_fee_min: default_ldk_reserve_fee_min(),
  284. bitcoin_network: None,
  285. chain_source_type: None,
  286. esplora_url: None,
  287. bitcoind_rpc_host: None,
  288. bitcoind_rpc_port: None,
  289. bitcoind_rpc_user: None,
  290. bitcoind_rpc_password: None,
  291. storage_dir_path: None,
  292. ldk_node_host: None,
  293. ldk_node_port: None,
  294. gossip_source_type: None,
  295. rgs_url: None,
  296. webserver_host: default_webserver_host(),
  297. webserver_port: default_webserver_port(),
  298. }
  299. }
  300. }
  301. #[cfg(feature = "ldk-node")]
  302. fn default_ldk_fee_percent() -> f32 {
  303. 0.04
  304. }
  305. #[cfg(feature = "ldk-node")]
  306. fn default_ldk_reserve_fee_min() -> Amount {
  307. 4.into()
  308. }
  309. #[cfg(feature = "ldk-node")]
  310. fn default_webserver_host() -> Option<String> {
  311. Some("127.0.0.1".to_string())
  312. }
  313. #[cfg(feature = "ldk-node")]
  314. fn default_webserver_port() -> Option<u16> {
  315. Some(8091)
  316. }
  317. #[cfg(feature = "fakewallet")]
  318. #[derive(Debug, Clone, Serialize, Deserialize)]
  319. pub struct FakeWallet {
  320. pub supported_units: Vec<CurrencyUnit>,
  321. pub fee_percent: f32,
  322. pub reserve_fee_min: Amount,
  323. #[serde(default = "default_min_delay_time")]
  324. pub min_delay_time: u64,
  325. #[serde(default = "default_max_delay_time")]
  326. pub max_delay_time: u64,
  327. }
  328. #[cfg(feature = "fakewallet")]
  329. impl Default for FakeWallet {
  330. fn default() -> Self {
  331. Self {
  332. supported_units: vec![CurrencyUnit::Sat],
  333. fee_percent: 0.02,
  334. reserve_fee_min: 2.into(),
  335. min_delay_time: 1,
  336. max_delay_time: 3,
  337. }
  338. }
  339. }
  340. // Helper functions to provide default values
  341. // Common fee defaults for all backends
  342. fn default_fee_percent() -> f32 {
  343. 0.02
  344. }
  345. fn default_reserve_fee_min() -> Amount {
  346. 2.into()
  347. }
  348. #[cfg(feature = "fakewallet")]
  349. fn default_min_delay_time() -> u64 {
  350. 1
  351. }
  352. #[cfg(feature = "fakewallet")]
  353. fn default_max_delay_time() -> u64 {
  354. 3
  355. }
  356. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
  357. pub struct GrpcProcessor {
  358. #[serde(default)]
  359. pub supported_units: Vec<CurrencyUnit>,
  360. #[serde(default = "default_grpc_addr")]
  361. pub addr: String,
  362. #[serde(default = "default_grpc_port")]
  363. pub port: u16,
  364. #[serde(default)]
  365. pub tls_dir: Option<PathBuf>,
  366. }
  367. impl Default for GrpcProcessor {
  368. fn default() -> Self {
  369. Self {
  370. supported_units: Vec::new(),
  371. addr: default_grpc_addr(),
  372. port: default_grpc_port(),
  373. tls_dir: None,
  374. }
  375. }
  376. }
  377. fn default_grpc_addr() -> String {
  378. "127.0.0.1".to_string()
  379. }
  380. fn default_grpc_port() -> u16 {
  381. 50051
  382. }
  383. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
  384. #[serde(rename_all = "lowercase")]
  385. pub enum DatabaseEngine {
  386. #[default]
  387. Sqlite,
  388. Postgres,
  389. }
  390. impl std::str::FromStr for DatabaseEngine {
  391. type Err = String;
  392. fn from_str(s: &str) -> Result<Self, Self::Err> {
  393. match s.to_lowercase().as_str() {
  394. "sqlite" => Ok(DatabaseEngine::Sqlite),
  395. "postgres" => Ok(DatabaseEngine::Postgres),
  396. _ => Err(format!("Unknown database engine: {s}")),
  397. }
  398. }
  399. }
  400. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  401. pub struct Database {
  402. pub engine: DatabaseEngine,
  403. pub postgres: Option<PostgresConfig>,
  404. }
  405. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  406. pub struct AuthDatabase {
  407. pub postgres: Option<PostgresAuthConfig>,
  408. }
  409. #[derive(Debug, Clone, Serialize, Deserialize)]
  410. pub struct PostgresAuthConfig {
  411. pub url: String,
  412. pub tls_mode: Option<String>,
  413. pub max_connections: Option<usize>,
  414. pub connection_timeout_seconds: Option<u64>,
  415. }
  416. impl Default for PostgresAuthConfig {
  417. fn default() -> Self {
  418. Self {
  419. url: String::new(),
  420. tls_mode: Some("disable".to_string()),
  421. max_connections: Some(20),
  422. connection_timeout_seconds: Some(10),
  423. }
  424. }
  425. }
  426. #[derive(Debug, Clone, Serialize, Deserialize)]
  427. pub struct PostgresConfig {
  428. pub url: String,
  429. pub tls_mode: Option<String>,
  430. pub max_connections: Option<usize>,
  431. pub connection_timeout_seconds: Option<u64>,
  432. }
  433. impl Default for PostgresConfig {
  434. fn default() -> Self {
  435. Self {
  436. url: String::new(),
  437. tls_mode: Some("disable".to_string()),
  438. max_connections: Some(20),
  439. connection_timeout_seconds: Some(10),
  440. }
  441. }
  442. }
  443. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
  444. #[serde(rename_all = "lowercase")]
  445. pub enum AuthType {
  446. Clear,
  447. Blind,
  448. #[default]
  449. None,
  450. }
  451. impl std::str::FromStr for AuthType {
  452. type Err = String;
  453. fn from_str(s: &str) -> Result<Self, Self::Err> {
  454. match s.to_lowercase().as_str() {
  455. "clear" => Ok(AuthType::Clear),
  456. "blind" => Ok(AuthType::Blind),
  457. "none" => Ok(AuthType::None),
  458. _ => Err(format!("Unknown auth type: {s}")),
  459. }
  460. }
  461. }
  462. #[derive(Debug, Clone, Default, Serialize, Deserialize)]
  463. pub struct Auth {
  464. #[serde(default)]
  465. pub auth_enabled: bool,
  466. pub openid_discovery: String,
  467. pub openid_client_id: String,
  468. pub mint_max_bat: u64,
  469. #[serde(default = "default_blind")]
  470. pub mint: AuthType,
  471. #[serde(default)]
  472. pub get_mint_quote: AuthType,
  473. #[serde(default)]
  474. pub check_mint_quote: AuthType,
  475. #[serde(default)]
  476. pub melt: AuthType,
  477. #[serde(default)]
  478. pub get_melt_quote: AuthType,
  479. #[serde(default)]
  480. pub check_melt_quote: AuthType,
  481. #[serde(default = "default_blind")]
  482. pub swap: AuthType,
  483. #[serde(default = "default_blind")]
  484. pub restore: AuthType,
  485. #[serde(default)]
  486. pub check_proof_state: AuthType,
  487. /// Enable WebSocket authentication support
  488. #[serde(default = "default_blind")]
  489. pub websocket_auth: AuthType,
  490. }
  491. fn default_blind() -> AuthType {
  492. AuthType::Blind
  493. }
  494. /// CDK settings, derived from `config.toml`
  495. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  496. pub struct Settings {
  497. pub info: Info,
  498. pub mint_info: MintInfo,
  499. pub ln: Ln,
  500. #[cfg(feature = "cln")]
  501. pub cln: Option<Cln>,
  502. #[cfg(feature = "lnbits")]
  503. pub lnbits: Option<LNbits>,
  504. #[cfg(feature = "lnd")]
  505. pub lnd: Option<Lnd>,
  506. #[cfg(feature = "ldk-node")]
  507. pub ldk_node: Option<LdkNode>,
  508. #[cfg(feature = "fakewallet")]
  509. pub fake_wallet: Option<FakeWallet>,
  510. pub grpc_processor: Option<GrpcProcessor>,
  511. pub database: Database,
  512. #[cfg(feature = "auth")]
  513. pub auth_database: Option<AuthDatabase>,
  514. #[cfg(feature = "management-rpc")]
  515. pub mint_management_rpc: Option<MintManagementRpc>,
  516. pub auth: Option<Auth>,
  517. #[cfg(feature = "prometheus")]
  518. pub prometheus: Option<Prometheus>,
  519. }
  520. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  521. #[cfg(feature = "prometheus")]
  522. pub struct Prometheus {
  523. pub enabled: bool,
  524. pub address: Option<String>,
  525. pub port: Option<u16>,
  526. }
  527. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  528. pub struct MintInfo {
  529. /// name of the mint and should be recognizable
  530. pub name: String,
  531. /// hex pubkey of the mint
  532. pub pubkey: Option<PublicKey>,
  533. /// short description of the mint
  534. pub description: String,
  535. /// long description
  536. pub description_long: Option<String>,
  537. /// url to the mint icon
  538. pub icon_url: Option<String>,
  539. /// message of the day that the wallet must display to the user
  540. pub motd: Option<String>,
  541. /// Nostr publickey
  542. pub contact_nostr_public_key: Option<String>,
  543. /// Contact email
  544. pub contact_email: Option<String>,
  545. /// URL to the terms of service
  546. pub tos_url: Option<String>,
  547. }
  548. #[cfg(feature = "management-rpc")]
  549. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  550. pub struct MintManagementRpc {
  551. /// When this is set to `true` the mint use the config file for the initial set up on first start.
  552. /// Changes to the `[mint_info]` after this **MUST** be made via the RPC changes to the config file or env vars will be ignored.
  553. pub enabled: bool,
  554. pub address: Option<String>,
  555. pub port: Option<u16>,
  556. pub tls_dir_path: Option<PathBuf>,
  557. }
  558. impl Settings {
  559. #[must_use]
  560. pub fn new<P>(config_file_name: Option<P>) -> Self
  561. where
  562. P: Into<PathBuf>,
  563. {
  564. let default_settings = Self::default();
  565. // attempt to construct settings with file
  566. let from_file = Self::new_from_default(&default_settings, config_file_name);
  567. match from_file {
  568. Ok(f) => f,
  569. Err(e) => {
  570. tracing::error!(
  571. "Error reading config file, falling back to defaults. Error: {e:?}"
  572. );
  573. default_settings
  574. }
  575. }
  576. }
  577. fn new_from_default<P>(
  578. default: &Settings,
  579. config_file_name: Option<P>,
  580. ) -> Result<Self, ConfigError>
  581. where
  582. P: Into<PathBuf>,
  583. {
  584. let mut default_config_file_name = home::home_dir()
  585. .ok_or(ConfigError::NotFound("Config Path".to_string()))?
  586. .join("cashu-rs-mint");
  587. default_config_file_name.push("config.toml");
  588. let config: String = match config_file_name {
  589. Some(value) => value.into().to_string_lossy().to_string(),
  590. None => default_config_file_name.to_string_lossy().to_string(),
  591. };
  592. let builder = Config::builder();
  593. let config: Config = builder
  594. // use defaults
  595. .add_source(Config::try_from(default)?)
  596. // override with file contents
  597. .add_source(File::with_name(&config))
  598. .build()?;
  599. let settings: Settings = config.try_deserialize()?;
  600. Ok(settings)
  601. }
  602. }
  603. #[cfg(test)]
  604. mod tests {
  605. use super::*;
  606. #[test]
  607. fn test_info_debug_impl() {
  608. // Create a sample Info struct with test data
  609. let info = Info {
  610. url: "http://example.com".to_string(),
  611. listen_host: "127.0.0.1".to_string(),
  612. listen_port: 8080,
  613. mnemonic: Some("test secret mnemonic phrase".to_string()),
  614. input_fee_ppk: Some(100),
  615. ..Default::default()
  616. };
  617. // Convert the Info struct to a debug string
  618. let debug_output = format!("{info:?}");
  619. // Verify the debug output contains expected fields
  620. assert!(debug_output.contains("url: \"http://example.com\""));
  621. assert!(debug_output.contains("listen_host: \"127.0.0.1\""));
  622. assert!(debug_output.contains("listen_port: 8080"));
  623. // The mnemonic should be hashed, not displayed in plaintext
  624. assert!(!debug_output.contains("test secret mnemonic phrase"));
  625. assert!(debug_output.contains("<hashed: "));
  626. assert!(debug_output.contains("input_fee_ppk: Some(100)"));
  627. }
  628. #[test]
  629. fn test_info_debug_with_empty_mnemonic() {
  630. // Test with an empty mnemonic to ensure it doesn't panic
  631. let info = Info {
  632. url: "http://example.com".to_string(),
  633. listen_host: "127.0.0.1".to_string(),
  634. listen_port: 8080,
  635. mnemonic: Some("".to_string()), // Empty mnemonic
  636. enable_swagger_ui: Some(false),
  637. ..Default::default()
  638. };
  639. // This should not panic
  640. let debug_output = format!("{:?}", info);
  641. // The empty mnemonic should still be hashed
  642. assert!(debug_output.contains("<hashed: "));
  643. }
  644. #[test]
  645. fn test_info_debug_with_special_chars() {
  646. // Test with a mnemonic containing special characters
  647. let info = Info {
  648. url: "http://example.com".to_string(),
  649. listen_host: "127.0.0.1".to_string(),
  650. listen_port: 8080,
  651. mnemonic: Some("特殊字符 !@#$%^&*()".to_string()), // Special characters
  652. ..Default::default()
  653. };
  654. // This should not panic
  655. let debug_output = format!("{:?}", info);
  656. // The mnemonic with special chars should be hashed
  657. assert!(!debug_output.contains("特殊字符 !@#$%^&*()"));
  658. assert!(debug_output.contains("<hashed: "));
  659. }
  660. /// Test that configuration can be loaded purely from environment variables
  661. /// without requiring a config.toml file with backend sections.
  662. ///
  663. /// This test runs sequentially for all enabled backends to avoid env var interference.
  664. #[test]
  665. fn test_env_var_only_config_all_backends() {
  666. // Run each backend test sequentially
  667. #[cfg(feature = "lnd")]
  668. test_lnd_env_config();
  669. #[cfg(feature = "cln")]
  670. test_cln_env_config();
  671. #[cfg(feature = "lnbits")]
  672. test_lnbits_env_config();
  673. #[cfg(feature = "fakewallet")]
  674. test_fakewallet_env_config();
  675. #[cfg(feature = "grpc-processor")]
  676. test_grpc_processor_env_config();
  677. #[cfg(feature = "ldk-node")]
  678. test_ldk_node_env_config();
  679. }
  680. #[cfg(feature = "lnd")]
  681. fn test_lnd_env_config() {
  682. use std::path::PathBuf;
  683. use std::{env, fs};
  684. // Create a temporary directory for config file
  685. let temp_dir = env::temp_dir().join("cdk_test_env_vars");
  686. fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
  687. let config_path = temp_dir.join("config.toml");
  688. // Create a minimal config.toml with backend set but NO [lnd] section
  689. let config_content = r#"
  690. [ln]
  691. backend = "lnd"
  692. min_mint = 1
  693. max_mint = 500000
  694. min_melt = 1
  695. max_melt = 500000
  696. "#;
  697. fs::write(&config_path, config_content).expect("Failed to write config file");
  698. // Set environment variables for LND configuration
  699. env::set_var(crate::env_vars::ENV_LN_BACKEND, "lnd");
  700. env::set_var(crate::env_vars::ENV_LND_ADDRESS, "https://localhost:10009");
  701. env::set_var(crate::env_vars::ENV_LND_CERT_FILE, "/tmp/test_tls.cert");
  702. env::set_var(
  703. crate::env_vars::ENV_LND_MACAROON_FILE,
  704. "/tmp/test_admin.macaroon",
  705. );
  706. env::set_var(crate::env_vars::ENV_LND_FEE_PERCENT, "0.01");
  707. env::set_var(crate::env_vars::ENV_LND_RESERVE_FEE_MIN, "4");
  708. // Load settings and apply environment variables (same as production code)
  709. let mut settings = Settings::new(Some(&config_path));
  710. settings.from_env().expect("Failed to apply env vars");
  711. // Verify that settings were populated from env vars
  712. assert!(settings.lnd.is_some());
  713. let lnd_config = settings.lnd.as_ref().unwrap();
  714. assert_eq!(lnd_config.address, "https://localhost:10009");
  715. assert_eq!(lnd_config.cert_file, PathBuf::from("/tmp/test_tls.cert"));
  716. assert_eq!(
  717. lnd_config.macaroon_file,
  718. PathBuf::from("/tmp/test_admin.macaroon")
  719. );
  720. assert_eq!(lnd_config.fee_percent, 0.01);
  721. let reserve_fee_u64: u64 = lnd_config.reserve_fee_min.into();
  722. assert_eq!(reserve_fee_u64, 4);
  723. // Cleanup env vars
  724. env::remove_var(crate::env_vars::ENV_LN_BACKEND);
  725. env::remove_var(crate::env_vars::ENV_LND_ADDRESS);
  726. env::remove_var(crate::env_vars::ENV_LND_CERT_FILE);
  727. env::remove_var(crate::env_vars::ENV_LND_MACAROON_FILE);
  728. env::remove_var(crate::env_vars::ENV_LND_FEE_PERCENT);
  729. env::remove_var(crate::env_vars::ENV_LND_RESERVE_FEE_MIN);
  730. // Cleanup test file
  731. let _ = fs::remove_dir_all(&temp_dir);
  732. }
  733. #[cfg(feature = "cln")]
  734. fn test_cln_env_config() {
  735. use std::path::PathBuf;
  736. use std::{env, fs};
  737. // Create a temporary directory for config file
  738. let temp_dir = env::temp_dir().join("cdk_test_env_vars_cln");
  739. fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
  740. let config_path = temp_dir.join("config.toml");
  741. // Create a minimal config.toml with backend set but NO [cln] section
  742. let config_content = r#"
  743. [ln]
  744. backend = "cln"
  745. min_mint = 1
  746. max_mint = 500000
  747. min_melt = 1
  748. max_melt = 500000
  749. "#;
  750. fs::write(&config_path, config_content).expect("Failed to write config file");
  751. // Set environment variables for CLN configuration
  752. env::set_var(crate::env_vars::ENV_LN_BACKEND, "cln");
  753. env::set_var(crate::env_vars::ENV_CLN_RPC_PATH, "/tmp/lightning-rpc");
  754. env::set_var(crate::env_vars::ENV_CLN_BOLT12, "false");
  755. env::set_var(crate::env_vars::ENV_CLN_FEE_PERCENT, "0.01");
  756. env::set_var(crate::env_vars::ENV_CLN_RESERVE_FEE_MIN, "4");
  757. // Load settings and apply environment variables (same as production code)
  758. let mut settings = Settings::new(Some(&config_path));
  759. settings.from_env().expect("Failed to apply env vars");
  760. // Verify that settings were populated from env vars
  761. assert!(settings.cln.is_some());
  762. let cln_config = settings.cln.as_ref().unwrap();
  763. assert_eq!(cln_config.rpc_path, PathBuf::from("/tmp/lightning-rpc"));
  764. assert_eq!(cln_config.bolt12, false);
  765. assert_eq!(cln_config.fee_percent, 0.01);
  766. let reserve_fee_u64: u64 = cln_config.reserve_fee_min.into();
  767. assert_eq!(reserve_fee_u64, 4);
  768. // Cleanup env vars
  769. env::remove_var(crate::env_vars::ENV_LN_BACKEND);
  770. env::remove_var(crate::env_vars::ENV_CLN_RPC_PATH);
  771. env::remove_var(crate::env_vars::ENV_CLN_BOLT12);
  772. env::remove_var(crate::env_vars::ENV_CLN_FEE_PERCENT);
  773. env::remove_var(crate::env_vars::ENV_CLN_RESERVE_FEE_MIN);
  774. // Cleanup test file
  775. let _ = fs::remove_dir_all(&temp_dir);
  776. }
  777. #[cfg(feature = "lnbits")]
  778. fn test_lnbits_env_config() {
  779. use std::{env, fs};
  780. // Create a temporary directory for config file
  781. let temp_dir = env::temp_dir().join("cdk_test_env_vars_lnbits");
  782. fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
  783. let config_path = temp_dir.join("config.toml");
  784. // Create a minimal config.toml with backend set but NO [lnbits] section
  785. let config_content = r#"
  786. [ln]
  787. backend = "lnbits"
  788. min_mint = 1
  789. max_mint = 500000
  790. min_melt = 1
  791. max_melt = 500000
  792. "#;
  793. fs::write(&config_path, config_content).expect("Failed to write config file");
  794. // Set environment variables for LNbits configuration
  795. env::set_var(crate::env_vars::ENV_LN_BACKEND, "lnbits");
  796. env::set_var(crate::env_vars::ENV_LNBITS_ADMIN_API_KEY, "test_admin_key");
  797. env::set_var(
  798. crate::env_vars::ENV_LNBITS_INVOICE_API_KEY,
  799. "test_invoice_key",
  800. );
  801. env::set_var(
  802. crate::env_vars::ENV_LNBITS_API,
  803. "https://lnbits.example.com",
  804. );
  805. env::set_var(crate::env_vars::ENV_LNBITS_FEE_PERCENT, "0.02");
  806. env::set_var(crate::env_vars::ENV_LNBITS_RESERVE_FEE_MIN, "5");
  807. // Load settings and apply environment variables (same as production code)
  808. let mut settings = Settings::new(Some(&config_path));
  809. settings.from_env().expect("Failed to apply env vars");
  810. // Verify that settings were populated from env vars
  811. assert!(settings.lnbits.is_some());
  812. let lnbits_config = settings.lnbits.as_ref().unwrap();
  813. assert_eq!(lnbits_config.admin_api_key, "test_admin_key");
  814. assert_eq!(lnbits_config.invoice_api_key, "test_invoice_key");
  815. assert_eq!(lnbits_config.lnbits_api, "https://lnbits.example.com");
  816. assert_eq!(lnbits_config.fee_percent, 0.02);
  817. let reserve_fee_u64: u64 = lnbits_config.reserve_fee_min.into();
  818. assert_eq!(reserve_fee_u64, 5);
  819. // Cleanup env vars
  820. env::remove_var(crate::env_vars::ENV_LN_BACKEND);
  821. env::remove_var(crate::env_vars::ENV_LNBITS_ADMIN_API_KEY);
  822. env::remove_var(crate::env_vars::ENV_LNBITS_INVOICE_API_KEY);
  823. env::remove_var(crate::env_vars::ENV_LNBITS_API);
  824. env::remove_var(crate::env_vars::ENV_LNBITS_FEE_PERCENT);
  825. env::remove_var(crate::env_vars::ENV_LNBITS_RESERVE_FEE_MIN);
  826. // Cleanup test file
  827. let _ = fs::remove_dir_all(&temp_dir);
  828. }
  829. #[cfg(feature = "fakewallet")]
  830. fn test_fakewallet_env_config() {
  831. use std::{env, fs};
  832. // Create a temporary directory for config file
  833. let temp_dir = env::temp_dir().join("cdk_test_env_vars_fakewallet");
  834. fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
  835. let config_path = temp_dir.join("config.toml");
  836. // Create a minimal config.toml with backend set but NO [fake_wallet] section
  837. let config_content = r#"
  838. [ln]
  839. backend = "fakewallet"
  840. min_mint = 1
  841. max_mint = 500000
  842. min_melt = 1
  843. max_melt = 500000
  844. "#;
  845. fs::write(&config_path, config_content).expect("Failed to write config file");
  846. // Set environment variables for FakeWallet configuration
  847. env::set_var(crate::env_vars::ENV_LN_BACKEND, "fakewallet");
  848. env::set_var(crate::env_vars::ENV_FAKE_WALLET_SUPPORTED_UNITS, "sat,msat");
  849. env::set_var(crate::env_vars::ENV_FAKE_WALLET_FEE_PERCENT, "0.0");
  850. env::set_var(crate::env_vars::ENV_FAKE_WALLET_RESERVE_FEE_MIN, "0");
  851. env::set_var(crate::env_vars::ENV_FAKE_WALLET_MIN_DELAY, "0");
  852. env::set_var(crate::env_vars::ENV_FAKE_WALLET_MAX_DELAY, "5");
  853. // Load settings and apply environment variables (same as production code)
  854. let mut settings = Settings::new(Some(&config_path));
  855. settings.from_env().expect("Failed to apply env vars");
  856. // Verify that settings were populated from env vars
  857. assert!(settings.fake_wallet.is_some());
  858. let fakewallet_config = settings.fake_wallet.as_ref().unwrap();
  859. assert_eq!(fakewallet_config.fee_percent, 0.0);
  860. let reserve_fee_u64: u64 = fakewallet_config.reserve_fee_min.into();
  861. assert_eq!(reserve_fee_u64, 0);
  862. assert_eq!(fakewallet_config.min_delay_time, 0);
  863. assert_eq!(fakewallet_config.max_delay_time, 5);
  864. // Cleanup env vars
  865. env::remove_var(crate::env_vars::ENV_LN_BACKEND);
  866. env::remove_var(crate::env_vars::ENV_FAKE_WALLET_SUPPORTED_UNITS);
  867. env::remove_var(crate::env_vars::ENV_FAKE_WALLET_FEE_PERCENT);
  868. env::remove_var(crate::env_vars::ENV_FAKE_WALLET_RESERVE_FEE_MIN);
  869. env::remove_var(crate::env_vars::ENV_FAKE_WALLET_MIN_DELAY);
  870. env::remove_var(crate::env_vars::ENV_FAKE_WALLET_MAX_DELAY);
  871. // Cleanup test file
  872. let _ = fs::remove_dir_all(&temp_dir);
  873. }
  874. #[cfg(feature = "grpc-processor")]
  875. fn test_grpc_processor_env_config() {
  876. use std::{env, fs};
  877. // Create a temporary directory for config file
  878. let temp_dir = env::temp_dir().join("cdk_test_env_vars_grpc");
  879. fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
  880. let config_path = temp_dir.join("config.toml");
  881. // Create a minimal config.toml with backend set but NO [grpc_processor] section
  882. let config_content = r#"
  883. [ln]
  884. backend = "grpcprocessor"
  885. min_mint = 1
  886. max_mint = 500000
  887. min_melt = 1
  888. max_melt = 500000
  889. "#;
  890. fs::write(&config_path, config_content).expect("Failed to write config file");
  891. // Set environment variables for GRPC Processor configuration
  892. env::set_var(crate::env_vars::ENV_LN_BACKEND, "grpcprocessor");
  893. env::set_var(
  894. crate::env_vars::ENV_GRPC_PROCESSOR_SUPPORTED_UNITS,
  895. "sat,msat",
  896. );
  897. env::set_var(crate::env_vars::ENV_GRPC_PROCESSOR_ADDRESS, "localhost");
  898. env::set_var(crate::env_vars::ENV_GRPC_PROCESSOR_PORT, "50051");
  899. // Load settings and apply environment variables (same as production code)
  900. let mut settings = Settings::new(Some(&config_path));
  901. settings.from_env().expect("Failed to apply env vars");
  902. // Verify that settings were populated from env vars
  903. assert!(settings.grpc_processor.is_some());
  904. let grpc_config = settings.grpc_processor.as_ref().unwrap();
  905. assert_eq!(grpc_config.addr, "localhost");
  906. assert_eq!(grpc_config.port, 50051);
  907. // Cleanup env vars
  908. env::remove_var(crate::env_vars::ENV_LN_BACKEND);
  909. env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_SUPPORTED_UNITS);
  910. env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_ADDRESS);
  911. env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_PORT);
  912. // Cleanup test file
  913. let _ = fs::remove_dir_all(&temp_dir);
  914. }
  915. #[cfg(feature = "ldk-node")]
  916. fn test_ldk_node_env_config() {
  917. use std::{env, fs};
  918. // Create a temporary directory for config file
  919. let temp_dir = env::temp_dir().join("cdk_test_env_vars_ldk");
  920. fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
  921. let config_path = temp_dir.join("config.toml");
  922. // Create a minimal config.toml with backend set but NO [ldk_node] section
  923. let config_content = r#"
  924. [ln]
  925. backend = "ldknode"
  926. min_mint = 1
  927. max_mint = 500000
  928. min_melt = 1
  929. max_melt = 500000
  930. "#;
  931. fs::write(&config_path, config_content).expect("Failed to write config file");
  932. // Set environment variables for LDK Node configuration
  933. env::set_var(crate::env_vars::ENV_LN_BACKEND, "ldknode");
  934. env::set_var(crate::env_vars::LDK_NODE_FEE_PERCENT_ENV_VAR, "0.01");
  935. env::set_var(crate::env_vars::LDK_NODE_RESERVE_FEE_MIN_ENV_VAR, "4");
  936. env::set_var(crate::env_vars::LDK_NODE_BITCOIN_NETWORK_ENV_VAR, "regtest");
  937. env::set_var(
  938. crate::env_vars::LDK_NODE_CHAIN_SOURCE_TYPE_ENV_VAR,
  939. "esplora",
  940. );
  941. env::set_var(
  942. crate::env_vars::LDK_NODE_ESPLORA_URL_ENV_VAR,
  943. "http://localhost:3000",
  944. );
  945. env::set_var(
  946. crate::env_vars::LDK_NODE_STORAGE_DIR_PATH_ENV_VAR,
  947. "/tmp/ldk",
  948. );
  949. // Load settings and apply environment variables (same as production code)
  950. let mut settings = Settings::new(Some(&config_path));
  951. settings.from_env().expect("Failed to apply env vars");
  952. // Verify that settings were populated from env vars
  953. assert!(settings.ldk_node.is_some());
  954. let ldk_config = settings.ldk_node.as_ref().unwrap();
  955. assert_eq!(ldk_config.fee_percent, 0.01);
  956. let reserve_fee_u64: u64 = ldk_config.reserve_fee_min.into();
  957. assert_eq!(reserve_fee_u64, 4);
  958. assert_eq!(ldk_config.bitcoin_network, Some("regtest".to_string()));
  959. assert_eq!(ldk_config.chain_source_type, Some("esplora".to_string()));
  960. assert_eq!(
  961. ldk_config.esplora_url,
  962. Some("http://localhost:3000".to_string())
  963. );
  964. assert_eq!(ldk_config.storage_dir_path, Some("/tmp/ldk".to_string()));
  965. // Cleanup env vars
  966. env::remove_var(crate::env_vars::ENV_LN_BACKEND);
  967. env::remove_var(crate::env_vars::LDK_NODE_FEE_PERCENT_ENV_VAR);
  968. env::remove_var(crate::env_vars::LDK_NODE_RESERVE_FEE_MIN_ENV_VAR);
  969. env::remove_var(crate::env_vars::LDK_NODE_BITCOIN_NETWORK_ENV_VAR);
  970. env::remove_var(crate::env_vars::LDK_NODE_CHAIN_SOURCE_TYPE_ENV_VAR);
  971. env::remove_var(crate::env_vars::LDK_NODE_ESPLORA_URL_ENV_VAR);
  972. env::remove_var(crate::env_vars::LDK_NODE_STORAGE_DIR_PATH_ENV_VAR);
  973. // Cleanup test file
  974. let _ = fs::remove_dir_all(&temp_dir);
  975. }
  976. }