config.rs 14 KB

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