1use std::path::PathBuf;
2
3use bitcoin::hashes::{sha256, Hash};
4use cdk::nuts::{CurrencyUnit, PublicKey};
5use cdk::Amount;
6use cdk_axum::cache;
7use cdk_common::common::QuoteTTL;
8use config::{Config, ConfigError, File};
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
12#[serde(rename_all = "lowercase")]
13pub enum LoggingOutput {
14 Stderr,
16 File,
18 #[default]
20 Both,
21}
22
23impl std::str::FromStr for LoggingOutput {
24 type Err = String;
25
26 fn from_str(s: &str) -> Result<Self, Self::Err> {
27 match s.to_lowercase().as_str() {
28 "stderr" => Ok(LoggingOutput::Stderr),
29 "file" => Ok(LoggingOutput::File),
30 "both" => Ok(LoggingOutput::Both),
31 _ => Err(format!(
32 "Unknown logging output: {s}. Valid options: stdout, file, both"
33 )),
34 }
35 }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, Default)]
39pub struct LoggingConfig {
40 #[serde(default)]
42 pub output: LoggingOutput,
43 pub console_level: Option<String>,
45 pub file_level: Option<String>,
47}
48
49#[derive(Clone, Serialize, Deserialize)]
50pub struct Info {
51 pub url: String,
52 pub listen_host: String,
53 pub listen_port: u16,
54 pub seed: Option<String>,
56 pub mnemonic: Option<String>,
57 pub signatory_url: Option<String>,
58 pub signatory_certs: Option<String>,
59 pub input_fee_ppk: Option<u64>,
60
61 pub http_cache: cache::Config,
62
63 #[serde(default)]
65 pub logging: LoggingConfig,
66
67 pub enable_swagger_ui: Option<bool>,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
77 pub quote_ttl: Option<QuoteTTL>,
78}
79
80impl Default for Info {
81 fn default() -> Self {
82 Info {
83 url: String::new(),
84 listen_host: "127.0.0.1".to_string(),
85 listen_port: 8091, seed: None,
87 mnemonic: None,
88 signatory_url: None,
89 signatory_certs: None,
90 input_fee_ppk: None,
91 http_cache: cache::Config::default(),
92 enable_swagger_ui: None,
93 logging: LoggingConfig::default(),
94 quote_ttl: None,
95 }
96 }
97}
98
99impl std::fmt::Debug for Info {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 let mnemonic_display: String = {
103 if let Some(mnemonic) = self.mnemonic.as_ref() {
104 let hash = sha256::Hash::hash(mnemonic.as_bytes());
105 format!("<hashed: {hash}>")
106 } else {
107 format!("<url: {}>", self.signatory_url.clone().unwrap_or_default())
108 }
109 };
110
111 f.debug_struct("Info")
112 .field("url", &self.url)
113 .field("listen_host", &self.listen_host)
114 .field("listen_port", &self.listen_port)
115 .field("mnemonic", &mnemonic_display)
116 .field("input_fee_ppk", &self.input_fee_ppk)
117 .field("http_cache", &self.http_cache)
118 .field("logging", &self.logging)
119 .field("enable_swagger_ui", &self.enable_swagger_ui)
120 .finish()
121 }
122}
123
124#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
125#[serde(rename_all = "lowercase")]
126pub enum LnBackend {
127 #[default]
128 None,
129 #[cfg(feature = "cln")]
130 Cln,
131 #[cfg(feature = "lnbits")]
132 LNbits,
133 #[cfg(feature = "fakewallet")]
134 FakeWallet,
135 #[cfg(feature = "lnd")]
136 Lnd,
137 #[cfg(feature = "ldk-node")]
138 LdkNode,
139 #[cfg(feature = "grpc-processor")]
140 GrpcProcessor,
141}
142
143impl std::str::FromStr for LnBackend {
144 type Err = String;
145
146 fn from_str(s: &str) -> Result<Self, Self::Err> {
147 match s.to_lowercase().as_str() {
148 #[cfg(feature = "cln")]
149 "cln" => Ok(LnBackend::Cln),
150 #[cfg(feature = "lnbits")]
151 "lnbits" => Ok(LnBackend::LNbits),
152 #[cfg(feature = "fakewallet")]
153 "fakewallet" => Ok(LnBackend::FakeWallet),
154 #[cfg(feature = "lnd")]
155 "lnd" => Ok(LnBackend::Lnd),
156 #[cfg(feature = "ldk-node")]
157 "ldk-node" | "ldknode" => Ok(LnBackend::LdkNode),
158 #[cfg(feature = "grpc-processor")]
159 "grpcprocessor" => Ok(LnBackend::GrpcProcessor),
160 _ => Err(format!("Unknown Lightning backend: {s}")),
161 }
162 }
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct Ln {
167 pub ln_backend: LnBackend,
168 pub invoice_description: Option<String>,
169 pub min_mint: Amount,
170 pub max_mint: Amount,
171 pub min_melt: Amount,
172 pub max_melt: Amount,
173}
174
175impl Default for Ln {
176 fn default() -> Self {
177 Ln {
178 ln_backend: LnBackend::default(),
179 invoice_description: None,
180 min_mint: 1.into(),
181 max_mint: 500_000.into(),
182 min_melt: 1.into(),
183 max_melt: 500_000.into(),
184 }
185 }
186}
187
188#[cfg(feature = "lnbits")]
189#[derive(Debug, Clone, Serialize, Deserialize, Default)]
190pub struct LNbits {
191 pub admin_api_key: String,
192 pub invoice_api_key: String,
193 pub lnbits_api: String,
194 pub fee_percent: f32,
195 pub reserve_fee_min: Amount,
196}
197
198#[cfg(feature = "cln")]
199#[derive(Debug, Clone, Serialize, Deserialize, Default)]
200pub struct Cln {
201 pub rpc_path: PathBuf,
202 #[serde(default)]
203 pub bolt12: bool,
204 pub fee_percent: f32,
205 pub reserve_fee_min: Amount,
206}
207
208#[cfg(feature = "lnd")]
209#[derive(Debug, Clone, Serialize, Deserialize, Default)]
210pub struct Lnd {
211 pub address: String,
212 pub cert_file: PathBuf,
213 pub macaroon_file: PathBuf,
214 pub fee_percent: f32,
215 pub reserve_fee_min: Amount,
216}
217
218#[cfg(feature = "ldk-node")]
219#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct LdkNode {
221 #[serde(default = "default_ldk_fee_percent")]
223 pub fee_percent: f32,
224 #[serde(default = "default_ldk_reserve_fee_min")]
226 pub reserve_fee_min: Amount,
227 pub bitcoin_network: Option<String>,
229 pub chain_source_type: Option<String>,
231 pub esplora_url: Option<String>,
233 pub bitcoind_rpc_host: Option<String>,
235 pub bitcoind_rpc_port: Option<u16>,
236 pub bitcoind_rpc_user: Option<String>,
237 pub bitcoind_rpc_password: Option<String>,
238 pub storage_dir_path: Option<String>,
240 pub ldk_node_host: Option<String>,
242 pub ldk_node_port: Option<u16>,
244 pub gossip_source_type: Option<String>,
246 pub rgs_url: Option<String>,
248 #[serde(default = "default_webserver_host")]
250 pub webserver_host: Option<String>,
251 #[serde(default = "default_webserver_port")]
253 pub webserver_port: Option<u16>,
254}
255
256#[cfg(feature = "ldk-node")]
257impl Default for LdkNode {
258 fn default() -> Self {
259 Self {
260 fee_percent: default_ldk_fee_percent(),
261 reserve_fee_min: default_ldk_reserve_fee_min(),
262 bitcoin_network: None,
263 chain_source_type: None,
264 esplora_url: None,
265 bitcoind_rpc_host: None,
266 bitcoind_rpc_port: None,
267 bitcoind_rpc_user: None,
268 bitcoind_rpc_password: None,
269 storage_dir_path: None,
270 ldk_node_host: None,
271 ldk_node_port: None,
272 gossip_source_type: None,
273 rgs_url: None,
274 webserver_host: default_webserver_host(),
275 webserver_port: default_webserver_port(),
276 }
277 }
278}
279
280#[cfg(feature = "ldk-node")]
281fn default_ldk_fee_percent() -> f32 {
282 0.04
283}
284
285#[cfg(feature = "ldk-node")]
286fn default_ldk_reserve_fee_min() -> Amount {
287 4.into()
288}
289
290#[cfg(feature = "ldk-node")]
291fn default_webserver_host() -> Option<String> {
292 Some("127.0.0.1".to_string())
293}
294
295#[cfg(feature = "ldk-node")]
296fn default_webserver_port() -> Option<u16> {
297 Some(8091)
298}
299
300#[cfg(feature = "fakewallet")]
301#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct FakeWallet {
303 pub supported_units: Vec<CurrencyUnit>,
304 pub fee_percent: f32,
305 pub reserve_fee_min: Amount,
306 #[serde(default = "default_min_delay_time")]
307 pub min_delay_time: u64,
308 #[serde(default = "default_max_delay_time")]
309 pub max_delay_time: u64,
310}
311
312#[cfg(feature = "fakewallet")]
313impl Default for FakeWallet {
314 fn default() -> Self {
315 Self {
316 supported_units: vec![CurrencyUnit::Sat],
317 fee_percent: 0.02,
318 reserve_fee_min: 2.into(),
319 min_delay_time: 1,
320 max_delay_time: 3,
321 }
322 }
323}
324
325#[cfg(feature = "fakewallet")]
327fn default_min_delay_time() -> u64 {
328 1
329}
330
331#[cfg(feature = "fakewallet")]
332fn default_max_delay_time() -> u64 {
333 3
334}
335
336#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
337pub struct GrpcProcessor {
338 pub supported_units: Vec<CurrencyUnit>,
339 pub addr: String,
340 pub port: u16,
341 pub tls_dir: Option<PathBuf>,
342}
343
344#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
345#[serde(rename_all = "lowercase")]
346pub enum DatabaseEngine {
347 #[default]
348 Sqlite,
349 Postgres,
350}
351
352impl std::str::FromStr for DatabaseEngine {
353 type Err = String;
354
355 fn from_str(s: &str) -> Result<Self, Self::Err> {
356 match s.to_lowercase().as_str() {
357 "sqlite" => Ok(DatabaseEngine::Sqlite),
358 "postgres" => Ok(DatabaseEngine::Postgres),
359 _ => Err(format!("Unknown database engine: {s}")),
360 }
361 }
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize, Default)]
365pub struct Database {
366 pub engine: DatabaseEngine,
367 pub postgres: Option<PostgresConfig>,
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize, Default)]
371pub struct AuthDatabase {
372 pub postgres: Option<PostgresAuthConfig>,
373}
374
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct PostgresAuthConfig {
377 pub url: String,
378 pub tls_mode: Option<String>,
379 pub max_connections: Option<usize>,
380 pub connection_timeout_seconds: Option<u64>,
381}
382
383impl Default for PostgresAuthConfig {
384 fn default() -> Self {
385 Self {
386 url: String::new(),
387 tls_mode: Some("disable".to_string()),
388 max_connections: Some(20),
389 connection_timeout_seconds: Some(10),
390 }
391 }
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct PostgresConfig {
396 pub url: String,
397 pub tls_mode: Option<String>,
398 pub max_connections: Option<usize>,
399 pub connection_timeout_seconds: Option<u64>,
400}
401
402impl Default for PostgresConfig {
403 fn default() -> Self {
404 Self {
405 url: String::new(),
406 tls_mode: Some("disable".to_string()),
407 max_connections: Some(20),
408 connection_timeout_seconds: Some(10),
409 }
410 }
411}
412
413#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
414#[serde(rename_all = "lowercase")]
415pub enum AuthType {
416 Clear,
417 Blind,
418 #[default]
419 None,
420}
421
422impl std::str::FromStr for AuthType {
423 type Err = String;
424
425 fn from_str(s: &str) -> Result<Self, Self::Err> {
426 match s.to_lowercase().as_str() {
427 "clear" => Ok(AuthType::Clear),
428 "blind" => Ok(AuthType::Blind),
429 "none" => Ok(AuthType::None),
430 _ => Err(format!("Unknown auth type: {s}")),
431 }
432 }
433}
434
435#[derive(Debug, Clone, Default, Serialize, Deserialize)]
436pub struct Auth {
437 #[serde(default)]
438 pub auth_enabled: bool,
439 pub openid_discovery: String,
440 pub openid_client_id: String,
441 pub mint_max_bat: u64,
442 #[serde(default = "default_blind")]
443 pub mint: AuthType,
444 #[serde(default)]
445 pub get_mint_quote: AuthType,
446 #[serde(default)]
447 pub check_mint_quote: AuthType,
448 #[serde(default)]
449 pub melt: AuthType,
450 #[serde(default)]
451 pub get_melt_quote: AuthType,
452 #[serde(default)]
453 pub check_melt_quote: AuthType,
454 #[serde(default = "default_blind")]
455 pub swap: AuthType,
456 #[serde(default = "default_blind")]
457 pub restore: AuthType,
458 #[serde(default)]
459 pub check_proof_state: AuthType,
460}
461
462fn default_blind() -> AuthType {
463 AuthType::Blind
464}
465
466#[derive(Debug, Clone, Serialize, Deserialize, Default)]
468pub struct Settings {
469 pub info: Info,
470 pub mint_info: MintInfo,
471 pub ln: Ln,
472 #[cfg(feature = "cln")]
473 pub cln: Option<Cln>,
474 #[cfg(feature = "lnbits")]
475 pub lnbits: Option<LNbits>,
476 #[cfg(feature = "lnd")]
477 pub lnd: Option<Lnd>,
478 #[cfg(feature = "ldk-node")]
479 pub ldk_node: Option<LdkNode>,
480 #[cfg(feature = "fakewallet")]
481 pub fake_wallet: Option<FakeWallet>,
482 pub grpc_processor: Option<GrpcProcessor>,
483 pub database: Database,
484 #[cfg(feature = "auth")]
485 pub auth_database: Option<AuthDatabase>,
486 #[cfg(feature = "management-rpc")]
487 pub mint_management_rpc: Option<MintManagementRpc>,
488 pub auth: Option<Auth>,
489 #[cfg(feature = "prometheus")]
490 pub prometheus: Option<Prometheus>,
491}
492
493#[derive(Debug, Clone, Serialize, Deserialize, Default)]
494#[cfg(feature = "prometheus")]
495pub struct Prometheus {
496 pub enabled: bool,
497 pub address: Option<String>,
498 pub port: Option<u16>,
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize, Default)]
502pub struct MintInfo {
503 pub name: String,
505 pub pubkey: Option<PublicKey>,
507 pub description: String,
509 pub description_long: Option<String>,
511 pub icon_url: Option<String>,
513 pub motd: Option<String>,
515 pub contact_nostr_public_key: Option<String>,
517 pub contact_email: Option<String>,
519 pub tos_url: Option<String>,
521}
522
523#[cfg(feature = "management-rpc")]
524#[derive(Debug, Clone, Serialize, Deserialize, Default)]
525pub struct MintManagementRpc {
526 pub enabled: bool,
529 pub address: Option<String>,
530 pub port: Option<u16>,
531 pub tls_dir_path: Option<PathBuf>,
532}
533
534impl Settings {
535 #[must_use]
536 pub fn new<P>(config_file_name: Option<P>) -> Self
537 where
538 P: Into<PathBuf>,
539 {
540 let default_settings = Self::default();
541 let from_file = Self::new_from_default(&default_settings, config_file_name);
543 match from_file {
544 Ok(f) => f,
545 Err(e) => {
546 tracing::error!(
547 "Error reading config file, falling back to defaults. Error: {e:?}"
548 );
549 default_settings
550 }
551 }
552 }
553
554 fn new_from_default<P>(
555 default: &Settings,
556 config_file_name: Option<P>,
557 ) -> Result<Self, ConfigError>
558 where
559 P: Into<PathBuf>,
560 {
561 let mut default_config_file_name = home::home_dir()
562 .ok_or(ConfigError::NotFound("Config Path".to_string()))?
563 .join("cashu-rs-mint");
564
565 default_config_file_name.push("config.toml");
566 let config: String = match config_file_name {
567 Some(value) => value.into().to_string_lossy().to_string(),
568 None => default_config_file_name.to_string_lossy().to_string(),
569 };
570 let builder = Config::builder();
571 let config: Config = builder
572 .add_source(Config::try_from(default)?)
574 .add_source(File::with_name(&config))
576 .build()?;
577 let settings: Settings = config.try_deserialize()?;
578
579 match settings.ln.ln_backend {
580 LnBackend::None => panic!("Ln backend must be set"),
581 #[cfg(feature = "cln")]
582 LnBackend::Cln => assert!(
583 settings.cln.is_some(),
584 "CLN backend requires a valid config."
585 ),
586 #[cfg(feature = "lnbits")]
587 LnBackend::LNbits => assert!(
588 settings.lnbits.is_some(),
589 "LNbits backend requires a valid config"
590 ),
591 #[cfg(feature = "lnd")]
592 LnBackend::Lnd => {
593 assert!(
594 settings.lnd.is_some(),
595 "LND backend requires a valid config."
596 )
597 }
598 #[cfg(feature = "ldk-node")]
599 LnBackend::LdkNode => {
600 assert!(
601 settings.ldk_node.is_some(),
602 "LDK Node backend requires a valid config."
603 )
604 }
605 #[cfg(feature = "fakewallet")]
606 LnBackend::FakeWallet => assert!(
607 settings.fake_wallet.is_some(),
608 "FakeWallet backend requires a valid config."
609 ),
610 #[cfg(feature = "grpc-processor")]
611 LnBackend::GrpcProcessor => {
612 assert!(
613 settings.grpc_processor.is_some(),
614 "GRPC backend requires a valid config."
615 )
616 }
617 }
618
619 Ok(settings)
620 }
621}
622
623#[cfg(test)]
624mod tests {
625
626 use super::*;
627
628 #[test]
629 fn test_info_debug_impl() {
630 let info = Info {
632 url: "http://example.com".to_string(),
633 listen_host: "127.0.0.1".to_string(),
634 listen_port: 8080,
635 mnemonic: Some("test secret mnemonic phrase".to_string()),
636 input_fee_ppk: Some(100),
637 ..Default::default()
638 };
639
640 let debug_output = format!("{info:?}");
642
643 assert!(debug_output.contains("url: \"http://example.com\""));
645 assert!(debug_output.contains("listen_host: \"127.0.0.1\""));
646 assert!(debug_output.contains("listen_port: 8080"));
647
648 assert!(!debug_output.contains("test secret mnemonic phrase"));
650 assert!(debug_output.contains("<hashed: "));
651
652 assert!(debug_output.contains("input_fee_ppk: Some(100)"));
653 }
654
655 #[test]
656 fn test_info_debug_with_empty_mnemonic() {
657 let info = Info {
659 url: "http://example.com".to_string(),
660 listen_host: "127.0.0.1".to_string(),
661 listen_port: 8080,
662 mnemonic: Some("".to_string()), enable_swagger_ui: Some(false),
664 ..Default::default()
665 };
666
667 let debug_output = format!("{:?}", info);
669
670 assert!(debug_output.contains("<hashed: "));
672 }
673
674 #[test]
675 fn test_info_debug_with_special_chars() {
676 let info = Info {
678 url: "http://example.com".to_string(),
679 listen_host: "127.0.0.1".to_string(),
680 listen_port: 8080,
681 mnemonic: Some("特殊字符 !@#$%^&*()".to_string()), ..Default::default()
683 };
684
685 let debug_output = format!("{:?}", info);
687
688 assert!(!debug_output.contains("特殊字符 !@#$%^&*()"));
690 assert!(debug_output.contains("<hashed: "));
691 }
692}