1use std::path::PathBuf;
2
3use bitcoin::hashes::{sha256, Hash};
4use cdk::nuts::{CurrencyUnit, PublicKey};
5use cdk::Amount;
6use cdk_axum::cache;
7use config::{Config, ConfigError, File};
8use serde::{Deserialize, Serialize};
9
10#[derive(Clone, Serialize, Deserialize, Default)]
11pub struct Info {
12 pub url: String,
13 pub listen_host: String,
14 pub listen_port: u16,
15 pub mnemonic: String,
16 pub input_fee_ppk: Option<u64>,
17
18 pub http_cache: cache::Config,
19
20 pub enable_swagger_ui: Option<bool>,
25}
26
27impl std::fmt::Debug for Info {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 let mnemonic_display = {
31 let hash = sha256::Hash::hash(self.mnemonic.clone().into_bytes().as_ref());
32 format!("<hashed: {hash}>")
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
47#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
48#[serde(rename_all = "lowercase")]
49pub enum LnBackend {
50 #[default]
51 None,
52 #[cfg(feature = "cln")]
53 Cln,
54 #[cfg(feature = "lnbits")]
55 LNbits,
56 #[cfg(feature = "fakewallet")]
57 FakeWallet,
58 #[cfg(feature = "lnd")]
59 Lnd,
60 #[cfg(feature = "grpc-processor")]
61 GrpcProcessor,
62}
63
64impl std::str::FromStr for LnBackend {
65 type Err = String;
66
67 fn from_str(s: &str) -> Result<Self, Self::Err> {
68 match s.to_lowercase().as_str() {
69 #[cfg(feature = "cln")]
70 "cln" => Ok(LnBackend::Cln),
71 #[cfg(feature = "lnbits")]
72 "lnbits" => Ok(LnBackend::LNbits),
73 #[cfg(feature = "fakewallet")]
74 "fakewallet" => Ok(LnBackend::FakeWallet),
75 #[cfg(feature = "lnd")]
76 "lnd" => Ok(LnBackend::Lnd),
77 #[cfg(feature = "grpc-processor")]
78 "grpcprocessor" => Ok(LnBackend::GrpcProcessor),
79 _ => Err(format!("Unknown Lightning backend: {s}")),
80 }
81 }
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct Ln {
86 pub ln_backend: LnBackend,
87 pub invoice_description: Option<String>,
88 pub min_mint: Amount,
89 pub max_mint: Amount,
90 pub min_melt: Amount,
91 pub max_melt: Amount,
92}
93
94impl Default for Ln {
95 fn default() -> Self {
96 Ln {
97 ln_backend: LnBackend::default(),
98 invoice_description: None,
99 min_mint: 1.into(),
100 max_mint: 500_000.into(),
101 min_melt: 1.into(),
102 max_melt: 500_000.into(),
103 }
104 }
105}
106
107#[cfg(feature = "lnbits")]
108#[derive(Debug, Clone, Serialize, Deserialize, Default)]
109pub struct LNbits {
110 pub admin_api_key: String,
111 pub invoice_api_key: String,
112 pub lnbits_api: String,
113 pub fee_percent: f32,
114 pub reserve_fee_min: Amount,
115}
116
117#[cfg(feature = "cln")]
118#[derive(Debug, Clone, Serialize, Deserialize, Default)]
119pub struct Cln {
120 pub rpc_path: PathBuf,
121 #[serde(default)]
122 pub bolt12: bool,
123 pub fee_percent: f32,
124 pub reserve_fee_min: Amount,
125}
126
127#[cfg(feature = "lnd")]
128#[derive(Debug, Clone, Serialize, Deserialize, Default)]
129pub struct Lnd {
130 pub address: String,
131 pub cert_file: PathBuf,
132 pub macaroon_file: PathBuf,
133 pub fee_percent: f32,
134 pub reserve_fee_min: Amount,
135}
136
137#[cfg(feature = "fakewallet")]
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct FakeWallet {
140 pub supported_units: Vec<CurrencyUnit>,
141 pub fee_percent: f32,
142 pub reserve_fee_min: Amount,
143 #[serde(default = "default_min_delay_time")]
144 pub min_delay_time: u64,
145 #[serde(default = "default_max_delay_time")]
146 pub max_delay_time: u64,
147}
148
149#[cfg(feature = "fakewallet")]
150impl Default for FakeWallet {
151 fn default() -> Self {
152 Self {
153 supported_units: vec![CurrencyUnit::Sat],
154 fee_percent: 0.02,
155 reserve_fee_min: 2.into(),
156 min_delay_time: 1,
157 max_delay_time: 3,
158 }
159 }
160}
161
162#[cfg(feature = "fakewallet")]
164fn default_min_delay_time() -> u64 {
165 1
166}
167
168#[cfg(feature = "fakewallet")]
169fn default_max_delay_time() -> u64 {
170 3
171}
172
173#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
174pub struct GrpcProcessor {
175 pub supported_units: Vec<CurrencyUnit>,
176 pub addr: String,
177 pub port: u16,
178 pub tls_dir: Option<PathBuf>,
179}
180
181#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
182#[serde(rename_all = "lowercase")]
183pub enum DatabaseEngine {
184 #[default]
185 Sqlite,
186 #[cfg(feature = "redb")]
187 Redb,
188}
189
190impl std::str::FromStr for DatabaseEngine {
191 type Err = String;
192
193 fn from_str(s: &str) -> Result<Self, Self::Err> {
194 match s.to_lowercase().as_str() {
195 "sqlite" => Ok(DatabaseEngine::Sqlite),
196 #[cfg(feature = "redb")]
197 "redb" => Ok(DatabaseEngine::Redb),
198 _ => Err(format!("Unknown database engine: {s}")),
199 }
200 }
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize, Default)]
204pub struct Database {
205 pub engine: DatabaseEngine,
206}
207
208#[derive(Debug, Clone, Default, Serialize, Deserialize)]
209pub struct Auth {
210 pub openid_discovery: String,
211 pub openid_client_id: String,
212 pub mint_max_bat: u64,
213 #[serde(default = "default_true")]
214 pub enabled_mint: bool,
215 #[serde(default = "default_true")]
216 pub enabled_melt: bool,
217 #[serde(default = "default_true")]
218 pub enabled_swap: bool,
219 #[serde(default = "default_true")]
220 pub enabled_check_mint_quote: bool,
221 #[serde(default = "default_true")]
222 pub enabled_check_melt_quote: bool,
223 #[serde(default = "default_true")]
224 pub enabled_restore: bool,
225 #[serde(default = "default_true")]
226 pub enabled_check_proof_state: bool,
227}
228
229fn default_true() -> bool {
230 true
231}
232#[derive(Debug, Clone, Serialize, Deserialize, Default)]
234pub struct Settings {
235 pub info: Info,
236 pub mint_info: MintInfo,
237 pub ln: Ln,
238 #[cfg(feature = "cln")]
239 pub cln: Option<Cln>,
240 #[cfg(feature = "lnbits")]
241 pub lnbits: Option<LNbits>,
242 #[cfg(feature = "lnd")]
243 pub lnd: Option<Lnd>,
244 #[cfg(feature = "fakewallet")]
245 pub fake_wallet: Option<FakeWallet>,
246 pub grpc_processor: Option<GrpcProcessor>,
247 pub database: Database,
248 #[cfg(feature = "management-rpc")]
249 pub mint_management_rpc: Option<MintManagementRpc>,
250 pub auth: Option<Auth>,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize, Default)]
254pub struct MintInfo {
255 pub name: String,
257 pub pubkey: Option<PublicKey>,
259 pub description: String,
261 pub description_long: Option<String>,
263 pub icon_url: Option<String>,
265 pub motd: Option<String>,
267 pub contact_nostr_public_key: Option<String>,
269 pub contact_email: Option<String>,
271 pub tos_url: Option<String>,
273}
274
275#[cfg(feature = "management-rpc")]
276#[derive(Debug, Clone, Serialize, Deserialize, Default)]
277pub struct MintManagementRpc {
278 pub enabled: bool,
281 pub address: Option<String>,
282 pub port: Option<u16>,
283 pub tls_dir_path: Option<PathBuf>,
284}
285
286impl 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 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
306 fn new_from_default<P>(
307 default: &Settings,
308 config_file_name: Option<P>,
309 ) -> Result<Self, ConfigError>
310 where
311 P: Into<PathBuf>,
312 {
313 let mut default_config_file_name = home::home_dir()
314 .ok_or(ConfigError::NotFound("Config Path".to_string()))?
315 .join("cashu-rs-mint");
316
317 default_config_file_name.push("config.toml");
318 let config: String = match config_file_name {
319 Some(value) => value.into().to_string_lossy().to_string(),
320 None => default_config_file_name.to_string_lossy().to_string(),
321 };
322 let builder = Config::builder();
323 let config: Config = builder
324 .add_source(Config::try_from(default)?)
326 .add_source(File::with_name(&config))
328 .build()?;
329 let settings: Settings = config.try_deserialize()?;
330
331 match settings.ln.ln_backend {
332 LnBackend::None => panic!("Ln backend must be set"),
333 #[cfg(feature = "cln")]
334 LnBackend::Cln => assert!(
335 settings.cln.is_some(),
336 "CLN backend requires a valid config."
337 ),
338 #[cfg(feature = "lnbits")]
339 LnBackend::LNbits => assert!(
340 settings.lnbits.is_some(),
341 "LNbits backend requires a valid config"
342 ),
343 #[cfg(feature = "lnd")]
344 LnBackend::Lnd => {
345 assert!(
346 settings.lnd.is_some(),
347 "LND backend requires a valid config."
348 )
349 }
350 #[cfg(feature = "fakewallet")]
351 LnBackend::FakeWallet => assert!(
352 settings.fake_wallet.is_some(),
353 "FakeWallet backend requires a valid config."
354 ),
355 #[cfg(feature = "grpc-processor")]
356 LnBackend::GrpcProcessor => {
357 assert!(
358 settings.grpc_processor.is_some(),
359 "GRPC backend requires a valid config."
360 )
361 }
362 }
363
364 Ok(settings)
365 }
366}
367
368#[cfg(test)]
369mod tests {
370
371 use super::*;
372
373 #[test]
374 fn test_info_debug_impl() {
375 let info = Info {
377 url: "http://example.com".to_string(),
378 listen_host: "127.0.0.1".to_string(),
379 listen_port: 8080,
380 mnemonic: "test secret mnemonic phrase".to_string(),
381 input_fee_ppk: Some(100),
382 ..Default::default()
383 };
384
385 let debug_output = format!("{info:?}");
387
388 assert!(debug_output.contains("url: \"http://example.com\""));
390 assert!(debug_output.contains("listen_host: \"127.0.0.1\""));
391 assert!(debug_output.contains("listen_port: 8080"));
392
393 assert!(!debug_output.contains("test secret mnemonic phrase"));
395 assert!(debug_output.contains("<hashed: "));
396
397 assert!(debug_output.contains("input_fee_ppk: Some(100)"));
398 }
399
400 #[test]
401 fn test_info_debug_with_empty_mnemonic() {
402 let info = Info {
404 url: "http://example.com".to_string(),
405 listen_host: "127.0.0.1".to_string(),
406 listen_port: 8080,
407 mnemonic: "".to_string(), enable_swagger_ui: Some(false),
409 ..Default::default()
410 };
411
412 let debug_output = format!("{:?}", info);
414
415 assert!(debug_output.contains("<hashed: "));
417 }
418
419 #[test]
420 fn test_info_debug_with_special_chars() {
421 let info = Info {
423 url: "http://example.com".to_string(),
424 listen_host: "127.0.0.1".to_string(),
425 listen_port: 8080,
426 mnemonic: "特殊字符 !@#$%^&*()".to_string(), ..Default::default()
428 };
429
430 let debug_output = format!("{:?}", info);
432
433 assert!(!debug_output.contains("特殊字符 !@#$%^&*()"));
435 assert!(debug_output.contains("<hashed: "));
436 }
437}