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)]
190pub struct LNbits {
191 pub admin_api_key: String,
192 pub invoice_api_key: String,
193 pub lnbits_api: String,
194 #[serde(default = "default_fee_percent")]
195 pub fee_percent: f32,
196 #[serde(default = "default_reserve_fee_min")]
197 pub reserve_fee_min: Amount,
198}
199
200#[cfg(feature = "lnbits")]
201impl Default for LNbits {
202 fn default() -> Self {
203 Self {
204 admin_api_key: String::new(),
205 invoice_api_key: String::new(),
206 lnbits_api: String::new(),
207 fee_percent: 0.02,
208 reserve_fee_min: 2.into(),
209 }
210 }
211}
212
213#[cfg(feature = "cln")]
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct Cln {
216 pub rpc_path: PathBuf,
217 #[serde(default = "default_cln_bolt12")]
218 pub bolt12: bool,
219 #[serde(default = "default_fee_percent")]
220 pub fee_percent: f32,
221 #[serde(default = "default_reserve_fee_min")]
222 pub reserve_fee_min: Amount,
223}
224
225#[cfg(feature = "cln")]
226impl Default for Cln {
227 fn default() -> Self {
228 Self {
229 rpc_path: PathBuf::new(),
230 bolt12: true,
231 fee_percent: 0.02,
232 reserve_fee_min: 2.into(),
233 }
234 }
235}
236
237#[cfg(feature = "cln")]
238fn default_cln_bolt12() -> bool {
239 true
240}
241
242#[cfg(feature = "lnd")]
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct Lnd {
245 pub address: String,
246 pub cert_file: PathBuf,
247 pub macaroon_file: PathBuf,
248 #[serde(default = "default_fee_percent")]
249 pub fee_percent: f32,
250 #[serde(default = "default_reserve_fee_min")]
251 pub reserve_fee_min: Amount,
252}
253
254#[cfg(feature = "lnd")]
255impl Default for Lnd {
256 fn default() -> Self {
257 Self {
258 address: String::new(),
259 cert_file: PathBuf::new(),
260 macaroon_file: PathBuf::new(),
261 fee_percent: 0.02,
262 reserve_fee_min: 2.into(),
263 }
264 }
265}
266
267#[cfg(feature = "ldk-node")]
268#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct LdkNode {
270 #[serde(default = "default_ldk_fee_percent")]
272 pub fee_percent: f32,
273 #[serde(default = "default_ldk_reserve_fee_min")]
275 pub reserve_fee_min: Amount,
276 pub bitcoin_network: Option<String>,
278 pub chain_source_type: Option<String>,
280 pub esplora_url: Option<String>,
282 pub bitcoind_rpc_host: Option<String>,
284 pub bitcoind_rpc_port: Option<u16>,
285 pub bitcoind_rpc_user: Option<String>,
286 pub bitcoind_rpc_password: Option<String>,
287 pub storage_dir_path: Option<String>,
289 pub ldk_node_host: Option<String>,
291 pub ldk_node_port: Option<u16>,
293 pub gossip_source_type: Option<String>,
295 pub rgs_url: Option<String>,
297 #[serde(default = "default_webserver_host")]
299 pub webserver_host: Option<String>,
300 #[serde(default = "default_webserver_port")]
302 pub webserver_port: Option<u16>,
303}
304
305#[cfg(feature = "ldk-node")]
306impl Default for LdkNode {
307 fn default() -> Self {
308 Self {
309 fee_percent: default_ldk_fee_percent(),
310 reserve_fee_min: default_ldk_reserve_fee_min(),
311 bitcoin_network: None,
312 chain_source_type: None,
313 esplora_url: None,
314 bitcoind_rpc_host: None,
315 bitcoind_rpc_port: None,
316 bitcoind_rpc_user: None,
317 bitcoind_rpc_password: None,
318 storage_dir_path: None,
319 ldk_node_host: None,
320 ldk_node_port: None,
321 gossip_source_type: None,
322 rgs_url: None,
323 webserver_host: default_webserver_host(),
324 webserver_port: default_webserver_port(),
325 }
326 }
327}
328
329#[cfg(feature = "ldk-node")]
330fn default_ldk_fee_percent() -> f32 {
331 0.04
332}
333
334#[cfg(feature = "ldk-node")]
335fn default_ldk_reserve_fee_min() -> Amount {
336 4.into()
337}
338
339#[cfg(feature = "ldk-node")]
340fn default_webserver_host() -> Option<String> {
341 Some("127.0.0.1".to_string())
342}
343
344#[cfg(feature = "ldk-node")]
345fn default_webserver_port() -> Option<u16> {
346 Some(8091)
347}
348
349#[cfg(feature = "fakewallet")]
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct FakeWallet {
352 pub supported_units: Vec<CurrencyUnit>,
353 pub fee_percent: f32,
354 pub reserve_fee_min: Amount,
355 #[serde(default = "default_min_delay_time")]
356 pub min_delay_time: u64,
357 #[serde(default = "default_max_delay_time")]
358 pub max_delay_time: u64,
359}
360
361#[cfg(feature = "fakewallet")]
362impl Default for FakeWallet {
363 fn default() -> Self {
364 Self {
365 supported_units: vec![CurrencyUnit::Sat],
366 fee_percent: 0.02,
367 reserve_fee_min: 2.into(),
368 min_delay_time: 1,
369 max_delay_time: 3,
370 }
371 }
372}
373
374fn default_fee_percent() -> f32 {
377 0.02
378}
379
380fn default_reserve_fee_min() -> Amount {
381 2.into()
382}
383
384#[cfg(feature = "fakewallet")]
385fn default_min_delay_time() -> u64 {
386 1
387}
388
389#[cfg(feature = "fakewallet")]
390fn default_max_delay_time() -> u64 {
391 3
392}
393
394#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
395pub struct GrpcProcessor {
396 #[serde(default)]
397 pub supported_units: Vec<CurrencyUnit>,
398 #[serde(default = "default_grpc_addr")]
399 pub addr: String,
400 #[serde(default = "default_grpc_port")]
401 pub port: u16,
402 #[serde(default)]
403 pub tls_dir: Option<PathBuf>,
404}
405
406impl Default for GrpcProcessor {
407 fn default() -> Self {
408 Self {
409 supported_units: Vec::new(),
410 addr: default_grpc_addr(),
411 port: default_grpc_port(),
412 tls_dir: None,
413 }
414 }
415}
416
417fn default_grpc_addr() -> String {
418 "127.0.0.1".to_string()
419}
420
421fn default_grpc_port() -> u16 {
422 50051
423}
424
425#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
426#[serde(rename_all = "lowercase")]
427pub enum DatabaseEngine {
428 #[default]
429 Sqlite,
430 Postgres,
431}
432
433impl std::str::FromStr for DatabaseEngine {
434 type Err = String;
435
436 fn from_str(s: &str) -> Result<Self, Self::Err> {
437 match s.to_lowercase().as_str() {
438 "sqlite" => Ok(DatabaseEngine::Sqlite),
439 "postgres" => Ok(DatabaseEngine::Postgres),
440 _ => Err(format!("Unknown database engine: {s}")),
441 }
442 }
443}
444
445#[derive(Debug, Clone, Serialize, Deserialize, Default)]
446pub struct Database {
447 pub engine: DatabaseEngine,
448 pub postgres: Option<PostgresConfig>,
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize, Default)]
452pub struct AuthDatabase {
453 pub postgres: Option<PostgresAuthConfig>,
454}
455
456#[derive(Debug, Clone, Serialize, Deserialize)]
457pub struct PostgresAuthConfig {
458 pub url: String,
459 pub tls_mode: Option<String>,
460 pub max_connections: Option<usize>,
461 pub connection_timeout_seconds: Option<u64>,
462}
463
464impl Default for PostgresAuthConfig {
465 fn default() -> Self {
466 Self {
467 url: String::new(),
468 tls_mode: Some("disable".to_string()),
469 max_connections: Some(20),
470 connection_timeout_seconds: Some(10),
471 }
472 }
473}
474
475#[derive(Debug, Clone, Serialize, Deserialize)]
476pub struct PostgresConfig {
477 pub url: String,
478 pub tls_mode: Option<String>,
479 pub max_connections: Option<usize>,
480 pub connection_timeout_seconds: Option<u64>,
481}
482
483impl Default for PostgresConfig {
484 fn default() -> Self {
485 Self {
486 url: String::new(),
487 tls_mode: Some("disable".to_string()),
488 max_connections: Some(20),
489 connection_timeout_seconds: Some(10),
490 }
491 }
492}
493
494#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
495#[serde(rename_all = "lowercase")]
496pub enum AuthType {
497 Clear,
498 Blind,
499 #[default]
500 None,
501}
502
503impl std::str::FromStr for AuthType {
504 type Err = String;
505
506 fn from_str(s: &str) -> Result<Self, Self::Err> {
507 match s.to_lowercase().as_str() {
508 "clear" => Ok(AuthType::Clear),
509 "blind" => Ok(AuthType::Blind),
510 "none" => Ok(AuthType::None),
511 _ => Err(format!("Unknown auth type: {s}")),
512 }
513 }
514}
515
516#[derive(Debug, Clone, Default, Serialize, Deserialize)]
517pub struct Auth {
518 #[serde(default)]
519 pub auth_enabled: bool,
520 pub openid_discovery: String,
521 pub openid_client_id: String,
522 pub mint_max_bat: u64,
523 #[serde(default = "default_blind")]
524 pub mint: AuthType,
525 #[serde(default)]
526 pub get_mint_quote: AuthType,
527 #[serde(default)]
528 pub check_mint_quote: AuthType,
529 #[serde(default)]
530 pub melt: AuthType,
531 #[serde(default)]
532 pub get_melt_quote: AuthType,
533 #[serde(default)]
534 pub check_melt_quote: AuthType,
535 #[serde(default = "default_blind")]
536 pub swap: AuthType,
537 #[serde(default = "default_blind")]
538 pub restore: AuthType,
539 #[serde(default)]
540 pub check_proof_state: AuthType,
541 #[serde(default = "default_blind")]
543 pub websocket_auth: AuthType,
544}
545
546fn default_blind() -> AuthType {
547 AuthType::Blind
548}
549
550#[derive(Debug, Clone, Serialize, Deserialize, Default)]
552pub struct Settings {
553 pub info: Info,
554 pub mint_info: MintInfo,
555 pub ln: Ln,
556 #[cfg(feature = "cln")]
557 pub cln: Option<Cln>,
558 #[cfg(feature = "lnbits")]
559 pub lnbits: Option<LNbits>,
560 #[cfg(feature = "lnd")]
561 pub lnd: Option<Lnd>,
562 #[cfg(feature = "ldk-node")]
563 pub ldk_node: Option<LdkNode>,
564 #[cfg(feature = "fakewallet")]
565 pub fake_wallet: Option<FakeWallet>,
566 pub grpc_processor: Option<GrpcProcessor>,
567 pub database: Database,
568 #[cfg(feature = "auth")]
569 pub auth_database: Option<AuthDatabase>,
570 #[cfg(feature = "management-rpc")]
571 pub mint_management_rpc: Option<MintManagementRpc>,
572 pub auth: Option<Auth>,
573 #[cfg(feature = "prometheus")]
574 pub prometheus: Option<Prometheus>,
575}
576
577#[derive(Debug, Clone, Serialize, Deserialize, Default)]
578#[cfg(feature = "prometheus")]
579pub struct Prometheus {
580 pub enabled: bool,
581 pub address: Option<String>,
582 pub port: Option<u16>,
583}
584
585#[derive(Debug, Clone, Serialize, Deserialize, Default)]
586pub struct MintInfo {
587 pub name: String,
589 pub pubkey: Option<PublicKey>,
591 pub description: String,
593 pub description_long: Option<String>,
595 pub icon_url: Option<String>,
597 pub motd: Option<String>,
599 pub contact_nostr_public_key: Option<String>,
601 pub contact_email: Option<String>,
603 pub tos_url: Option<String>,
605}
606
607#[cfg(feature = "management-rpc")]
608#[derive(Debug, Clone, Serialize, Deserialize, Default)]
609pub struct MintManagementRpc {
610 pub enabled: bool,
613 pub address: Option<String>,
614 pub port: Option<u16>,
615 pub tls_dir_path: Option<PathBuf>,
616}
617
618impl Settings {
619 #[must_use]
620 pub fn new<P>(config_file_name: Option<P>) -> Self
621 where
622 P: Into<PathBuf>,
623 {
624 let default_settings = Self::default();
625 let from_file = Self::new_from_default(&default_settings, config_file_name);
627 match from_file {
628 Ok(f) => f,
629 Err(e) => {
630 tracing::error!(
631 "Error reading config file, falling back to defaults. Error: {e:?}"
632 );
633 default_settings
634 }
635 }
636 }
637
638 fn new_from_default<P>(
639 default: &Settings,
640 config_file_name: Option<P>,
641 ) -> Result<Self, ConfigError>
642 where
643 P: Into<PathBuf>,
644 {
645 let mut default_config_file_name = home::home_dir()
646 .ok_or(ConfigError::NotFound("Config Path".to_string()))?
647 .join("cashu-rs-mint");
648
649 default_config_file_name.push("config.toml");
650 let config: String = match config_file_name {
651 Some(value) => value.into().to_string_lossy().to_string(),
652 None => default_config_file_name.to_string_lossy().to_string(),
653 };
654 let builder = Config::builder();
655 let config: Config = builder
656 .add_source(Config::try_from(default)?)
658 .add_source(File::with_name(&config))
660 .build()?;
661 let settings: Settings = config.try_deserialize()?;
662
663 Ok(settings)
664 }
665}
666
667#[cfg(test)]
668mod tests {
669
670 use super::*;
671
672 #[test]
673 fn test_info_debug_impl() {
674 let info = Info {
676 url: "http://example.com".to_string(),
677 listen_host: "127.0.0.1".to_string(),
678 listen_port: 8080,
679 mnemonic: Some("test secret mnemonic phrase".to_string()),
680 input_fee_ppk: Some(100),
681 ..Default::default()
682 };
683
684 let debug_output = format!("{info:?}");
686
687 assert!(debug_output.contains("url: \"http://example.com\""));
689 assert!(debug_output.contains("listen_host: \"127.0.0.1\""));
690 assert!(debug_output.contains("listen_port: 8080"));
691
692 assert!(!debug_output.contains("test secret mnemonic phrase"));
694 assert!(debug_output.contains("<hashed: "));
695
696 assert!(debug_output.contains("input_fee_ppk: Some(100)"));
697 }
698
699 #[test]
700 fn test_info_debug_with_empty_mnemonic() {
701 let info = Info {
703 url: "http://example.com".to_string(),
704 listen_host: "127.0.0.1".to_string(),
705 listen_port: 8080,
706 mnemonic: Some("".to_string()), enable_swagger_ui: Some(false),
708 ..Default::default()
709 };
710
711 let debug_output = format!("{:?}", info);
713
714 assert!(debug_output.contains("<hashed: "));
716 }
717
718 #[test]
719 fn test_info_debug_with_special_chars() {
720 let info = Info {
722 url: "http://example.com".to_string(),
723 listen_host: "127.0.0.1".to_string(),
724 listen_port: 8080,
725 mnemonic: Some("特殊字符 !@#$%^&*()".to_string()), ..Default::default()
727 };
728
729 let debug_output = format!("{:?}", info);
731
732 assert!(!debug_output.contains("特殊字符 !@#$%^&*()"));
734 assert!(debug_output.contains("<hashed: "));
735 }
736
737 #[test]
742 fn test_env_var_only_config_all_backends() {
743 #[cfg(feature = "lnd")]
745 test_lnd_env_config();
746
747 #[cfg(feature = "cln")]
748 test_cln_env_config();
749
750 #[cfg(feature = "lnbits")]
751 test_lnbits_env_config();
752
753 #[cfg(feature = "fakewallet")]
754 test_fakewallet_env_config();
755
756 #[cfg(feature = "grpc-processor")]
757 test_grpc_processor_env_config();
758
759 #[cfg(feature = "ldk-node")]
760 test_ldk_node_env_config();
761 }
762
763 #[cfg(feature = "lnd")]
764 fn test_lnd_env_config() {
765 use std::path::PathBuf;
766 use std::{env, fs};
767
768 let temp_dir = env::temp_dir().join("cdk_test_env_vars");
770 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
771 let config_path = temp_dir.join("config.toml");
772
773 let config_content = r#"
775[ln]
776backend = "lnd"
777min_mint = 1
778max_mint = 500000
779min_melt = 1
780max_melt = 500000
781"#;
782 fs::write(&config_path, config_content).expect("Failed to write config file");
783
784 env::set_var(crate::env_vars::ENV_LN_BACKEND, "lnd");
786 env::set_var(crate::env_vars::ENV_LND_ADDRESS, "https://localhost:10009");
787 env::set_var(crate::env_vars::ENV_LND_CERT_FILE, "/tmp/test_tls.cert");
788 env::set_var(
789 crate::env_vars::ENV_LND_MACAROON_FILE,
790 "/tmp/test_admin.macaroon",
791 );
792 env::set_var(crate::env_vars::ENV_LND_FEE_PERCENT, "0.01");
793 env::set_var(crate::env_vars::ENV_LND_RESERVE_FEE_MIN, "4");
794
795 let mut settings = Settings::new(Some(&config_path));
797 settings.from_env().expect("Failed to apply env vars");
798
799 assert!(settings.lnd.is_some());
801 let lnd_config = settings.lnd.as_ref().unwrap();
802 assert_eq!(lnd_config.address, "https://localhost:10009");
803 assert_eq!(lnd_config.cert_file, PathBuf::from("/tmp/test_tls.cert"));
804 assert_eq!(
805 lnd_config.macaroon_file,
806 PathBuf::from("/tmp/test_admin.macaroon")
807 );
808 assert_eq!(lnd_config.fee_percent, 0.01);
809 let reserve_fee_u64: u64 = lnd_config.reserve_fee_min.into();
810 assert_eq!(reserve_fee_u64, 4);
811
812 env::remove_var(crate::env_vars::ENV_LN_BACKEND);
814 env::remove_var(crate::env_vars::ENV_LND_ADDRESS);
815 env::remove_var(crate::env_vars::ENV_LND_CERT_FILE);
816 env::remove_var(crate::env_vars::ENV_LND_MACAROON_FILE);
817 env::remove_var(crate::env_vars::ENV_LND_FEE_PERCENT);
818 env::remove_var(crate::env_vars::ENV_LND_RESERVE_FEE_MIN);
819
820 let _ = fs::remove_dir_all(&temp_dir);
822 }
823
824 #[cfg(feature = "cln")]
825 fn test_cln_env_config() {
826 use std::path::PathBuf;
827 use std::{env, fs};
828
829 let temp_dir = env::temp_dir().join("cdk_test_env_vars_cln");
831 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
832 let config_path = temp_dir.join("config.toml");
833
834 let config_content = r#"
836[ln]
837backend = "cln"
838min_mint = 1
839max_mint = 500000
840min_melt = 1
841max_melt = 500000
842"#;
843 fs::write(&config_path, config_content).expect("Failed to write config file");
844
845 env::set_var(crate::env_vars::ENV_LN_BACKEND, "cln");
847 env::set_var(crate::env_vars::ENV_CLN_RPC_PATH, "/tmp/lightning-rpc");
848 env::set_var(crate::env_vars::ENV_CLN_BOLT12, "false");
849 env::set_var(crate::env_vars::ENV_CLN_FEE_PERCENT, "0.01");
850 env::set_var(crate::env_vars::ENV_CLN_RESERVE_FEE_MIN, "4");
851
852 let mut settings = Settings::new(Some(&config_path));
854 settings.from_env().expect("Failed to apply env vars");
855
856 assert!(settings.cln.is_some());
858 let cln_config = settings.cln.as_ref().unwrap();
859 assert_eq!(cln_config.rpc_path, PathBuf::from("/tmp/lightning-rpc"));
860 assert_eq!(cln_config.bolt12, false);
861 assert_eq!(cln_config.fee_percent, 0.01);
862 let reserve_fee_u64: u64 = cln_config.reserve_fee_min.into();
863 assert_eq!(reserve_fee_u64, 4);
864
865 env::remove_var(crate::env_vars::ENV_LN_BACKEND);
867 env::remove_var(crate::env_vars::ENV_CLN_RPC_PATH);
868 env::remove_var(crate::env_vars::ENV_CLN_BOLT12);
869 env::remove_var(crate::env_vars::ENV_CLN_FEE_PERCENT);
870 env::remove_var(crate::env_vars::ENV_CLN_RESERVE_FEE_MIN);
871
872 let _ = fs::remove_dir_all(&temp_dir);
874 }
875
876 #[cfg(feature = "lnbits")]
877 fn test_lnbits_env_config() {
878 use std::{env, fs};
879
880 let temp_dir = env::temp_dir().join("cdk_test_env_vars_lnbits");
882 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
883 let config_path = temp_dir.join("config.toml");
884
885 let config_content = r#"
887[ln]
888backend = "lnbits"
889min_mint = 1
890max_mint = 500000
891min_melt = 1
892max_melt = 500000
893"#;
894 fs::write(&config_path, config_content).expect("Failed to write config file");
895
896 env::set_var(crate::env_vars::ENV_LN_BACKEND, "lnbits");
898 env::set_var(crate::env_vars::ENV_LNBITS_ADMIN_API_KEY, "test_admin_key");
899 env::set_var(
900 crate::env_vars::ENV_LNBITS_INVOICE_API_KEY,
901 "test_invoice_key",
902 );
903 env::set_var(
904 crate::env_vars::ENV_LNBITS_API,
905 "https://lnbits.example.com",
906 );
907 env::set_var(crate::env_vars::ENV_LNBITS_FEE_PERCENT, "0.02");
908 env::set_var(crate::env_vars::ENV_LNBITS_RESERVE_FEE_MIN, "5");
909
910 let mut settings = Settings::new(Some(&config_path));
912 settings.from_env().expect("Failed to apply env vars");
913
914 assert!(settings.lnbits.is_some());
916 let lnbits_config = settings.lnbits.as_ref().unwrap();
917 assert_eq!(lnbits_config.admin_api_key, "test_admin_key");
918 assert_eq!(lnbits_config.invoice_api_key, "test_invoice_key");
919 assert_eq!(lnbits_config.lnbits_api, "https://lnbits.example.com");
920 assert_eq!(lnbits_config.fee_percent, 0.02);
921 let reserve_fee_u64: u64 = lnbits_config.reserve_fee_min.into();
922 assert_eq!(reserve_fee_u64, 5);
923
924 env::remove_var(crate::env_vars::ENV_LN_BACKEND);
926 env::remove_var(crate::env_vars::ENV_LNBITS_ADMIN_API_KEY);
927 env::remove_var(crate::env_vars::ENV_LNBITS_INVOICE_API_KEY);
928 env::remove_var(crate::env_vars::ENV_LNBITS_API);
929 env::remove_var(crate::env_vars::ENV_LNBITS_FEE_PERCENT);
930 env::remove_var(crate::env_vars::ENV_LNBITS_RESERVE_FEE_MIN);
931
932 let _ = fs::remove_dir_all(&temp_dir);
934 }
935
936 #[cfg(feature = "fakewallet")]
937 fn test_fakewallet_env_config() {
938 use std::{env, fs};
939
940 let temp_dir = env::temp_dir().join("cdk_test_env_vars_fakewallet");
942 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
943 let config_path = temp_dir.join("config.toml");
944
945 let config_content = r#"
947[ln]
948backend = "fakewallet"
949min_mint = 1
950max_mint = 500000
951min_melt = 1
952max_melt = 500000
953"#;
954 fs::write(&config_path, config_content).expect("Failed to write config file");
955
956 env::set_var(crate::env_vars::ENV_LN_BACKEND, "fakewallet");
958 env::set_var(crate::env_vars::ENV_FAKE_WALLET_SUPPORTED_UNITS, "sat,msat");
959 env::set_var(crate::env_vars::ENV_FAKE_WALLET_FEE_PERCENT, "0.0");
960 env::set_var(crate::env_vars::ENV_FAKE_WALLET_RESERVE_FEE_MIN, "0");
961 env::set_var(crate::env_vars::ENV_FAKE_WALLET_MIN_DELAY, "0");
962 env::set_var(crate::env_vars::ENV_FAKE_WALLET_MAX_DELAY, "5");
963
964 let mut settings = Settings::new(Some(&config_path));
966 settings.from_env().expect("Failed to apply env vars");
967
968 assert!(settings.fake_wallet.is_some());
970 let fakewallet_config = settings.fake_wallet.as_ref().unwrap();
971 assert_eq!(fakewallet_config.fee_percent, 0.0);
972 let reserve_fee_u64: u64 = fakewallet_config.reserve_fee_min.into();
973 assert_eq!(reserve_fee_u64, 0);
974 assert_eq!(fakewallet_config.min_delay_time, 0);
975 assert_eq!(fakewallet_config.max_delay_time, 5);
976
977 env::remove_var(crate::env_vars::ENV_LN_BACKEND);
979 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_SUPPORTED_UNITS);
980 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_FEE_PERCENT);
981 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_RESERVE_FEE_MIN);
982 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_MIN_DELAY);
983 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_MAX_DELAY);
984
985 let _ = fs::remove_dir_all(&temp_dir);
987 }
988
989 #[cfg(feature = "grpc-processor")]
990 fn test_grpc_processor_env_config() {
991 use std::{env, fs};
992
993 let temp_dir = env::temp_dir().join("cdk_test_env_vars_grpc");
995 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
996 let config_path = temp_dir.join("config.toml");
997
998 let config_content = r#"
1000[ln]
1001backend = "grpcprocessor"
1002min_mint = 1
1003max_mint = 500000
1004min_melt = 1
1005max_melt = 500000
1006"#;
1007 fs::write(&config_path, config_content).expect("Failed to write config file");
1008
1009 env::set_var(crate::env_vars::ENV_LN_BACKEND, "grpcprocessor");
1011 env::set_var(
1012 crate::env_vars::ENV_GRPC_PROCESSOR_SUPPORTED_UNITS,
1013 "sat,msat",
1014 );
1015 env::set_var(crate::env_vars::ENV_GRPC_PROCESSOR_ADDRESS, "localhost");
1016 env::set_var(crate::env_vars::ENV_GRPC_PROCESSOR_PORT, "50051");
1017
1018 let mut settings = Settings::new(Some(&config_path));
1020 settings.from_env().expect("Failed to apply env vars");
1021
1022 assert!(settings.grpc_processor.is_some());
1024 let grpc_config = settings.grpc_processor.as_ref().unwrap();
1025 assert_eq!(grpc_config.addr, "localhost");
1026 assert_eq!(grpc_config.port, 50051);
1027
1028 env::remove_var(crate::env_vars::ENV_LN_BACKEND);
1030 env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_SUPPORTED_UNITS);
1031 env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_ADDRESS);
1032 env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_PORT);
1033
1034 let _ = fs::remove_dir_all(&temp_dir);
1036 }
1037
1038 #[cfg(feature = "ldk-node")]
1039 fn test_ldk_node_env_config() {
1040 use std::{env, fs};
1041
1042 let temp_dir = env::temp_dir().join("cdk_test_env_vars_ldk");
1044 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1045 let config_path = temp_dir.join("config.toml");
1046
1047 let config_content = r#"
1049[ln]
1050backend = "ldknode"
1051min_mint = 1
1052max_mint = 500000
1053min_melt = 1
1054max_melt = 500000
1055"#;
1056 fs::write(&config_path, config_content).expect("Failed to write config file");
1057
1058 env::set_var(crate::env_vars::ENV_LN_BACKEND, "ldknode");
1060 env::set_var(crate::env_vars::LDK_NODE_FEE_PERCENT_ENV_VAR, "0.01");
1061 env::set_var(crate::env_vars::LDK_NODE_RESERVE_FEE_MIN_ENV_VAR, "4");
1062 env::set_var(crate::env_vars::LDK_NODE_BITCOIN_NETWORK_ENV_VAR, "regtest");
1063 env::set_var(
1064 crate::env_vars::LDK_NODE_CHAIN_SOURCE_TYPE_ENV_VAR,
1065 "esplora",
1066 );
1067 env::set_var(
1068 crate::env_vars::LDK_NODE_ESPLORA_URL_ENV_VAR,
1069 "http://localhost:3000",
1070 );
1071 env::set_var(
1072 crate::env_vars::LDK_NODE_STORAGE_DIR_PATH_ENV_VAR,
1073 "/tmp/ldk",
1074 );
1075
1076 let mut settings = Settings::new(Some(&config_path));
1078 settings.from_env().expect("Failed to apply env vars");
1079
1080 assert!(settings.ldk_node.is_some());
1082 let ldk_config = settings.ldk_node.as_ref().unwrap();
1083 assert_eq!(ldk_config.fee_percent, 0.01);
1084 let reserve_fee_u64: u64 = ldk_config.reserve_fee_min.into();
1085 assert_eq!(reserve_fee_u64, 4);
1086 assert_eq!(ldk_config.bitcoin_network, Some("regtest".to_string()));
1087 assert_eq!(ldk_config.chain_source_type, Some("esplora".to_string()));
1088 assert_eq!(
1089 ldk_config.esplora_url,
1090 Some("http://localhost:3000".to_string())
1091 );
1092 assert_eq!(ldk_config.storage_dir_path, Some("/tmp/ldk".to_string()));
1093
1094 env::remove_var(crate::env_vars::ENV_LN_BACKEND);
1096 env::remove_var(crate::env_vars::LDK_NODE_FEE_PERCENT_ENV_VAR);
1097 env::remove_var(crate::env_vars::LDK_NODE_RESERVE_FEE_MIN_ENV_VAR);
1098 env::remove_var(crate::env_vars::LDK_NODE_BITCOIN_NETWORK_ENV_VAR);
1099 env::remove_var(crate::env_vars::LDK_NODE_CHAIN_SOURCE_TYPE_ENV_VAR);
1100 env::remove_var(crate::env_vars::LDK_NODE_ESPLORA_URL_ENV_VAR);
1101 env::remove_var(crate::env_vars::LDK_NODE_STORAGE_DIR_PATH_ENV_VAR);
1102
1103 let _ = fs::remove_dir_all(&temp_dir);
1105 }
1106}