config.rs 13 KB

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