config.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  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 config::{Config, ConfigError, File};
  7. use serde::{Deserialize, Serialize};
  8. #[derive(Clone, Serialize, Deserialize, Default)]
  9. pub struct Info {
  10. pub url: String,
  11. pub listen_host: String,
  12. pub listen_port: u16,
  13. pub mnemonic: String,
  14. pub input_fee_ppk: Option<u64>,
  15. pub http_cache: cache::Config,
  16. /// When this is set to true, the mint exposes a Swagger UI for it's API at
  17. /// `[listen_host]:[listen_port]/swagger-ui`
  18. ///
  19. /// This requires `mintd` was built with the `swagger` feature flag.
  20. pub enable_swagger_ui: Option<bool>,
  21. }
  22. impl std::fmt::Debug for Info {
  23. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  24. // Use a fallback approach that won't panic
  25. let mnemonic_display = match sha256::Hash::hash(self.mnemonic.clone().into_bytes().as_ref())
  26. {
  27. Ok(hash) => format!("<hashed: {}>", hash),
  28. Err(_) => String::from("<protected>"), // Fallback if hashing fails
  29. };
  30. f.debug_struct("Info")
  31. .field("url", &self.url)
  32. .field("listen_host", &self.listen_host)
  33. .field("listen_port", &self.listen_port)
  34. .field("mnemonic", &mnemonic_display)
  35. .field("input_fee_ppk", &self.input_fee_ppk)
  36. .field("http_cache", &self.http_cache)
  37. .field("enable_swagger_ui", &self.enable_swagger_ui)
  38. .finish()
  39. }
  40. }
  41. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
  42. #[serde(rename_all = "lowercase")]
  43. pub enum LnBackend {
  44. #[default]
  45. None,
  46. #[cfg(feature = "cln")]
  47. Cln,
  48. #[cfg(feature = "lnbits")]
  49. LNbits,
  50. #[cfg(feature = "fakewallet")]
  51. FakeWallet,
  52. #[cfg(feature = "lnd")]
  53. Lnd,
  54. #[cfg(feature = "grpc-processor")]
  55. GrpcProcessor,
  56. }
  57. impl std::str::FromStr for LnBackend {
  58. type Err = String;
  59. fn from_str(s: &str) -> Result<Self, Self::Err> {
  60. match s.to_lowercase().as_str() {
  61. #[cfg(feature = "cln")]
  62. "cln" => Ok(LnBackend::Cln),
  63. #[cfg(feature = "lnbits")]
  64. "lnbits" => Ok(LnBackend::LNbits),
  65. #[cfg(feature = "fakewallet")]
  66. "fakewallet" => Ok(LnBackend::FakeWallet),
  67. #[cfg(feature = "lnd")]
  68. "lnd" => Ok(LnBackend::Lnd),
  69. #[cfg(feature = "grpc-processor")]
  70. "grpcprocessor" => Ok(LnBackend::GrpcProcessor),
  71. _ => Err(format!("Unknown Lightning backend: {}", s)),
  72. }
  73. }
  74. }
  75. #[derive(Debug, Clone, Serialize, Deserialize)]
  76. pub struct Ln {
  77. pub ln_backend: LnBackend,
  78. pub invoice_description: Option<String>,
  79. pub min_mint: Amount,
  80. pub max_mint: Amount,
  81. pub min_melt: Amount,
  82. pub max_melt: Amount,
  83. }
  84. impl Default for Ln {
  85. fn default() -> Self {
  86. Ln {
  87. ln_backend: LnBackend::default(),
  88. invoice_description: None,
  89. min_mint: 1.into(),
  90. max_mint: 500_000.into(),
  91. min_melt: 1.into(),
  92. max_melt: 500_000.into(),
  93. }
  94. }
  95. }
  96. #[cfg(feature = "lnbits")]
  97. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  98. pub struct LNbits {
  99. pub admin_api_key: String,
  100. pub invoice_api_key: String,
  101. pub lnbits_api: String,
  102. pub fee_percent: f32,
  103. pub reserve_fee_min: Amount,
  104. }
  105. #[cfg(feature = "cln")]
  106. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  107. pub struct Cln {
  108. pub rpc_path: PathBuf,
  109. #[serde(default)]
  110. pub bolt12: bool,
  111. pub fee_percent: f32,
  112. pub reserve_fee_min: Amount,
  113. }
  114. #[cfg(feature = "lnd")]
  115. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  116. pub struct Lnd {
  117. pub address: String,
  118. pub cert_file: PathBuf,
  119. pub macaroon_file: PathBuf,
  120. pub fee_percent: f32,
  121. pub reserve_fee_min: Amount,
  122. }
  123. #[cfg(feature = "fakewallet")]
  124. #[derive(Debug, Clone, Serialize, Deserialize)]
  125. pub struct FakeWallet {
  126. pub supported_units: Vec<CurrencyUnit>,
  127. pub fee_percent: f32,
  128. pub reserve_fee_min: Amount,
  129. #[serde(default = "default_min_delay_time")]
  130. pub min_delay_time: u64,
  131. #[serde(default = "default_max_delay_time")]
  132. pub max_delay_time: u64,
  133. }
  134. #[cfg(feature = "fakewallet")]
  135. impl Default for FakeWallet {
  136. fn default() -> Self {
  137. Self {
  138. supported_units: vec![CurrencyUnit::Sat],
  139. fee_percent: 0.02,
  140. reserve_fee_min: 2.into(),
  141. min_delay_time: 1,
  142. max_delay_time: 3,
  143. }
  144. }
  145. }
  146. // Helper functions to provide default values
  147. #[cfg(feature = "fakewallet")]
  148. fn default_min_delay_time() -> u64 {
  149. 1
  150. }
  151. #[cfg(feature = "fakewallet")]
  152. fn default_max_delay_time() -> u64 {
  153. 3
  154. }
  155. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
  156. pub struct GrpcProcessor {
  157. pub supported_units: Vec<CurrencyUnit>,
  158. pub addr: String,
  159. pub port: u16,
  160. pub tls_dir: Option<PathBuf>,
  161. }
  162. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
  163. #[serde(rename_all = "lowercase")]
  164. pub enum DatabaseEngine {
  165. #[default]
  166. Sqlite,
  167. #[cfg(feature = "redb")]
  168. Redb,
  169. }
  170. impl std::str::FromStr for DatabaseEngine {
  171. type Err = String;
  172. fn from_str(s: &str) -> Result<Self, Self::Err> {
  173. match s.to_lowercase().as_str() {
  174. "sqlite" => Ok(DatabaseEngine::Sqlite),
  175. #[cfg(feature = "redb")]
  176. "redb" => Ok(DatabaseEngine::Redb),
  177. _ => Err(format!("Unknown database engine: {}", s)),
  178. }
  179. }
  180. }
  181. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  182. pub struct Database {
  183. pub engine: DatabaseEngine,
  184. }
  185. #[derive(Debug, Clone, Default, Serialize, Deserialize)]
  186. pub struct Auth {
  187. pub openid_discovery: String,
  188. pub openid_client_id: String,
  189. pub mint_max_bat: u64,
  190. #[serde(default = "default_true")]
  191. pub enabled_mint: bool,
  192. #[serde(default = "default_true")]
  193. pub enabled_melt: bool,
  194. #[serde(default = "default_true")]
  195. pub enabled_swap: bool,
  196. #[serde(default = "default_true")]
  197. pub enabled_check_mint_quote: bool,
  198. #[serde(default = "default_true")]
  199. pub enabled_check_melt_quote: bool,
  200. #[serde(default = "default_true")]
  201. pub enabled_restore: bool,
  202. #[serde(default = "default_true")]
  203. pub enabled_check_proof_state: bool,
  204. }
  205. fn default_true() -> bool {
  206. true
  207. }
  208. /// CDK settings, derived from `config.toml`
  209. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  210. pub struct Settings {
  211. pub info: Info,
  212. pub mint_info: MintInfo,
  213. pub ln: Ln,
  214. #[cfg(feature = "cln")]
  215. pub cln: Option<Cln>,
  216. #[cfg(feature = "lnbits")]
  217. pub lnbits: Option<LNbits>,
  218. #[cfg(feature = "lnd")]
  219. pub lnd: Option<Lnd>,
  220. #[cfg(feature = "fakewallet")]
  221. pub fake_wallet: Option<FakeWallet>,
  222. pub grpc_processor: Option<GrpcProcessor>,
  223. pub database: Database,
  224. #[cfg(feature = "management-rpc")]
  225. pub mint_management_rpc: Option<MintManagementRpc>,
  226. pub auth: Option<Auth>,
  227. }
  228. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  229. pub struct MintInfo {
  230. /// name of the mint and should be recognizable
  231. pub name: String,
  232. /// hex pubkey of the mint
  233. pub pubkey: Option<PublicKey>,
  234. /// short description of the mint
  235. pub description: String,
  236. /// long description
  237. pub description_long: Option<String>,
  238. /// url to the mint icon
  239. pub icon_url: Option<String>,
  240. /// message of the day that the wallet must display to the user
  241. pub motd: Option<String>,
  242. /// Nostr publickey
  243. pub contact_nostr_public_key: Option<String>,
  244. /// Contact email
  245. pub contact_email: Option<String>,
  246. /// URL to the terms of service
  247. pub tos_url: Option<String>,
  248. }
  249. #[cfg(feature = "management-rpc")]
  250. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  251. pub struct MintManagementRpc {
  252. /// When this is set to `true` the mint use the config file for the initial set up on first start.
  253. /// Changes to the `[mint_info]` after this **MUST** be made via the RPC changes to the config file or env vars will be ignored.
  254. pub enabled: bool,
  255. pub address: Option<String>,
  256. pub port: Option<u16>,
  257. pub tls_dir_path: Option<PathBuf>,
  258. }
  259. impl Settings {
  260. #[must_use]
  261. pub fn new<P>(config_file_name: Option<P>) -> Self
  262. where
  263. P: Into<PathBuf>,
  264. {
  265. let default_settings = Self::default();
  266. // attempt to construct settings with file
  267. let from_file = Self::new_from_default(&default_settings, config_file_name);
  268. match from_file {
  269. Ok(f) => f,
  270. Err(e) => {
  271. tracing::error!(
  272. "Error reading config file, falling back to defaults. Error: {e:?}"
  273. );
  274. default_settings
  275. }
  276. }
  277. }
  278. fn new_from_default<P>(
  279. default: &Settings,
  280. config_file_name: Option<P>,
  281. ) -> Result<Self, ConfigError>
  282. where
  283. P: Into<PathBuf>,
  284. {
  285. let mut default_config_file_name = home::home_dir()
  286. .ok_or(ConfigError::NotFound("Config Path".to_string()))?
  287. .join("cashu-rs-mint");
  288. default_config_file_name.push("config.toml");
  289. let config: String = match config_file_name {
  290. Some(value) => value.into().to_string_lossy().to_string(),
  291. None => default_config_file_name.to_string_lossy().to_string(),
  292. };
  293. let builder = Config::builder();
  294. let config: Config = builder
  295. // use defaults
  296. .add_source(Config::try_from(default)?)
  297. // override with file contents
  298. .add_source(File::with_name(&config))
  299. .build()?;
  300. let settings: Settings = config.try_deserialize()?;
  301. match settings.ln.ln_backend {
  302. LnBackend::None => panic!("Ln backend must be set"),
  303. #[cfg(feature = "cln")]
  304. LnBackend::Cln => assert!(
  305. settings.cln.is_some(),
  306. "CLN backend requires a valid config."
  307. ),
  308. #[cfg(feature = "lnbits")]
  309. LnBackend::LNbits => assert!(
  310. settings.lnbits.is_some(),
  311. "LNbits backend requires a valid config"
  312. ),
  313. #[cfg(feature = "lnd")]
  314. LnBackend::Lnd => {
  315. assert!(
  316. settings.lnd.is_some(),
  317. "LND backend requires a valid config."
  318. )
  319. }
  320. #[cfg(feature = "fakewallet")]
  321. LnBackend::FakeWallet => assert!(
  322. settings.fake_wallet.is_some(),
  323. "FakeWallet backend requires a valid config."
  324. ),
  325. #[cfg(feature = "grpc-processor")]
  326. LnBackend::GrpcProcessor => {
  327. assert!(
  328. settings.grpc_processor.is_some(),
  329. "GRPC backend requires a valid config."
  330. )
  331. }
  332. }
  333. Ok(settings)
  334. }
  335. }
  336. #[cfg(test)]
  337. mod tests {
  338. use super::*;
  339. #[test]
  340. fn test_info_debug_impl() {
  341. // Create a sample Info struct with test data
  342. let info = Info {
  343. url: "http://example.com".to_string(),
  344. listen_host: "127.0.0.1".to_string(),
  345. listen_port: 8080,
  346. mnemonic: "test secret mnemonic phrase".to_string(),
  347. input_fee_ppk: Some(100),
  348. ..Default::default()
  349. };
  350. // Convert the Info struct to a debug string
  351. let debug_output = format!("{:?}", info);
  352. // Verify the debug output contains expected fields
  353. assert!(debug_output.contains("url: \"http://example.com\""));
  354. assert!(debug_output.contains("listen_host: \"127.0.0.1\""));
  355. assert!(debug_output.contains("listen_port: 8080"));
  356. // The mnemonic should be hashed, not displayed in plaintext
  357. assert!(!debug_output.contains("test secret mnemonic phrase"));
  358. assert!(debug_output.contains("<hashed: "));
  359. assert!(debug_output.contains("input_fee_ppk: 100"));
  360. assert!(debug_output.contains("http_cache: true"));
  361. assert!(debug_output.contains("enable_swagger_ui: false"));
  362. }
  363. #[test]
  364. fn test_info_debug_with_empty_mnemonic() {
  365. // Test with an empty mnemonic to ensure it doesn't panic
  366. let info = Info {
  367. url: "http://example.com".to_string(),
  368. listen_host: "127.0.0.1".to_string(),
  369. listen_port: 8080,
  370. mnemonic: "".to_string(), // Empty mnemonic
  371. enable_swagger_ui: Some(false),
  372. ..Default::default()
  373. };
  374. // This should not panic
  375. let debug_output = format!("{:?}", info);
  376. // The empty mnemonic should still be hashed
  377. assert!(debug_output.contains("<hashed: "));
  378. }
  379. #[test]
  380. fn test_info_debug_with_special_chars() {
  381. // Test with a mnemonic containing special characters
  382. let info = Info {
  383. url: "http://example.com".to_string(),
  384. listen_host: "127.0.0.1".to_string(),
  385. listen_port: 8080,
  386. mnemonic: "特殊字符 !@#$%^&*()".to_string(), // Special characters
  387. ..Default::default()
  388. };
  389. // This should not panic
  390. let debug_output = format!("{:?}", info);
  391. // The mnemonic with special chars should be hashed
  392. assert!(!debug_output.contains("特殊字符 !@#$%^&*()"));
  393. assert!(debug_output.contains("<hashed: "));
  394. }
  395. }