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 pub use_keyset_v2: Option<bool>,
62
63 pub http_cache: cache::Config,
64
65 #[serde(default)]
67 pub logging: LoggingConfig,
68
69 pub enable_swagger_ui: Option<bool>,
74
75 pub enable_info_page: Option<bool>,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
85 pub quote_ttl: Option<QuoteTTL>,
86}
87
88impl Default for Info {
89 fn default() -> Self {
90 Info {
91 url: String::new(),
92 listen_host: "127.0.0.1".to_string(),
93 listen_port: 8091, seed: None,
95 mnemonic: None,
96 signatory_url: None,
97 signatory_certs: None,
98 input_fee_ppk: None,
99 use_keyset_v2: None,
100 http_cache: cache::Config::default(),
101 enable_swagger_ui: None,
102 enable_info_page: Some(true),
103 logging: LoggingConfig::default(),
104 quote_ttl: None,
105 }
106 }
107}
108
109impl std::fmt::Debug for Info {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 let mnemonic_display: String = {
113 if let Some(mnemonic) = self.mnemonic.as_ref() {
114 let hash = sha256::Hash::hash(mnemonic.as_bytes());
115 format!("<hashed: {hash}>")
116 } else {
117 format!("<url: {}>", self.signatory_url.clone().unwrap_or_default())
118 }
119 };
120
121 f.debug_struct("Info")
122 .field("url", &self.url)
123 .field("listen_host", &self.listen_host)
124 .field("listen_port", &self.listen_port)
125 .field("mnemonic", &mnemonic_display)
126 .field("input_fee_ppk", &self.input_fee_ppk)
127 .field("use_keyset_v2", &self.use_keyset_v2)
128 .field("http_cache", &self.http_cache)
129 .field("logging", &self.logging)
130 .field("enable_swagger_ui", &self.enable_swagger_ui)
131 .field("enable_info_page", &self.enable_info_page)
132 .finish()
133 }
134}
135
136#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
137#[serde(rename_all = "lowercase")]
138pub enum LnBackend {
139 #[default]
140 None,
141 #[cfg(feature = "cln")]
142 Cln,
143 #[cfg(feature = "lnbits")]
144 LNbits,
145 #[cfg(feature = "fakewallet")]
146 FakeWallet,
147 #[cfg(feature = "lnd")]
148 Lnd,
149 #[cfg(feature = "ldk-node")]
150 LdkNode,
151 #[cfg(feature = "grpc-processor")]
152 GrpcProcessor,
153}
154
155impl std::str::FromStr for LnBackend {
156 type Err = String;
157
158 fn from_str(s: &str) -> Result<Self, Self::Err> {
159 match s.to_lowercase().as_str() {
160 #[cfg(feature = "cln")]
161 "cln" => Ok(LnBackend::Cln),
162 #[cfg(feature = "lnbits")]
163 "lnbits" => Ok(LnBackend::LNbits),
164 #[cfg(feature = "fakewallet")]
165 "fakewallet" => Ok(LnBackend::FakeWallet),
166 #[cfg(feature = "lnd")]
167 "lnd" => Ok(LnBackend::Lnd),
168 #[cfg(feature = "ldk-node")]
169 "ldk-node" | "ldknode" => Ok(LnBackend::LdkNode),
170 #[cfg(feature = "grpc-processor")]
171 "grpcprocessor" => Ok(LnBackend::GrpcProcessor),
172 _ => Err(format!("Unknown Lightning backend: {s}")),
173 }
174 }
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct Ln {
179 pub ln_backend: LnBackend,
180 pub invoice_description: Option<String>,
181 pub min_mint: Amount,
182 pub max_mint: Amount,
183 pub min_melt: Amount,
184 pub max_melt: Amount,
185}
186
187impl Default for Ln {
188 fn default() -> Self {
189 Ln {
190 ln_backend: LnBackend::default(),
191 invoice_description: None,
192 min_mint: 1.into(),
193 max_mint: 500_000.into(),
194 min_melt: 1.into(),
195 max_melt: 500_000.into(),
196 }
197 }
198}
199
200#[cfg(feature = "lnbits")]
201#[derive(Clone, Serialize, Deserialize)]
202pub struct LNbits {
203 pub admin_api_key: String,
204 pub invoice_api_key: String,
205 pub lnbits_api: String,
206 #[serde(default = "default_fee_percent")]
207 pub fee_percent: f32,
208 #[serde(default = "default_reserve_fee_min")]
209 pub reserve_fee_min: Amount,
210}
211
212#[cfg(feature = "lnbits")]
213impl std::fmt::Debug for LNbits {
214 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215 f.debug_struct("LNbits")
216 .field("admin_api_key", &"[REDACTED]")
217 .field("invoice_api_key", &"[REDACTED]")
218 .field("lnbits_api", &self.lnbits_api)
219 .field("fee_percent", &self.fee_percent)
220 .field("reserve_fee_min", &self.reserve_fee_min)
221 .finish()
222 }
223}
224
225#[cfg(feature = "lnbits")]
226impl Default for LNbits {
227 fn default() -> Self {
228 Self {
229 admin_api_key: String::new(),
230 invoice_api_key: String::new(),
231 lnbits_api: String::new(),
232 fee_percent: 0.02,
233 reserve_fee_min: 2.into(),
234 }
235 }
236}
237
238#[cfg(feature = "cln")]
239#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct Cln {
241 pub rpc_path: PathBuf,
242 #[serde(default = "default_cln_bolt12")]
243 pub bolt12: bool,
244 #[serde(default)]
245 pub expose_private_channels: bool,
246 #[serde(default = "default_fee_percent")]
247 pub fee_percent: f32,
248 #[serde(default = "default_reserve_fee_min")]
249 pub reserve_fee_min: Amount,
250}
251
252#[cfg(feature = "cln")]
253impl Default for Cln {
254 fn default() -> Self {
255 Self {
256 rpc_path: PathBuf::new(),
257 bolt12: true,
258 expose_private_channels: false,
259 fee_percent: 0.02,
260 reserve_fee_min: 2.into(),
261 }
262 }
263}
264
265#[cfg(feature = "cln")]
266fn default_cln_bolt12() -> bool {
267 true
268}
269
270#[cfg(feature = "lnd")]
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct Lnd {
273 pub address: String,
274 pub cert_file: PathBuf,
275 pub macaroon_file: PathBuf,
276 #[serde(default = "default_fee_percent")]
277 pub fee_percent: f32,
278 #[serde(default = "default_reserve_fee_min")]
279 pub reserve_fee_min: Amount,
280}
281
282#[cfg(feature = "lnd")]
283impl Default for Lnd {
284 fn default() -> Self {
285 Self {
286 address: String::new(),
287 cert_file: PathBuf::new(),
288 macaroon_file: PathBuf::new(),
289 fee_percent: 0.02,
290 reserve_fee_min: 2.into(),
291 }
292 }
293}
294
295#[cfg(feature = "ldk-node")]
296#[derive(Clone, Serialize, Deserialize)]
297pub struct LdkNode {
298 #[serde(default = "default_ldk_fee_percent")]
300 pub fee_percent: f32,
301 #[serde(default = "default_ldk_reserve_fee_min")]
303 pub reserve_fee_min: Amount,
304 pub bitcoin_network: Option<String>,
306 pub chain_source_type: Option<String>,
308 pub esplora_url: Option<String>,
310 pub bitcoind_rpc_host: Option<String>,
312 pub bitcoind_rpc_port: Option<u16>,
313 pub bitcoind_rpc_user: Option<String>,
314 pub bitcoind_rpc_password: Option<String>,
315 pub storage_dir_path: Option<String>,
317 pub log_dir_path: Option<String>,
319 pub ldk_node_host: Option<String>,
321 pub ldk_node_port: Option<u16>,
323 pub ldk_node_announce_addresses: Option<Vec<String>>,
325 pub gossip_source_type: Option<String>,
327 pub rgs_url: Option<String>,
329 #[serde(default = "default_webserver_host")]
331 pub webserver_host: Option<String>,
332 #[serde(default = "default_webserver_port")]
334 pub webserver_port: Option<u16>,
335 pub ldk_node_mnemonic: Option<String>,
338}
339
340#[cfg(feature = "ldk-node")]
341impl Default for LdkNode {
342 fn default() -> Self {
343 Self {
344 fee_percent: default_ldk_fee_percent(),
345 reserve_fee_min: default_ldk_reserve_fee_min(),
346 bitcoin_network: None,
347 chain_source_type: None,
348 esplora_url: None,
349 bitcoind_rpc_host: None,
350 bitcoind_rpc_port: None,
351 bitcoind_rpc_user: None,
352 ldk_node_announce_addresses: None,
353 bitcoind_rpc_password: None,
354 storage_dir_path: None,
355 ldk_node_host: None,
356 log_dir_path: None,
357 ldk_node_port: None,
358 gossip_source_type: None,
359 rgs_url: None,
360 webserver_host: default_webserver_host(),
361 webserver_port: default_webserver_port(),
362 ldk_node_mnemonic: None,
363 }
364 }
365}
366
367#[cfg(feature = "ldk-node")]
368impl std::fmt::Debug for LdkNode {
369 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370 f.debug_struct("LdkNode")
371 .field("fee_percent", &self.fee_percent)
372 .field("reserve_fee_min", &self.reserve_fee_min)
373 .field("bitcoin_network", &self.bitcoin_network)
374 .field("chain_source_type", &self.chain_source_type)
375 .field("esplora_url", &self.esplora_url)
376 .field("bitcoind_rpc_host", &self.bitcoind_rpc_host)
377 .field("bitcoind_rpc_port", &self.bitcoind_rpc_port)
378 .field("bitcoind_rpc_user", &self.bitcoind_rpc_user)
379 .field("bitcoind_rpc_password", &"[REDACTED]")
380 .field("storage_dir_path", &self.storage_dir_path)
381 .field("log_dir_path", &self.log_dir_path)
382 .field("ldk_node_host", &self.ldk_node_host)
383 .field("ldk_node_port", &self.ldk_node_port)
384 .field(
385 "ldk_node_announce_addresses",
386 &self.ldk_node_announce_addresses,
387 )
388 .field("gossip_source_type", &self.gossip_source_type)
389 .field("rgs_url", &self.rgs_url)
390 .field("webserver_host", &self.webserver_host)
391 .field("webserver_port", &self.webserver_port)
392 .field("ldk_node_mnemonic", &"[REDACTED]")
393 .finish()
394 }
395}
396
397#[cfg(feature = "ldk-node")]
398fn default_ldk_fee_percent() -> f32 {
399 0.04
400}
401
402#[cfg(feature = "ldk-node")]
403fn default_ldk_reserve_fee_min() -> Amount {
404 4.into()
405}
406
407#[cfg(feature = "ldk-node")]
408fn default_webserver_host() -> Option<String> {
409 Some("127.0.0.1".to_string())
410}
411
412#[cfg(feature = "ldk-node")]
413fn default_webserver_port() -> Option<u16> {
414 Some(8091)
415}
416
417#[cfg(feature = "fakewallet")]
418#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct FakeWalletKeysetRotation {
420 pub unit: CurrencyUnit,
422 #[serde(default)]
424 pub input_fee_ppk: u64,
425 #[serde(default = "default_keyset_version")]
427 pub version: String,
428 #[serde(default)]
430 pub expired: bool,
431}
432
433#[cfg(feature = "fakewallet")]
434fn default_keyset_version() -> String {
435 "v1".to_string()
436}
437
438#[cfg(feature = "fakewallet")]
439#[derive(Debug, Clone, Serialize, Deserialize)]
440pub struct FakeWallet {
441 pub supported_units: Vec<CurrencyUnit>,
442 pub fee_percent: f32,
443 pub reserve_fee_min: Amount,
444 #[serde(default = "default_min_delay_time")]
445 pub min_delay_time: u64,
446 #[serde(default = "default_max_delay_time")]
447 pub max_delay_time: u64,
448 #[serde(default)]
450 pub keyset_rotations: Vec<FakeWalletKeysetRotation>,
451}
452
453#[cfg(feature = "fakewallet")]
454impl Default for FakeWallet {
455 fn default() -> Self {
456 Self {
457 supported_units: vec![CurrencyUnit::Sat],
458 fee_percent: 0.02,
459 reserve_fee_min: 2.into(),
460 min_delay_time: 1,
461 max_delay_time: 3,
462 keyset_rotations: Vec::new(),
463 }
464 }
465}
466
467#[cfg(any(feature = "cln", feature = "lnbits", feature = "lnd"))]
470fn default_fee_percent() -> f32 {
471 0.02
472}
473
474#[cfg(any(feature = "cln", feature = "lnbits", feature = "lnd"))]
475fn default_reserve_fee_min() -> Amount {
476 2.into()
477}
478
479#[cfg(feature = "fakewallet")]
480fn default_min_delay_time() -> u64 {
481 1
482}
483
484#[cfg(feature = "fakewallet")]
485fn default_max_delay_time() -> u64 {
486 3
487}
488
489#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
490pub struct GrpcProcessor {
491 #[serde(default)]
492 pub supported_units: Vec<CurrencyUnit>,
493 #[serde(default = "default_grpc_addr")]
494 pub addr: String,
495 #[serde(default = "default_grpc_port")]
496 pub port: u16,
497 #[serde(default)]
498 pub tls_dir: Option<PathBuf>,
499}
500
501impl Default for GrpcProcessor {
502 fn default() -> Self {
503 Self {
504 supported_units: Vec::new(),
505 addr: default_grpc_addr(),
506 port: default_grpc_port(),
507 tls_dir: None,
508 }
509 }
510}
511
512fn default_grpc_addr() -> String {
513 "127.0.0.1".to_string()
514}
515
516fn default_grpc_port() -> u16 {
517 50051
518}
519
520#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
521#[serde(rename_all = "lowercase")]
522pub enum DatabaseEngine {
523 #[default]
524 Sqlite,
525 Postgres,
526}
527
528impl std::str::FromStr for DatabaseEngine {
529 type Err = String;
530
531 fn from_str(s: &str) -> Result<Self, Self::Err> {
532 match s.to_lowercase().as_str() {
533 "sqlite" => Ok(DatabaseEngine::Sqlite),
534 "postgres" => Ok(DatabaseEngine::Postgres),
535 _ => Err(format!("Unknown database engine: {s}")),
536 }
537 }
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize, Default)]
541pub struct Database {
542 pub engine: DatabaseEngine,
543 pub postgres: Option<PostgresConfig>,
544}
545
546#[derive(Debug, Clone, Serialize, Deserialize, Default)]
547pub struct AuthDatabase {
548 pub postgres: Option<PostgresAuthConfig>,
549}
550
551#[derive(Debug, Clone, Serialize, Deserialize)]
552pub struct PostgresAuthConfig {
553 pub url: String,
554 pub tls_mode: Option<String>,
555 pub max_connections: Option<usize>,
556 pub connection_timeout_seconds: Option<u64>,
557}
558
559impl Default for PostgresAuthConfig {
560 fn default() -> Self {
561 Self {
562 url: String::new(),
563 tls_mode: Some("disable".to_string()),
564 max_connections: Some(20),
565 connection_timeout_seconds: Some(10),
566 }
567 }
568}
569
570#[derive(Debug, Clone, Serialize, Deserialize)]
571pub struct PostgresConfig {
572 pub url: String,
573 pub tls_mode: Option<String>,
574 pub max_connections: Option<usize>,
575 pub connection_timeout_seconds: Option<u64>,
576}
577
578impl Default for PostgresConfig {
579 fn default() -> Self {
580 Self {
581 url: String::new(),
582 tls_mode: Some("disable".to_string()),
583 max_connections: Some(20),
584 connection_timeout_seconds: Some(10),
585 }
586 }
587}
588
589#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
590#[serde(rename_all = "lowercase")]
591pub enum AuthType {
592 Clear,
593 Blind,
594 #[default]
595 None,
596}
597
598impl std::str::FromStr for AuthType {
599 type Err = String;
600
601 fn from_str(s: &str) -> Result<Self, Self::Err> {
602 match s.to_lowercase().as_str() {
603 "clear" => Ok(AuthType::Clear),
604 "blind" => Ok(AuthType::Blind),
605 "none" => Ok(AuthType::None),
606 _ => Err(format!("Unknown auth type: {s}")),
607 }
608 }
609}
610
611#[derive(Debug, Clone, Default, Serialize, Deserialize)]
612pub struct Auth {
613 #[serde(default)]
614 pub auth_enabled: bool,
615 pub openid_discovery: String,
616 pub openid_client_id: String,
617 pub mint_max_bat: u64,
618 #[serde(default = "default_blind")]
619 pub mint: AuthType,
620 #[serde(default)]
621 pub get_mint_quote: AuthType,
622 #[serde(default)]
623 pub check_mint_quote: AuthType,
624 #[serde(default)]
625 pub melt: AuthType,
626 #[serde(default)]
627 pub get_melt_quote: AuthType,
628 #[serde(default)]
629 pub check_melt_quote: AuthType,
630 #[serde(default = "default_blind")]
631 pub swap: AuthType,
632 #[serde(default = "default_blind")]
633 pub restore: AuthType,
634 #[serde(default)]
635 pub check_proof_state: AuthType,
636 #[serde(default = "default_blind")]
638 pub websocket_auth: AuthType,
639}
640
641fn default_blind() -> AuthType {
642 AuthType::Blind
643}
644
645#[derive(Debug, Clone, Serialize, Deserialize, Default)]
647pub struct Settings {
648 pub info: Info,
649 pub mint_info: MintInfo,
650 pub ln: Ln,
651 #[serde(default)]
653 pub limits: Limits,
654 #[cfg(feature = "cln")]
655 pub cln: Option<Cln>,
656 #[cfg(feature = "lnbits")]
657 pub lnbits: Option<LNbits>,
658 #[cfg(feature = "lnd")]
659 pub lnd: Option<Lnd>,
660 #[cfg(feature = "ldk-node")]
661 pub ldk_node: Option<LdkNode>,
662 #[cfg(feature = "fakewallet")]
663 pub fake_wallet: Option<FakeWallet>,
664 pub grpc_processor: Option<GrpcProcessor>,
665 pub database: Database,
666 pub auth_database: Option<AuthDatabase>,
667 #[cfg(feature = "management-rpc")]
668 pub mint_management_rpc: Option<MintManagementRpc>,
669 pub auth: Option<Auth>,
670 #[cfg(feature = "prometheus")]
671 pub prometheus: Option<Prometheus>,
672}
673
674#[derive(Debug, Clone, Serialize, Deserialize, Default)]
675#[cfg(feature = "prometheus")]
676pub struct Prometheus {
677 pub enabled: bool,
678 pub address: Option<String>,
679 pub port: Option<u16>,
680}
681
682#[derive(Debug, Clone, Serialize, Deserialize)]
684pub struct Limits {
685 #[serde(default = "default_max_inputs")]
687 pub max_inputs: usize,
688 #[serde(default = "default_max_outputs")]
690 pub max_outputs: usize,
691}
692
693impl Default for Limits {
694 fn default() -> Self {
695 Self {
696 max_inputs: 1000,
697 max_outputs: 1000,
698 }
699 }
700}
701
702fn default_max_inputs() -> usize {
703 1000
704}
705
706fn default_max_outputs() -> usize {
707 1000
708}
709
710#[derive(Debug, Clone, Serialize, Deserialize, Default)]
711pub struct MintInfo {
712 pub name: String,
714 pub pubkey: Option<PublicKey>,
716 pub description: String,
718 pub description_long: Option<String>,
720 pub icon_url: Option<String>,
722 pub motd: Option<String>,
724 pub contact_nostr_public_key: Option<String>,
726 pub contact_email: Option<String>,
728 pub tos_url: Option<String>,
730}
731
732#[cfg(feature = "management-rpc")]
733#[derive(Debug, Clone, Serialize, Deserialize, Default)]
734pub struct MintManagementRpc {
735 pub enabled: bool,
738 pub address: Option<String>,
739 pub port: Option<u16>,
740 pub tls_dir_path: Option<PathBuf>,
741}
742
743impl Settings {
744 #[must_use]
745 pub fn new<P>(config_file_name: Option<P>) -> Self
746 where
747 P: Into<PathBuf>,
748 {
749 let default_settings = Self::default();
750 let from_file = Self::new_from_default(&default_settings, config_file_name);
752 match from_file {
753 Ok(f) => f,
754 Err(e) => {
755 tracing::error!(
756 "Error reading config file, falling back to defaults. Error: {e:?}"
757 );
758 default_settings
759 }
760 }
761 }
762
763 fn new_from_default<P>(
764 default: &Settings,
765 config_file_name: Option<P>,
766 ) -> Result<Self, ConfigError>
767 where
768 P: Into<PathBuf>,
769 {
770 let mut default_config_file_name = home::home_dir()
771 .ok_or(ConfigError::NotFound("Config Path".to_string()))?
772 .join("cashu-rs-mint");
773
774 default_config_file_name.push("config.toml");
775 let config: String = match config_file_name {
776 Some(value) => value.into().to_string_lossy().to_string(),
777 None => default_config_file_name.to_string_lossy().to_string(),
778 };
779 let builder = Config::builder();
780 let config: Config = builder
781 .add_source(Config::try_from(default)?)
783 .add_source(File::with_name(&config))
785 .build()?;
786 let settings: Settings = config.try_deserialize()?;
787
788 Ok(settings)
789 }
790}
791
792#[cfg(test)]
793mod tests {
794
795 use super::*;
796
797 #[test]
798 fn test_info_debug_impl() {
799 let info = Info {
801 url: "http://example.com".to_string(),
802 listen_host: "127.0.0.1".to_string(),
803 listen_port: 8080,
804 mnemonic: Some("test secret mnemonic phrase".to_string()),
805 input_fee_ppk: Some(100),
806 ..Default::default()
807 };
808
809 let debug_output = format!("{info:?}");
811
812 assert!(debug_output.contains("url: \"http://example.com\""));
814 assert!(debug_output.contains("listen_host: \"127.0.0.1\""));
815 assert!(debug_output.contains("listen_port: 8080"));
816
817 assert!(!debug_output.contains("test secret mnemonic phrase"));
819 assert!(debug_output.contains("<hashed: "));
820
821 assert!(debug_output.contains("input_fee_ppk: Some(100)"));
822 }
823
824 #[test]
825 fn test_info_debug_with_empty_mnemonic() {
826 let info = Info {
828 url: "http://example.com".to_string(),
829 listen_host: "127.0.0.1".to_string(),
830 listen_port: 8080,
831 mnemonic: Some("".to_string()), enable_swagger_ui: Some(false),
833 ..Default::default()
834 };
835
836 let debug_output = format!("{:?}", info);
838
839 assert!(debug_output.contains("<hashed: "));
841 }
842
843 #[test]
844 fn test_info_debug_with_special_chars() {
845 let info = Info {
847 url: "http://example.com".to_string(),
848 listen_host: "127.0.0.1".to_string(),
849 listen_port: 8080,
850 mnemonic: Some("特殊字符 !@#$%^&*()".to_string()), ..Default::default()
852 };
853
854 let debug_output = format!("{:?}", info);
856
857 assert!(!debug_output.contains("特殊字符 !@#$%^&*()"));
859 assert!(debug_output.contains("<hashed: "));
860 }
861
862 #[test]
867 fn test_env_var_only_config_all_backends() {
868 #[cfg(feature = "lnd")]
870 test_lnd_env_config();
871
872 #[cfg(feature = "cln")]
873 test_cln_env_config();
874
875 #[cfg(feature = "lnbits")]
876 test_lnbits_env_config();
877
878 #[cfg(feature = "fakewallet")]
879 test_fakewallet_env_config();
880
881 #[cfg(feature = "grpc-processor")]
882 test_grpc_processor_env_config();
883
884 #[cfg(feature = "ldk-node")]
885 test_ldk_node_env_config();
886 }
887
888 #[cfg(feature = "lnd")]
889 fn test_lnd_env_config() {
890 use std::path::PathBuf;
891 use std::{env, fs};
892
893 let temp_dir = env::temp_dir().join("cdk_test_env_vars");
895 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
896 let config_path = temp_dir.join("config.toml");
897
898 let config_content = r#"
900[ln]
901backend = "lnd"
902min_mint = 1
903max_mint = 500000
904min_melt = 1
905max_melt = 500000
906"#;
907 fs::write(&config_path, config_content).expect("Failed to write config file");
908
909 env::set_var(crate::env_vars::ENV_LN_BACKEND, "lnd");
911 env::set_var(crate::env_vars::ENV_LND_ADDRESS, "https://localhost:10009");
912 env::set_var(crate::env_vars::ENV_LND_CERT_FILE, "/tmp/test_tls.cert");
913 env::set_var(
914 crate::env_vars::ENV_LND_MACAROON_FILE,
915 "/tmp/test_admin.macaroon",
916 );
917 env::set_var(crate::env_vars::ENV_LND_FEE_PERCENT, "0.01");
918 env::set_var(crate::env_vars::ENV_LND_RESERVE_FEE_MIN, "4");
919
920 let mut settings = Settings::new(Some(&config_path));
922 settings.from_env().expect("Failed to apply env vars");
923
924 assert!(settings.lnd.is_some());
926 let lnd_config = settings.lnd.as_ref().unwrap();
927 assert_eq!(lnd_config.address, "https://localhost:10009");
928 assert_eq!(lnd_config.cert_file, PathBuf::from("/tmp/test_tls.cert"));
929 assert_eq!(
930 lnd_config.macaroon_file,
931 PathBuf::from("/tmp/test_admin.macaroon")
932 );
933 assert_eq!(lnd_config.fee_percent, 0.01);
934 let reserve_fee_u64: u64 = lnd_config.reserve_fee_min.into();
935 assert_eq!(reserve_fee_u64, 4);
936
937 env::remove_var(crate::env_vars::ENV_LN_BACKEND);
939 env::remove_var(crate::env_vars::ENV_LND_ADDRESS);
940 env::remove_var(crate::env_vars::ENV_LND_CERT_FILE);
941 env::remove_var(crate::env_vars::ENV_LND_MACAROON_FILE);
942 env::remove_var(crate::env_vars::ENV_LND_FEE_PERCENT);
943 env::remove_var(crate::env_vars::ENV_LND_RESERVE_FEE_MIN);
944
945 let _ = fs::remove_dir_all(&temp_dir);
947 }
948
949 #[cfg(feature = "cln")]
950 fn test_cln_env_config() {
951 use std::path::PathBuf;
952 use std::{env, fs};
953
954 let temp_dir = env::temp_dir().join("cdk_test_env_vars_cln");
956 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
957 let config_path = temp_dir.join("config.toml");
958
959 let config_content = r#"
961[ln]
962backend = "cln"
963min_mint = 1
964max_mint = 500000
965min_melt = 1
966max_melt = 500000
967"#;
968 fs::write(&config_path, config_content).expect("Failed to write config file");
969
970 env::set_var(crate::env_vars::ENV_LN_BACKEND, "cln");
972 env::set_var(crate::env_vars::ENV_CLN_RPC_PATH, "/tmp/lightning-rpc");
973 env::set_var(crate::env_vars::ENV_CLN_BOLT12, "false");
974 env::set_var(crate::env_vars::ENV_CLN_FEE_PERCENT, "0.01");
975 env::set_var(crate::env_vars::ENV_CLN_RESERVE_FEE_MIN, "4");
976
977 let mut settings = Settings::new(Some(&config_path));
979 settings.from_env().expect("Failed to apply env vars");
980
981 assert!(settings.cln.is_some());
983 let cln_config = settings.cln.as_ref().unwrap();
984 assert_eq!(cln_config.rpc_path, PathBuf::from("/tmp/lightning-rpc"));
985 assert!(!cln_config.bolt12);
986 assert_eq!(cln_config.fee_percent, 0.01);
987 let reserve_fee_u64: u64 = cln_config.reserve_fee_min.into();
988 assert_eq!(reserve_fee_u64, 4);
989
990 env::remove_var(crate::env_vars::ENV_LN_BACKEND);
992 env::remove_var(crate::env_vars::ENV_CLN_RPC_PATH);
993 env::remove_var(crate::env_vars::ENV_CLN_BOLT12);
994 env::remove_var(crate::env_vars::ENV_CLN_FEE_PERCENT);
995 env::remove_var(crate::env_vars::ENV_CLN_RESERVE_FEE_MIN);
996
997 let _ = fs::remove_dir_all(&temp_dir);
999 }
1000
1001 #[cfg(feature = "lnbits")]
1002 fn test_lnbits_env_config() {
1003 use std::{env, fs};
1004
1005 let temp_dir = env::temp_dir().join("cdk_test_env_vars_lnbits");
1007 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1008 let config_path = temp_dir.join("config.toml");
1009
1010 let config_content = r#"
1012[ln]
1013backend = "lnbits"
1014min_mint = 1
1015max_mint = 500000
1016min_melt = 1
1017max_melt = 500000
1018"#;
1019 fs::write(&config_path, config_content).expect("Failed to write config file");
1020
1021 env::set_var(crate::env_vars::ENV_LN_BACKEND, "lnbits");
1023 env::set_var(crate::env_vars::ENV_LNBITS_ADMIN_API_KEY, "test_admin_key");
1024 env::set_var(
1025 crate::env_vars::ENV_LNBITS_INVOICE_API_KEY,
1026 "test_invoice_key",
1027 );
1028 env::set_var(
1029 crate::env_vars::ENV_LNBITS_API,
1030 "https://lnbits.example.com",
1031 );
1032 env::set_var(crate::env_vars::ENV_LNBITS_FEE_PERCENT, "0.02");
1033 env::set_var(crate::env_vars::ENV_LNBITS_RESERVE_FEE_MIN, "5");
1034
1035 let mut settings = Settings::new(Some(&config_path));
1037 settings.from_env().expect("Failed to apply env vars");
1038
1039 assert!(settings.lnbits.is_some());
1041 let lnbits_config = settings.lnbits.as_ref().unwrap();
1042 assert_eq!(lnbits_config.admin_api_key, "test_admin_key");
1043 assert_eq!(lnbits_config.invoice_api_key, "test_invoice_key");
1044 assert_eq!(lnbits_config.lnbits_api, "https://lnbits.example.com");
1045 assert_eq!(lnbits_config.fee_percent, 0.02);
1046 let reserve_fee_u64: u64 = lnbits_config.reserve_fee_min.into();
1047 assert_eq!(reserve_fee_u64, 5);
1048
1049 env::remove_var(crate::env_vars::ENV_LN_BACKEND);
1051 env::remove_var(crate::env_vars::ENV_LNBITS_ADMIN_API_KEY);
1052 env::remove_var(crate::env_vars::ENV_LNBITS_INVOICE_API_KEY);
1053 env::remove_var(crate::env_vars::ENV_LNBITS_API);
1054 env::remove_var(crate::env_vars::ENV_LNBITS_FEE_PERCENT);
1055 env::remove_var(crate::env_vars::ENV_LNBITS_RESERVE_FEE_MIN);
1056
1057 let _ = fs::remove_dir_all(&temp_dir);
1059 }
1060
1061 #[cfg(feature = "fakewallet")]
1062 fn test_fakewallet_env_config() {
1063 use std::{env, fs};
1064
1065 let temp_dir = env::temp_dir().join("cdk_test_env_vars_fakewallet");
1067 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1068 let config_path = temp_dir.join("config.toml");
1069
1070 let config_content = r#"
1072[ln]
1073backend = "fakewallet"
1074min_mint = 1
1075max_mint = 500000
1076min_melt = 1
1077max_melt = 500000
1078"#;
1079 fs::write(&config_path, config_content).expect("Failed to write config file");
1080
1081 env::set_var(crate::env_vars::ENV_LN_BACKEND, "fakewallet");
1083 env::set_var(crate::env_vars::ENV_FAKE_WALLET_SUPPORTED_UNITS, "sat,msat");
1084 env::set_var(crate::env_vars::ENV_FAKE_WALLET_FEE_PERCENT, "0.0");
1085 env::set_var(crate::env_vars::ENV_FAKE_WALLET_RESERVE_FEE_MIN, "0");
1086 env::set_var(crate::env_vars::ENV_FAKE_WALLET_MIN_DELAY, "0");
1087 env::set_var(crate::env_vars::ENV_FAKE_WALLET_MAX_DELAY, "5");
1088
1089 let mut settings = Settings::new(Some(&config_path));
1091 settings.from_env().expect("Failed to apply env vars");
1092
1093 assert!(settings.fake_wallet.is_some());
1095 let fakewallet_config = settings.fake_wallet.as_ref().unwrap();
1096 assert_eq!(fakewallet_config.fee_percent, 0.0);
1097 let reserve_fee_u64: u64 = fakewallet_config.reserve_fee_min.into();
1098 assert_eq!(reserve_fee_u64, 0);
1099 assert_eq!(fakewallet_config.min_delay_time, 0);
1100 assert_eq!(fakewallet_config.max_delay_time, 5);
1101
1102 env::remove_var(crate::env_vars::ENV_LN_BACKEND);
1104 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_SUPPORTED_UNITS);
1105 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_FEE_PERCENT);
1106 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_RESERVE_FEE_MIN);
1107 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_MIN_DELAY);
1108 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_MAX_DELAY);
1109
1110 let _ = fs::remove_dir_all(&temp_dir);
1112 }
1113
1114 #[cfg(feature = "grpc-processor")]
1115 fn test_grpc_processor_env_config() {
1116 use std::{env, fs};
1117
1118 let temp_dir = env::temp_dir().join("cdk_test_env_vars_grpc");
1120 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1121 let config_path = temp_dir.join("config.toml");
1122
1123 let config_content = r#"
1125[ln]
1126backend = "grpcprocessor"
1127min_mint = 1
1128max_mint = 500000
1129min_melt = 1
1130max_melt = 500000
1131"#;
1132 fs::write(&config_path, config_content).expect("Failed to write config file");
1133
1134 env::set_var(crate::env_vars::ENV_LN_BACKEND, "grpcprocessor");
1136 env::set_var(
1137 crate::env_vars::ENV_GRPC_PROCESSOR_SUPPORTED_UNITS,
1138 "sat,msat",
1139 );
1140 env::set_var(crate::env_vars::ENV_GRPC_PROCESSOR_ADDRESS, "localhost");
1141 env::set_var(crate::env_vars::ENV_GRPC_PROCESSOR_PORT, "50051");
1142
1143 let mut settings = Settings::new(Some(&config_path));
1145 settings.from_env().expect("Failed to apply env vars");
1146
1147 assert!(settings.grpc_processor.is_some());
1149 let grpc_config = settings.grpc_processor.as_ref().unwrap();
1150 assert_eq!(grpc_config.addr, "localhost");
1151 assert_eq!(grpc_config.port, 50051);
1152
1153 env::remove_var(crate::env_vars::ENV_LN_BACKEND);
1155 env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_SUPPORTED_UNITS);
1156 env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_ADDRESS);
1157 env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_PORT);
1158
1159 let _ = fs::remove_dir_all(&temp_dir);
1161 }
1162
1163 #[cfg(feature = "ldk-node")]
1164 fn test_ldk_node_env_config() {
1165 use std::{env, fs};
1166
1167 let temp_dir = env::temp_dir().join("cdk_test_env_vars_ldk");
1169 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1170 let config_path = temp_dir.join("config.toml");
1171
1172 let config_content = r#"
1174[ln]
1175backend = "ldknode"
1176min_mint = 1
1177max_mint = 500000
1178min_melt = 1
1179max_melt = 500000
1180"#;
1181 fs::write(&config_path, config_content).expect("Failed to write config file");
1182
1183 env::set_var(crate::env_vars::ENV_LN_BACKEND, "ldknode");
1185 env::set_var(crate::env_vars::LDK_NODE_FEE_PERCENT_ENV_VAR, "0.01");
1186 env::set_var(crate::env_vars::LDK_NODE_RESERVE_FEE_MIN_ENV_VAR, "4");
1187 env::set_var(crate::env_vars::LDK_NODE_BITCOIN_NETWORK_ENV_VAR, "regtest");
1188 env::set_var(
1189 crate::env_vars::LDK_NODE_CHAIN_SOURCE_TYPE_ENV_VAR,
1190 "esplora",
1191 );
1192 env::set_var(
1193 crate::env_vars::LDK_NODE_ESPLORA_URL_ENV_VAR,
1194 "http://localhost:3000",
1195 );
1196 env::set_var(
1197 crate::env_vars::LDK_NODE_STORAGE_DIR_PATH_ENV_VAR,
1198 "/tmp/ldk",
1199 );
1200
1201 let mut settings = Settings::new(Some(&config_path));
1203 settings.from_env().expect("Failed to apply env vars");
1204
1205 assert!(settings.ldk_node.is_some());
1207 let ldk_config = settings.ldk_node.as_ref().unwrap();
1208 assert_eq!(ldk_config.fee_percent, 0.01);
1209 let reserve_fee_u64: u64 = ldk_config.reserve_fee_min.into();
1210 assert_eq!(reserve_fee_u64, 4);
1211 assert_eq!(ldk_config.bitcoin_network, Some("regtest".to_string()));
1212 assert_eq!(ldk_config.chain_source_type, Some("esplora".to_string()));
1213 assert_eq!(
1214 ldk_config.esplora_url,
1215 Some("http://localhost:3000".to_string())
1216 );
1217 assert_eq!(ldk_config.storage_dir_path, Some("/tmp/ldk".to_string()));
1218
1219 env::remove_var(crate::env_vars::ENV_LN_BACKEND);
1221 env::remove_var(crate::env_vars::LDK_NODE_FEE_PERCENT_ENV_VAR);
1222 env::remove_var(crate::env_vars::LDK_NODE_RESERVE_FEE_MIN_ENV_VAR);
1223 env::remove_var(crate::env_vars::LDK_NODE_BITCOIN_NETWORK_ENV_VAR);
1224 env::remove_var(crate::env_vars::LDK_NODE_CHAIN_SOURCE_TYPE_ENV_VAR);
1225 env::remove_var(crate::env_vars::LDK_NODE_ESPLORA_URL_ENV_VAR);
1226 env::remove_var(crate::env_vars::LDK_NODE_STORAGE_DIR_PATH_ENV_VAR);
1227
1228 let _ = fs::remove_dir_all(&temp_dir);
1230 }
1231}