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: 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, Default, Serialize, Deserialize)]
  188. pub struct Auth {
  189. pub openid_discovery: String,
  190. pub openid_client_id: String,
  191. pub mint_max_bat: u64,
  192. #[serde(default = "default_true")]
  193. pub enabled_mint: bool,
  194. #[serde(default = "default_true")]
  195. pub enabled_melt: bool,
  196. #[serde(default = "default_true")]
  197. pub enabled_swap: bool,
  198. #[serde(default = "default_true")]
  199. pub enabled_check_mint_quote: bool,
  200. #[serde(default = "default_true")]
  201. pub enabled_check_melt_quote: bool,
  202. #[serde(default = "default_true")]
  203. pub enabled_restore: bool,
  204. #[serde(default = "default_true")]
  205. pub enabled_check_proof_state: bool,
  206. }
  207. fn default_true() -> bool {
  208. true
  209. }
  210. /// CDK settings, derived from `config.toml`
  211. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  212. pub struct Settings {
  213. pub info: Info,
  214. pub mint_info: MintInfo,
  215. pub ln: Ln,
  216. #[cfg(feature = "cln")]
  217. pub cln: Option<Cln>,
  218. #[cfg(feature = "lnbits")]
  219. pub lnbits: Option<LNbits>,
  220. #[cfg(feature = "lnd")]
  221. pub lnd: Option<Lnd>,
  222. #[cfg(feature = "fakewallet")]
  223. pub fake_wallet: Option<FakeWallet>,
  224. pub grpc_processor: Option<GrpcProcessor>,
  225. pub database: Database,
  226. #[cfg(feature = "management-rpc")]
  227. pub mint_management_rpc: Option<MintManagementRpc>,
  228. pub auth: Option<Auth>,
  229. }
  230. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  231. pub struct MintInfo {
  232. /// name of the mint and should be recognizable
  233. pub name: String,
  234. /// hex pubkey of the mint
  235. pub pubkey: Option<PublicKey>,
  236. /// short description of the mint
  237. pub description: String,
  238. /// long description
  239. pub description_long: Option<String>,
  240. /// url to the mint icon
  241. pub icon_url: Option<String>,
  242. /// message of the day that the wallet must display to the user
  243. pub motd: Option<String>,
  244. /// Nostr publickey
  245. pub contact_nostr_public_key: Option<String>,
  246. /// Contact email
  247. pub contact_email: Option<String>,
  248. /// URL to the terms of service
  249. pub tos_url: Option<String>,
  250. }
  251. #[cfg(feature = "management-rpc")]
  252. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  253. pub struct MintManagementRpc {
  254. /// When this is set to `true` the mint use the config file for the initial set up on first start.
  255. /// Changes to the `[mint_info]` after this **MUST** be made via the RPC changes to the config file or env vars will be ignored.
  256. pub enabled: bool,
  257. pub address: Option<String>,
  258. pub port: Option<u16>,
  259. pub tls_dir_path: Option<PathBuf>,
  260. }
  261. impl Settings {
  262. #[must_use]
  263. pub fn new<P>(config_file_name: Option<P>) -> Self
  264. where
  265. P: Into<PathBuf>,
  266. {
  267. let default_settings = Self::default();
  268. // attempt to construct settings with file
  269. let from_file = Self::new_from_default(&default_settings, config_file_name);
  270. match from_file {
  271. Ok(f) => f,
  272. Err(e) => {
  273. tracing::error!(
  274. "Error reading config file, falling back to defaults. Error: {e:?}"
  275. );
  276. default_settings
  277. }
  278. }
  279. }
  280. fn new_from_default<P>(
  281. default: &Settings,
  282. config_file_name: Option<P>,
  283. ) -> Result<Self, ConfigError>
  284. where
  285. P: Into<PathBuf>,
  286. {
  287. let mut default_config_file_name = home::home_dir()
  288. .ok_or(ConfigError::NotFound("Config Path".to_string()))?
  289. .join("cashu-rs-mint");
  290. default_config_file_name.push("config.toml");
  291. let config: String = match config_file_name {
  292. Some(value) => value.into().to_string_lossy().to_string(),
  293. None => default_config_file_name.to_string_lossy().to_string(),
  294. };
  295. let builder = Config::builder();
  296. let config: Config = builder
  297. // use defaults
  298. .add_source(Config::try_from(default)?)
  299. // override with file contents
  300. .add_source(File::with_name(&config))
  301. .build()?;
  302. let settings: Settings = config.try_deserialize()?;
  303. match settings.ln.ln_backend {
  304. LnBackend::None => panic!("Ln backend must be set"),
  305. #[cfg(feature = "cln")]
  306. LnBackend::Cln => assert!(
  307. settings.cln.is_some(),
  308. "CLN backend requires a valid config."
  309. ),
  310. #[cfg(feature = "lnbits")]
  311. LnBackend::LNbits => assert!(
  312. settings.lnbits.is_some(),
  313. "LNbits backend requires a valid config"
  314. ),
  315. #[cfg(feature = "lnd")]
  316. LnBackend::Lnd => {
  317. assert!(
  318. settings.lnd.is_some(),
  319. "LND backend requires a valid config."
  320. )
  321. }
  322. #[cfg(feature = "fakewallet")]
  323. LnBackend::FakeWallet => assert!(
  324. settings.fake_wallet.is_some(),
  325. "FakeWallet backend requires a valid config."
  326. ),
  327. #[cfg(feature = "grpc-processor")]
  328. LnBackend::GrpcProcessor => {
  329. assert!(
  330. settings.grpc_processor.is_some(),
  331. "GRPC backend requires a valid config."
  332. )
  333. }
  334. }
  335. Ok(settings)
  336. }
  337. }
  338. #[cfg(test)]
  339. mod tests {
  340. use super::*;
  341. #[test]
  342. fn test_info_debug_impl() {
  343. // Create a sample Info struct with test data
  344. let info = Info {
  345. url: "http://example.com".to_string(),
  346. listen_host: "127.0.0.1".to_string(),
  347. listen_port: 8080,
  348. mnemonic: Some("test secret mnemonic phrase".to_string()),
  349. input_fee_ppk: Some(100),
  350. ..Default::default()
  351. };
  352. // Convert the Info struct to a debug string
  353. let debug_output = format!("{info:?}");
  354. // Verify the debug output contains expected fields
  355. assert!(debug_output.contains("url: \"http://example.com\""));
  356. assert!(debug_output.contains("listen_host: \"127.0.0.1\""));
  357. assert!(debug_output.contains("listen_port: 8080"));
  358. // The mnemonic should be hashed, not displayed in plaintext
  359. assert!(!debug_output.contains("test secret mnemonic phrase"));
  360. assert!(debug_output.contains("<hashed: "));
  361. assert!(debug_output.contains("input_fee_ppk: Some(100)"));
  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: Some("".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: Some("特殊字符 !@#$%^&*()".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. }