config.rs 13 KB

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