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