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_info_page: 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_info_page: Some(true),
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_info_page", &self.enable_info_page)
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 #[serde(default)]
173 pub unit: CurrencyUnit,
174 pub invoice_description: Option<String>,
175 pub min_mint: Amount,
176 pub max_mint: Amount,
177 pub min_melt: Amount,
178 pub max_melt: Amount,
179}
180
181impl Default for Ln {
182 fn default() -> Self {
183 Ln {
184 ln_backend: LnBackend::default(),
185 unit: CurrencyUnit::default(),
186 invoice_description: None,
187 min_mint: 1.into(),
188 max_mint: 500_000.into(),
189 min_melt: 1.into(),
190 max_melt: 500_000.into(),
191 }
192 }
193}
194
195fn deserialize_ln<'de, D>(deserializer: D) -> Result<Vec<Ln>, D::Error>
196where
197 D: serde::Deserializer<'de>,
198{
199 #[derive(Deserialize)]
200 #[serde(untagged)]
201 enum LnOneOrMany {
202 Many(Vec<Ln>),
203 One(Ln),
204 }
205
206 match LnOneOrMany::deserialize(deserializer)? {
207 LnOneOrMany::Many(ln) => Ok(ln),
208 LnOneOrMany::One(ln) => Ok(vec![ln]),
209 }
210}
211
212#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
213#[serde(rename_all = "lowercase")]
214pub enum OnchainBackend {
215 #[default]
216 None,
217 #[cfg(feature = "bdk")]
218 Bdk,
219 #[cfg(feature = "fakewallet")]
220 FakeWallet,
221}
222
223impl std::str::FromStr for OnchainBackend {
224 type Err = String;
225
226 fn from_str(s: &str) -> Result<Self, Self::Err> {
227 match s.to_lowercase().as_str() {
228 "none" => Ok(OnchainBackend::None),
229 #[cfg(feature = "bdk")]
230 "bdk" => Ok(OnchainBackend::Bdk),
231 #[cfg(feature = "fakewallet")]
232 "fakewallet" => Ok(OnchainBackend::FakeWallet),
233 _ => Err(format!("Unknown Onchain backend: {s}")),
234 }
235 }
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct Onchain {
240 pub onchain_backend: OnchainBackend,
241 pub min_mint: Amount,
242 pub max_mint: Amount,
243 pub min_melt: Amount,
244 pub max_melt: Amount,
245}
246
247impl Default for Onchain {
248 fn default() -> Self {
249 Onchain {
250 onchain_backend: OnchainBackend::default(),
251 min_mint: 1.into(),
252 max_mint: 500_000.into(),
253 min_melt: 1.into(),
254 max_melt: 500_000.into(),
255 }
256 }
257}
258
259#[cfg(feature = "bdk")]
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct BatchConfig {
262 #[serde(default = "default_bdk_poll_interval_secs")]
264 pub poll_interval_secs: u64,
265 #[serde(default = "default_bdk_max_batch_size")]
267 pub max_batch_size: usize,
268 #[serde(default = "default_bdk_target_block_time_secs")]
270 pub target_block_time_secs: u64,
271 #[serde(default)]
273 pub standard_deadline_secs: Option<u64>,
274 #[serde(default)]
276 pub economy_deadline_secs: Option<u64>,
277 #[serde(default = "default_bdk_fee_options")]
279 pub fee_options: Vec<String>,
280 #[serde(default = "default_bdk_fee_fallback_sat_per_vb")]
282 pub fee_fallback_sat_per_vb: f64,
283 #[serde(default = "default_bdk_fee_cache_ttl_secs")]
285 pub fee_cache_ttl_secs: u64,
286 #[serde(default = "default_bdk_quote_max_input_count")]
288 pub quote_max_input_count: usize,
289 #[serde(default = "default_bdk_quote_fixed_safety_sat")]
291 pub quote_fixed_safety_sat: u64,
292 #[serde(default = "default_bdk_quote_safety_multiplier")]
294 pub quote_safety_multiplier: f64,
295}
296
297#[cfg(feature = "bdk")]
298impl Default for BatchConfig {
299 fn default() -> Self {
300 Self {
301 poll_interval_secs: default_bdk_poll_interval_secs(),
302 max_batch_size: default_bdk_max_batch_size(),
303 target_block_time_secs: default_bdk_target_block_time_secs(),
304 standard_deadline_secs: None,
305 economy_deadline_secs: None,
306 fee_options: default_bdk_fee_options(),
307 fee_fallback_sat_per_vb: default_bdk_fee_fallback_sat_per_vb(),
308 fee_cache_ttl_secs: default_bdk_fee_cache_ttl_secs(),
309 quote_max_input_count: default_bdk_quote_max_input_count(),
310 quote_fixed_safety_sat: default_bdk_quote_fixed_safety_sat(),
311 quote_safety_multiplier: default_bdk_quote_safety_multiplier(),
312 }
313 }
314}
315
316#[cfg(feature = "bdk")]
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct Bdk {
319 #[serde(default = "default_fee_percent")]
321 pub fee_percent: f32,
322 #[serde(default = "default_reserve_fee_min")]
324 pub reserve_fee_min: Amount,
325 pub network: Option<String>,
327 pub chain_source_type: Option<String>,
329 pub esplora_url: Option<String>,
331 #[serde(default = "default_bdk_esplora_parallel_requests")]
337 pub esplora_parallel_requests: usize,
338 pub bitcoind_rpc_host: Option<String>,
340 pub bitcoind_rpc_port: Option<u16>,
342 pub bitcoind_rpc_user: Option<String>,
344 pub bitcoind_rpc_password: Option<String>,
346 pub mnemonic: Option<String>,
348 #[serde(default)]
350 pub batch_config: BatchConfig,
351 #[serde(default = "default_bdk_num_confs")]
358 pub num_confs: u32,
359 #[serde(default = "default_bdk_min_receive_amount_sat")]
361 pub min_receive_amount_sat: u64,
362 #[serde(default = "default_bdk_min_send_amount_sat")]
364 pub min_send_amount_sat: u64,
365 #[serde(default = "default_bdk_sync_interval_secs")]
367 pub sync_interval_secs: u64,
368}
369
370#[cfg(feature = "bdk")]
371impl Default for Bdk {
372 fn default() -> Self {
373 Self {
374 fee_percent: default_fee_percent(),
375 reserve_fee_min: default_reserve_fee_min(),
376 network: None,
377 chain_source_type: None,
378 esplora_url: None,
379 esplora_parallel_requests: default_bdk_esplora_parallel_requests(),
380 bitcoind_rpc_host: None,
381 bitcoind_rpc_port: None,
382 bitcoind_rpc_user: None,
383 bitcoind_rpc_password: None,
384 mnemonic: None,
385 batch_config: BatchConfig::default(),
386 num_confs: default_bdk_num_confs(),
387 min_receive_amount_sat: default_bdk_min_receive_amount_sat(),
388 min_send_amount_sat: default_bdk_min_send_amount_sat(),
389 sync_interval_secs: default_bdk_sync_interval_secs(),
390 }
391 }
392}
393
394#[cfg(feature = "bdk")]
395impl Bdk {
396 pub fn validate(&self) -> Result<(), String> {
398 if self.num_confs == 0 {
399 return Err(
400 "BDK num_confs must be >= 1 (0 is rejected because it still \
401 requires an on-chain anchor and is almost never intended; \
402 use 1 for 'any confirmation')"
403 .to_string(),
404 );
405 }
406
407 if self.min_send_amount_sat == 0 {
408 return Err("BDK min_send_amount_sat must be >= 1".to_string());
409 }
410
411 if self.batch_config.target_block_time_secs == 0 {
412 return Err("BDK batch_config.target_block_time_secs must be >= 1".to_string());
413 }
414
415 validate_bdk_fee_options(&self.batch_config.fee_options)?;
416
417 Ok(())
418 }
419}
420
421#[cfg(feature = "bdk")]
422fn default_bdk_num_confs() -> u32 {
423 6
424}
425
426#[cfg(feature = "bdk")]
427fn default_bdk_min_receive_amount_sat() -> u64 {
428 1000
429}
430
431#[cfg(feature = "bdk")]
432fn default_bdk_min_send_amount_sat() -> u64 {
433 546
434}
435
436#[cfg(feature = "bdk")]
437fn default_bdk_sync_interval_secs() -> u64 {
438 30
439}
440
441#[cfg(feature = "bdk")]
442fn default_bdk_esplora_parallel_requests() -> usize {
443 1
444}
445
446#[cfg(feature = "bdk")]
447fn default_bdk_poll_interval_secs() -> u64 {
448 30
449}
450
451#[cfg(feature = "bdk")]
452fn default_bdk_max_batch_size() -> usize {
453 50
454}
455
456#[cfg(feature = "bdk")]
457fn default_bdk_target_block_time_secs() -> u64 {
458 cdk_bdk::DEFAULT_TARGET_BLOCK_TIME_SECS
459}
460
461#[cfg(feature = "bdk")]
462fn default_bdk_fee_options() -> Vec<String> {
463 vec!["immediate".to_string()]
464}
465
466#[cfg(feature = "bdk")]
467fn validate_bdk_fee_options(fee_options: &[String]) -> Result<(), String> {
468 let tiers = fee_options
469 .iter()
470 .map(|tier| {
471 cdk_bdk::PaymentTier::from_config_name(tier).ok_or_else(|| {
472 format!(
473 "Unknown BDK batch_config.fee_options tier '{tier}'; expected immediate, standard, or economy"
474 )
475 })
476 })
477 .collect::<Result<Vec<_>, _>>()?;
478
479 cdk_bdk::types::validate_fee_options(&tiers)
480}
481
482#[cfg(feature = "bdk")]
483fn default_bdk_fee_fallback_sat_per_vb() -> f64 {
484 2.0
485}
486
487#[cfg(feature = "bdk")]
488fn default_bdk_fee_cache_ttl_secs() -> u64 {
489 60
490}
491
492#[cfg(feature = "bdk")]
493fn default_bdk_quote_max_input_count() -> usize {
494 24
495}
496
497#[cfg(feature = "bdk")]
498fn default_bdk_quote_fixed_safety_sat() -> u64 {
499 500
500}
501
502#[cfg(feature = "bdk")]
503fn default_bdk_quote_safety_multiplier() -> f64 {
504 1.25
505}
506
507#[cfg(feature = "lnbits")]
508#[derive(Clone, Serialize, Deserialize)]
509pub struct LNbits {
510 pub admin_api_key: String,
511 pub invoice_api_key: String,
512 pub lnbits_api: String,
513 #[serde(default = "default_fee_percent")]
514 pub fee_percent: f32,
515 #[serde(default = "default_reserve_fee_min")]
516 pub reserve_fee_min: Amount,
517}
518
519#[cfg(feature = "lnbits")]
520impl std::fmt::Debug for LNbits {
521 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
522 f.debug_struct("LNbits")
523 .field("admin_api_key", &"[REDACTED]")
524 .field("invoice_api_key", &"[REDACTED]")
525 .field("lnbits_api", &self.lnbits_api)
526 .field("fee_percent", &self.fee_percent)
527 .field("reserve_fee_min", &self.reserve_fee_min)
528 .finish()
529 }
530}
531
532#[cfg(feature = "lnbits")]
533impl Default for LNbits {
534 fn default() -> Self {
535 Self {
536 admin_api_key: String::new(),
537 invoice_api_key: String::new(),
538 lnbits_api: String::new(),
539 fee_percent: 0.02,
540 reserve_fee_min: 2.into(),
541 }
542 }
543}
544
545#[cfg(feature = "cln")]
546#[derive(Debug, Clone, Serialize, Deserialize)]
547pub struct Cln {
548 pub rpc_path: PathBuf,
549 #[serde(default = "default_cln_bolt12")]
550 pub bolt12: bool,
551 #[serde(default)]
552 pub expose_private_channels: bool,
553 #[serde(default = "default_fee_percent")]
554 pub fee_percent: f32,
555 #[serde(default = "default_reserve_fee_min")]
556 pub reserve_fee_min: Amount,
557}
558
559#[cfg(feature = "cln")]
560impl Default for Cln {
561 fn default() -> Self {
562 Self {
563 rpc_path: PathBuf::new(),
564 bolt12: true,
565 expose_private_channels: false,
566 fee_percent: 0.02,
567 reserve_fee_min: 2.into(),
568 }
569 }
570}
571
572#[cfg(feature = "cln")]
573fn default_cln_bolt12() -> bool {
574 true
575}
576
577#[cfg(feature = "lnd")]
578#[derive(Debug, Clone, Serialize, Deserialize)]
579pub struct Lnd {
580 pub address: String,
581 pub cert_file: PathBuf,
582 pub macaroon_file: PathBuf,
583 #[serde(default = "default_fee_percent")]
584 pub fee_percent: f32,
585 #[serde(default = "default_reserve_fee_min")]
586 pub reserve_fee_min: Amount,
587}
588
589#[cfg(feature = "lnd")]
590impl Default for Lnd {
591 fn default() -> Self {
592 Self {
593 address: String::new(),
594 cert_file: PathBuf::new(),
595 macaroon_file: PathBuf::new(),
596 fee_percent: 0.02,
597 reserve_fee_min: 2.into(),
598 }
599 }
600}
601
602#[cfg(feature = "ldk-node")]
603#[derive(Clone, Serialize, Deserialize)]
604pub struct LdkNode {
605 #[serde(default = "default_ldk_fee_percent")]
607 pub fee_percent: f32,
608 #[serde(default = "default_ldk_reserve_fee_min")]
610 pub reserve_fee_min: Amount,
611 pub bitcoin_network: Option<String>,
613 pub chain_source_type: Option<String>,
615 pub esplora_url: Option<String>,
617 pub bitcoind_rpc_host: Option<String>,
619 pub bitcoind_rpc_port: Option<u16>,
620 pub bitcoind_rpc_user: Option<String>,
621 pub bitcoind_rpc_password: Option<String>,
622 pub storage_dir_path: Option<String>,
624 pub log_dir_path: Option<String>,
626 pub ldk_node_host: Option<String>,
628 pub ldk_node_port: Option<u16>,
630 pub ldk_node_announce_addresses: Option<Vec<String>>,
632 pub gossip_source_type: Option<String>,
634 pub rgs_url: Option<String>,
636 #[serde(default = "default_webserver_host")]
638 pub webserver_host: Option<String>,
639 #[serde(default = "default_webserver_port")]
641 pub webserver_port: Option<u16>,
642 pub ldk_node_mnemonic: Option<String>,
645}
646
647#[cfg(feature = "ldk-node")]
648impl Default for LdkNode {
649 fn default() -> Self {
650 Self {
651 fee_percent: default_ldk_fee_percent(),
652 reserve_fee_min: default_ldk_reserve_fee_min(),
653 bitcoin_network: None,
654 chain_source_type: None,
655 esplora_url: None,
656 bitcoind_rpc_host: None,
657 bitcoind_rpc_port: None,
658 bitcoind_rpc_user: None,
659 ldk_node_announce_addresses: None,
660 bitcoind_rpc_password: None,
661 storage_dir_path: None,
662 ldk_node_host: None,
663 log_dir_path: None,
664 ldk_node_port: None,
665 gossip_source_type: None,
666 rgs_url: None,
667 webserver_host: default_webserver_host(),
668 webserver_port: default_webserver_port(),
669 ldk_node_mnemonic: None,
670 }
671 }
672}
673
674#[cfg(feature = "ldk-node")]
675impl std::fmt::Debug for LdkNode {
676 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
677 f.debug_struct("LdkNode")
678 .field("fee_percent", &self.fee_percent)
679 .field("reserve_fee_min", &self.reserve_fee_min)
680 .field("bitcoin_network", &self.bitcoin_network)
681 .field("chain_source_type", &self.chain_source_type)
682 .field("esplora_url", &self.esplora_url)
683 .field("bitcoind_rpc_host", &self.bitcoind_rpc_host)
684 .field("bitcoind_rpc_port", &self.bitcoind_rpc_port)
685 .field("bitcoind_rpc_user", &self.bitcoind_rpc_user)
686 .field("bitcoind_rpc_password", &"[REDACTED]")
687 .field("storage_dir_path", &self.storage_dir_path)
688 .field("log_dir_path", &self.log_dir_path)
689 .field("ldk_node_host", &self.ldk_node_host)
690 .field("ldk_node_port", &self.ldk_node_port)
691 .field(
692 "ldk_node_announce_addresses",
693 &self.ldk_node_announce_addresses,
694 )
695 .field("gossip_source_type", &self.gossip_source_type)
696 .field("rgs_url", &self.rgs_url)
697 .field("webserver_host", &self.webserver_host)
698 .field("webserver_port", &self.webserver_port)
699 .field("ldk_node_mnemonic", &"[REDACTED]")
700 .finish()
701 }
702}
703
704#[cfg(feature = "ldk-node")]
705fn default_ldk_fee_percent() -> f32 {
706 0.04
707}
708
709#[cfg(feature = "ldk-node")]
710fn default_ldk_reserve_fee_min() -> Amount {
711 4.into()
712}
713
714#[cfg(feature = "ldk-node")]
715fn default_webserver_host() -> Option<String> {
716 Some("127.0.0.1".to_string())
717}
718
719#[cfg(feature = "ldk-node")]
720fn default_webserver_port() -> Option<u16> {
721 Some(8091)
722}
723
724#[cfg(feature = "fakewallet")]
725#[derive(Debug, Clone, Serialize, Deserialize)]
726pub struct FakeWalletKeysetRotation {
727 pub unit: CurrencyUnit,
729 #[serde(default)]
731 pub input_fee_ppk: u64,
732 #[serde(default = "default_keyset_version")]
734 pub version: String,
735 #[serde(default)]
737 pub expired: bool,
738}
739
740#[cfg(feature = "fakewallet")]
741fn default_keyset_version() -> String {
742 "v1".to_string()
743}
744
745#[cfg(feature = "fakewallet")]
746#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
747#[serde(untagged)]
748pub enum FakeWalletCustomPaymentMethod {
749 Method(String),
751 MethodForUnit {
753 method: String,
755 unit: CurrencyUnit,
757 },
758}
759
760#[cfg(feature = "fakewallet")]
761impl FakeWalletCustomPaymentMethod {
762 pub fn method(&self) -> &str {
763 match self {
764 Self::Method(method) => method,
765 Self::MethodForUnit { method, .. } => method,
766 }
767 }
768
769 pub fn applies_to_unit(&self, unit: &CurrencyUnit) -> bool {
770 match self {
771 Self::Method(_) => true,
772 Self::MethodForUnit {
773 unit: method_unit, ..
774 } => method_unit == unit,
775 }
776 }
777}
778
779#[cfg(feature = "fakewallet")]
780#[derive(Debug, Clone, Serialize, Deserialize)]
781pub struct FakeWallet {
782 #[serde(default = "default_fake_wallet_supported_units")]
783 pub supported_units: Vec<CurrencyUnit>,
784 pub fee_percent: f32,
785 pub reserve_fee_min: Amount,
786 #[serde(default = "default_fake_wallet_custom_payment_methods")]
787 pub custom_payment_methods: Vec<FakeWalletCustomPaymentMethod>,
788 #[serde(default = "default_min_delay_time")]
789 pub min_delay_time: u64,
790 #[serde(default = "default_max_delay_time")]
791 pub max_delay_time: u64,
792 #[serde(default)]
794 pub keyset_rotations: Vec<FakeWalletKeysetRotation>,
795}
796
797#[cfg(feature = "fakewallet")]
798impl Default for FakeWallet {
799 fn default() -> Self {
800 Self {
801 supported_units: vec![CurrencyUnit::Sat],
802 fee_percent: 0.02,
803 reserve_fee_min: 2.into(),
804 custom_payment_methods: default_fake_wallet_custom_payment_methods(),
805 min_delay_time: 1,
806 max_delay_time: 3,
807 keyset_rotations: Vec::new(),
808 }
809 }
810}
811
812#[cfg(any(feature = "cln", feature = "lnbits", feature = "lnd"))]
815fn default_fee_percent() -> f32 {
816 0.02
817}
818
819#[cfg(any(feature = "cln", feature = "lnbits", feature = "lnd"))]
820fn default_reserve_fee_min() -> Amount {
821 2.into()
822}
823
824#[cfg(feature = "fakewallet")]
825fn default_min_delay_time() -> u64 {
826 1
827}
828
829#[cfg(feature = "fakewallet")]
830fn default_max_delay_time() -> u64 {
831 3
832}
833
834#[cfg(feature = "fakewallet")]
835fn default_fake_wallet_custom_payment_methods() -> Vec<FakeWalletCustomPaymentMethod> {
836 vec![FakeWalletCustomPaymentMethod::Method("paypal".to_string())]
837}
838
839#[cfg(feature = "fakewallet")]
840fn default_fake_wallet_supported_units() -> Vec<CurrencyUnit> {
841 vec![CurrencyUnit::Sat]
842}
843
844#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
845pub struct GrpcProcessor {
846 #[serde(default)]
847 pub supported_units: Vec<CurrencyUnit>,
848 #[serde(default = "default_grpc_addr")]
849 pub addr: String,
850 #[serde(default = "default_grpc_port")]
851 pub port: u16,
852 #[serde(default)]
853 pub tls_dir: Option<PathBuf>,
854}
855
856impl Default for GrpcProcessor {
857 fn default() -> Self {
858 Self {
859 supported_units: Vec::new(),
860 addr: default_grpc_addr(),
861 port: default_grpc_port(),
862 tls_dir: None,
863 }
864 }
865}
866
867fn default_grpc_addr() -> String {
868 "127.0.0.1".to_string()
869}
870
871fn default_grpc_port() -> u16 {
872 50051
873}
874
875#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
876#[serde(rename_all = "lowercase")]
877pub enum DatabaseEngine {
878 #[default]
879 Sqlite,
880 Postgres,
881}
882
883impl std::str::FromStr for DatabaseEngine {
884 type Err = String;
885
886 fn from_str(s: &str) -> Result<Self, Self::Err> {
887 match s.to_lowercase().as_str() {
888 "sqlite" => Ok(DatabaseEngine::Sqlite),
889 "postgres" => Ok(DatabaseEngine::Postgres),
890 _ => Err(format!("Unknown database engine: {s}")),
891 }
892 }
893}
894
895#[derive(Debug, Clone, Serialize, Deserialize, Default)]
896pub struct Database {
897 pub engine: DatabaseEngine,
898 pub postgres: Option<PostgresConfig>,
899}
900
901#[derive(Debug, Clone, Serialize, Deserialize, Default)]
902pub struct AuthDatabase {
903 pub postgres: Option<PostgresAuthConfig>,
904}
905
906#[derive(Debug, Clone, Serialize, Deserialize)]
907pub struct PostgresAuthConfig {
908 pub url: String,
909 pub tls_mode: Option<String>,
910 pub max_connections: Option<usize>,
911 pub connection_timeout_seconds: Option<u64>,
912}
913
914impl Default for PostgresAuthConfig {
915 fn default() -> Self {
916 Self {
917 url: String::new(),
918 tls_mode: Some("disable".to_string()),
919 max_connections: Some(20),
920 connection_timeout_seconds: Some(10),
921 }
922 }
923}
924
925#[derive(Debug, Clone, Serialize, Deserialize)]
926pub struct PostgresConfig {
927 pub url: String,
928 pub tls_mode: Option<String>,
929 pub max_connections: Option<usize>,
930 pub connection_timeout_seconds: Option<u64>,
931}
932
933impl Default for PostgresConfig {
934 fn default() -> Self {
935 Self {
936 url: String::new(),
937 tls_mode: Some("disable".to_string()),
938 max_connections: Some(20),
939 connection_timeout_seconds: Some(10),
940 }
941 }
942}
943
944#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
945#[serde(rename_all = "lowercase")]
946pub enum AuthType {
947 Clear,
948 Blind,
949 #[default]
950 None,
951}
952
953impl std::str::FromStr for AuthType {
954 type Err = String;
955
956 fn from_str(s: &str) -> Result<Self, Self::Err> {
957 match s.to_lowercase().as_str() {
958 "clear" => Ok(AuthType::Clear),
959 "blind" => Ok(AuthType::Blind),
960 "none" => Ok(AuthType::None),
961 _ => Err(format!("Unknown auth type: {s}")),
962 }
963 }
964}
965
966#[derive(Debug, Clone, Default, Serialize, Deserialize)]
967pub struct Auth {
968 #[serde(default)]
969 pub auth_enabled: bool,
970 pub openid_discovery: String,
971 pub openid_client_id: String,
972 pub mint_max_bat: u64,
973 #[serde(default = "default_blind")]
974 pub mint: AuthType,
975 #[serde(default)]
976 pub get_mint_quote: AuthType,
977 #[serde(default)]
978 pub check_mint_quote: AuthType,
979 #[serde(default)]
980 pub melt: AuthType,
981 #[serde(default)]
982 pub get_melt_quote: AuthType,
983 #[serde(default)]
984 pub check_melt_quote: AuthType,
985 #[serde(default = "default_blind")]
986 pub swap: AuthType,
987 #[serde(default = "default_blind")]
988 pub restore: AuthType,
989 #[serde(default)]
990 pub check_proof_state: AuthType,
991 #[serde(default = "default_blind")]
993 pub websocket_auth: AuthType,
994}
995
996fn default_blind() -> AuthType {
997 AuthType::Blind
998}
999
1000#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1002pub struct Settings {
1003 pub info: Info,
1004 pub mint_info: MintInfo,
1005 #[serde(default, deserialize_with = "deserialize_ln")]
1006 pub ln: Vec<Ln>,
1007 pub onchain: Option<Onchain>,
1008 #[serde(default)]
1010 pub limits: Limits,
1011 #[cfg(feature = "cln")]
1012 pub cln: Option<Cln>,
1013 #[cfg(feature = "lnbits")]
1014 pub lnbits: Option<LNbits>,
1015 #[cfg(feature = "lnd")]
1016 pub lnd: Option<Lnd>,
1017 #[cfg(feature = "ldk-node")]
1018 pub ldk_node: Option<LdkNode>,
1019 #[cfg(feature = "fakewallet")]
1020 pub fake_wallet: Option<FakeWallet>,
1021 pub grpc_processor: Option<GrpcProcessor>,
1022 #[cfg(feature = "bdk")]
1023 pub bdk: Option<Bdk>,
1024 pub database: Database,
1025 pub auth_database: Option<AuthDatabase>,
1026 #[cfg(feature = "management-rpc")]
1027 pub mint_management_rpc: Option<MintManagementRpc>,
1028 pub auth: Option<Auth>,
1029 #[cfg(feature = "prometheus")]
1030 #[serde(default, skip_serializing_if = "Option::is_none")]
1031 pub prometheus: Option<Prometheus>,
1032}
1033
1034#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1035#[cfg(feature = "prometheus")]
1036pub struct Prometheus {
1037 pub enabled: bool,
1038 pub address: Option<String>,
1039 pub port: Option<u16>,
1040}
1041
1042#[derive(Debug, Clone, Serialize, Deserialize)]
1044pub struct Limits {
1045 #[serde(default = "default_max_inputs")]
1047 pub max_inputs: usize,
1048 #[serde(default = "default_max_outputs")]
1050 pub max_outputs: usize,
1051}
1052
1053impl Default for Limits {
1054 fn default() -> Self {
1055 Self {
1056 max_inputs: 1000,
1057 max_outputs: 1000,
1058 }
1059 }
1060}
1061
1062fn default_max_inputs() -> usize {
1063 1000
1064}
1065
1066fn default_max_outputs() -> usize {
1067 1000
1068}
1069
1070#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1071pub struct MintInfo {
1072 pub name: String,
1074 pub pubkey: Option<PublicKey>,
1076 pub description: String,
1078 pub description_long: Option<String>,
1080 pub icon_url: Option<String>,
1082 pub motd: Option<String>,
1084 pub contact_nostr_public_key: Option<String>,
1086 pub contact_email: Option<String>,
1088 pub tos_url: Option<String>,
1090}
1091
1092#[cfg(feature = "management-rpc")]
1093#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1094pub struct MintManagementRpc {
1095 pub enabled: bool,
1098 pub address: Option<String>,
1099 pub port: Option<u16>,
1100 pub tls_dir_path: Option<PathBuf>,
1101}
1102
1103impl Settings {
1104 pub fn validate_backend_pairing(&self) -> Result<(), String> {
1106 #[cfg(feature = "fakewallet")]
1107 self.validate_fake_wallet_backend_pairing()?;
1108
1109 Ok(())
1110 }
1111
1112 #[cfg(feature = "fakewallet")]
1113 fn validate_fake_wallet_backend_pairing(&self) -> Result<(), String> {
1114 let onchain_backend = self
1115 .onchain
1116 .as_ref()
1117 .map(|onchain| &onchain.onchain_backend)
1118 .unwrap_or(&OnchainBackend::None);
1119
1120 let has_fake_wallet_ln_backend = self
1121 .ln
1122 .iter()
1123 .any(|ln| ln.ln_backend == LnBackend::FakeWallet);
1124 let has_real_ln_backend = self
1125 .ln
1126 .iter()
1127 .any(|ln| !matches!(ln.ln_backend, LnBackend::None | LnBackend::FakeWallet));
1128
1129 if has_fake_wallet_ln_backend && has_real_ln_backend {
1131 return Err(
1132 "ln_backend = \"fakewallet\" cannot be combined with a real \
1133 Lightning backend; use only fakewallet backends or only real backends"
1134 .to_string(),
1135 );
1136 }
1137
1138 match onchain_backend {
1139 #[cfg(feature = "bdk")]
1140 OnchainBackend::Bdk if has_fake_wallet_ln_backend => {
1141 return Err("ln_backend = \"fakewallet\" cannot be combined with \
1142 onchain_backend = \"bdk\"; use onchain_backend = \
1143 \"fakewallet\" or \"none\""
1144 .to_string());
1145 }
1146 OnchainBackend::FakeWallet if has_real_ln_backend => {
1147 return Err("onchain_backend = \"fakewallet\" cannot be combined with \
1148 a real Lightning backend; use ln_backend = \"fakewallet\" \
1149 or \"none\""
1150 .to_string());
1151 }
1152 _ => {}
1153 }
1154
1155 Ok(())
1156 }
1157
1158 #[must_use]
1159 pub fn new<P>(config_file_name: Option<P>) -> Self
1160 where
1161 P: Into<PathBuf>,
1162 {
1163 let default_settings = Self::default();
1164 let from_file = Self::new_from_default(&default_settings, config_file_name);
1166 match from_file {
1167 Ok(f) => f,
1168 Err(e) => {
1169 tracing::error!(
1170 "Error reading config file, falling back to defaults. Error: {e:?}"
1171 );
1172 default_settings
1173 }
1174 }
1175 }
1176
1177 pub fn try_new<P>(config_file_name: Option<P>) -> Result<Self, ConfigError>
1178 where
1179 P: Into<PathBuf>,
1180 {
1181 Self::new_from_default(&Self::default(), config_file_name)
1182 }
1183
1184 fn new_from_default<P>(
1185 default: &Settings,
1186 config_file_name: Option<P>,
1187 ) -> Result<Self, ConfigError>
1188 where
1189 P: Into<PathBuf>,
1190 {
1191 let mut default_config_file_name = home::home_dir()
1192 .ok_or(ConfigError::NotFound("Config Path".to_string()))?
1193 .join("cashu-rs-mint");
1194
1195 default_config_file_name.push("config.toml");
1196 let config: String = match config_file_name {
1197 Some(value) => value.into().to_string_lossy().to_string(),
1198 None => default_config_file_name.to_string_lossy().to_string(),
1199 };
1200 let builder = Config::builder();
1201 let config: Config = builder
1202 .add_source(Config::try_from(default)?)
1204 .add_source(File::with_name(&config))
1206 .build()?;
1207 let settings: Settings = config.try_deserialize()?;
1208
1209 Ok(settings)
1210 }
1211}
1212
1213#[cfg(test)]
1214mod tests {
1215
1216 use super::*;
1217
1218 fn config_env_lock() -> std::sync::MutexGuard<'static, ()> {
1219 static CONFIG_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1220
1221 CONFIG_ENV_LOCK
1222 .lock()
1223 .expect("config env test lock should not be poisoned")
1224 }
1225
1226 #[cfg(feature = "bdk")]
1227 fn clear_bdk_env_vars() {
1228 std::env::remove_var(crate::env_vars::BDK_MNEMONIC_ENV_VAR);
1229 std::env::remove_var(crate::env_vars::BDK_NETWORK_ENV_VAR);
1230 std::env::remove_var(crate::env_vars::BDK_MIN_SEND_AMOUNT_SAT_ENV_VAR);
1231 std::env::remove_var(crate::env_vars::BDK_TARGET_BLOCK_TIME_SECS_ENV_VAR);
1232 std::env::remove_var(crate::env_vars::BDK_FEE_OPTIONS_ENV_VAR);
1233 std::env::remove_var(crate::env_vars::ENV_ONCHAIN_BACKEND);
1234 }
1235
1236 #[test]
1237 fn test_info_debug_impl() {
1238 let info = Info {
1240 url: "http://example.com".to_string(),
1241 listen_host: "127.0.0.1".to_string(),
1242 listen_port: 8080,
1243 mnemonic: Some("test secret mnemonic phrase".to_string()),
1244 input_fee_ppk: Some(100),
1245 ..Default::default()
1246 };
1247
1248 let debug_output = format!("{info:?}");
1250
1251 assert!(debug_output.contains("url: \"http://example.com\""));
1253 assert!(debug_output.contains("listen_host: \"127.0.0.1\""));
1254 assert!(debug_output.contains("listen_port: 8080"));
1255
1256 assert!(!debug_output.contains("test secret mnemonic phrase"));
1258 assert!(debug_output.contains("<hashed: "));
1259
1260 assert!(debug_output.contains("input_fee_ppk: Some(100)"));
1261 }
1262
1263 #[test]
1264 fn test_info_debug_with_empty_mnemonic() {
1265 let info = Info {
1267 url: "http://example.com".to_string(),
1268 listen_host: "127.0.0.1".to_string(),
1269 listen_port: 8080,
1270 mnemonic: Some("".to_string()), ..Default::default()
1272 };
1273
1274 let debug_output = format!("{:?}", info);
1276
1277 assert!(debug_output.contains("<hashed: "));
1279 }
1280
1281 #[cfg(feature = "bdk")]
1282 #[test]
1283 fn test_bdk_default_min_send_amount_sat() {
1284 assert_eq!(Bdk::default().min_send_amount_sat, 546);
1285 }
1286
1287 #[cfg(feature = "bdk")]
1288 #[test]
1289 fn test_bdk_config_min_send_amount_sat_override() {
1290 use std::{env, fs};
1291
1292 let temp_dir = env::temp_dir().join("cdk_test_bdk_min_send_config");
1293 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1294 let config_path = temp_dir.join("config.toml");
1295
1296 let config_content = r#"
1297[bdk]
1298min_send_amount_sat = 1200
1299"#;
1300 fs::write(&config_path, config_content).expect("Failed to write config file");
1301
1302 let settings = Settings::new(Some(&config_path));
1303
1304 assert_eq!(
1305 settings
1306 .bdk
1307 .as_ref()
1308 .expect("bdk config should be present")
1309 .min_send_amount_sat,
1310 1200
1311 );
1312
1313 let _ = fs::remove_dir_all(&temp_dir);
1314 }
1315
1316 #[cfg(feature = "bdk")]
1317 #[test]
1318 fn test_bdk_env_min_send_amount_sat_override() {
1319 let _guard = config_env_lock();
1320 clear_bdk_env_vars();
1321 std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1322 std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1323 std::env::set_var(crate::env_vars::BDK_MIN_SEND_AMOUNT_SAT_ENV_VAR, "777");
1324
1325 let mut settings = Settings::default();
1326 settings.from_env().expect("Failed to apply env vars");
1327
1328 assert_eq!(
1329 settings
1330 .bdk
1331 .as_ref()
1332 .expect("bdk config should be present")
1333 .min_send_amount_sat,
1334 777
1335 );
1336
1337 clear_bdk_env_vars();
1338 }
1339
1340 #[cfg(feature = "bdk")]
1341 #[test]
1342 fn test_bdk_default_fee_options_immediate_only() {
1343 assert_eq!(
1344 Bdk::default().batch_config.fee_options,
1345 vec!["immediate".to_string()]
1346 );
1347 }
1348
1349 #[cfg(feature = "bdk")]
1350 #[test]
1351 fn test_bdk_default_batch_deadlines_derive_from_target_block_time() {
1352 let batch_config: cdk_bdk::BatchConfig = Bdk::default().batch_config.into();
1353
1354 assert_eq!(
1355 batch_config.target_block_time,
1356 std::time::Duration::from_secs(600)
1357 );
1358 assert_eq!(
1359 batch_config.standard_deadline,
1360 std::time::Duration::from_secs(3600)
1361 );
1362 assert_eq!(
1363 batch_config.economy_deadline,
1364 std::time::Duration::from_secs(86_400)
1365 );
1366 assert_eq!(
1367 batch_config.max_intent_age,
1368 Some(std::time::Duration::from_secs(86_430))
1369 );
1370 }
1371
1372 #[cfg(feature = "bdk")]
1373 #[test]
1374 fn test_bdk_config_fee_options_override() {
1375 use std::{env, fs};
1376
1377 let temp_dir = env::temp_dir().join("cdk_test_bdk_fee_options_config");
1378 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1379 let config_path = temp_dir.join("config.toml");
1380
1381 let config_content = r#"
1382[bdk.batch_config]
1383fee_options = ["immediate", "economy"]
1384"#;
1385 fs::write(&config_path, config_content).expect("Failed to write config file");
1386
1387 let settings = Settings::new(Some(&config_path));
1388
1389 assert_eq!(
1390 settings
1391 .bdk
1392 .as_ref()
1393 .expect("bdk config should be present")
1394 .batch_config
1395 .fee_options,
1396 vec!["immediate".to_string(), "economy".to_string()]
1397 );
1398
1399 let _ = fs::remove_dir_all(&temp_dir);
1400 }
1401
1402 #[cfg(feature = "bdk")]
1403 #[test]
1404 fn test_bdk_config_target_block_time_derives_deadlines() {
1405 use std::{env, fs};
1406
1407 let temp_dir = env::temp_dir().join("cdk_test_bdk_target_block_time_config");
1408 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1409 let config_path = temp_dir.join("config.toml");
1410
1411 let config_content = r#"
1412[bdk.batch_config]
1413target_block_time_secs = 300
1414"#;
1415 fs::write(&config_path, config_content).expect("Failed to write config file");
1416
1417 let settings = Settings::new(Some(&config_path));
1418 let batch_config: cdk_bdk::BatchConfig = settings
1419 .bdk
1420 .as_ref()
1421 .expect("bdk config should be present")
1422 .batch_config
1423 .clone()
1424 .into();
1425
1426 assert_eq!(
1427 batch_config.target_block_time,
1428 std::time::Duration::from_secs(300)
1429 );
1430 assert_eq!(
1431 batch_config.standard_deadline,
1432 std::time::Duration::from_secs(1800)
1433 );
1434 assert_eq!(
1435 batch_config.economy_deadline,
1436 std::time::Duration::from_secs(43_200)
1437 );
1438 assert_eq!(
1439 batch_config.max_intent_age,
1440 Some(std::time::Duration::from_secs(43_230))
1441 );
1442
1443 let _ = fs::remove_dir_all(&temp_dir);
1444 }
1445
1446 #[cfg(feature = "bdk")]
1447 #[test]
1448 fn test_bdk_env_fee_options_override() {
1449 let _guard = config_env_lock();
1450 clear_bdk_env_vars();
1451 std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1452 std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1453 std::env::set_var(
1454 crate::env_vars::BDK_FEE_OPTIONS_ENV_VAR,
1455 "immediate,standard,economy",
1456 );
1457
1458 let mut settings = Settings::default();
1459 settings.from_env().expect("Failed to apply env vars");
1460
1461 assert_eq!(
1462 settings
1463 .bdk
1464 .as_ref()
1465 .expect("bdk config should be present")
1466 .batch_config
1467 .fee_options,
1468 vec![
1469 "immediate".to_string(),
1470 "standard".to_string(),
1471 "economy".to_string()
1472 ]
1473 );
1474
1475 clear_bdk_env_vars();
1476 }
1477
1478 #[cfg(feature = "bdk")]
1479 #[test]
1480 fn test_bdk_env_target_block_time_override() {
1481 let _guard = config_env_lock();
1482 clear_bdk_env_vars();
1483 std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1484 std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1485 std::env::set_var(crate::env_vars::BDK_TARGET_BLOCK_TIME_SECS_ENV_VAR, "120");
1486
1487 let mut settings = Settings::default();
1488 settings.from_env().expect("Failed to apply env vars");
1489 let batch_config: cdk_bdk::BatchConfig = settings
1490 .bdk
1491 .as_ref()
1492 .expect("bdk config should be present")
1493 .batch_config
1494 .clone()
1495 .into();
1496
1497 assert_eq!(
1498 batch_config.target_block_time,
1499 std::time::Duration::from_secs(120)
1500 );
1501 assert_eq!(
1502 batch_config.standard_deadline,
1503 std::time::Duration::from_secs(720)
1504 );
1505 assert_eq!(
1506 batch_config.economy_deadline,
1507 std::time::Duration::from_secs(17_280)
1508 );
1509 assert_eq!(
1510 batch_config.max_intent_age,
1511 Some(std::time::Duration::from_secs(17_310))
1512 );
1513
1514 clear_bdk_env_vars();
1515 }
1516
1517 #[cfg(feature = "bdk")]
1518 #[test]
1519 fn test_bdk_invalid_fee_options_rejected() {
1520 for fee_options in [
1521 Vec::new(),
1522 vec!["immediate".to_string(), "immediate".to_string()],
1523 vec!["urgent".to_string()],
1524 vec![
1525 "immediate".to_string(),
1526 "standard".to_string(),
1527 "economy".to_string(),
1528 "immediate".to_string(),
1529 ],
1530 ] {
1531 let bdk = Bdk {
1532 batch_config: BatchConfig {
1533 fee_options,
1534 ..BatchConfig::default()
1535 },
1536 ..Default::default()
1537 };
1538
1539 let err = bdk.validate().expect_err("invalid fee options should fail");
1540
1541 assert!(err.contains("fee_options"));
1542 }
1543 }
1544
1545 #[cfg(feature = "bdk")]
1546 #[test]
1547 fn test_bdk_target_block_time_zero_rejected() {
1548 let bdk = Bdk {
1549 batch_config: BatchConfig {
1550 target_block_time_secs: 0,
1551 ..BatchConfig::default()
1552 },
1553 ..Default::default()
1554 };
1555
1556 let err = bdk
1557 .validate()
1558 .expect_err("zero target block time should fail");
1559
1560 assert!(err.contains("target_block_time_secs"));
1561 }
1562
1563 #[cfg(feature = "bdk")]
1564 #[test]
1565 fn test_bdk_min_send_amount_sat_zero_rejected() {
1566 let bdk = Bdk {
1567 min_send_amount_sat: 0,
1568 ..Default::default()
1569 };
1570
1571 let err = bdk.validate().expect_err("zero send minimum should fail");
1572
1573 assert!(err.contains("min_send_amount_sat"));
1574 }
1575
1576 #[cfg(all(feature = "fakewallet", feature = "bdk"))]
1577 #[test]
1578 fn test_fakewallet_ln_with_bdk_onchain_rejected() {
1579 let settings = Settings {
1580 ln: vec![Ln {
1581 ln_backend: LnBackend::FakeWallet,
1582 ..Default::default()
1583 }],
1584 onchain: Some(Onchain {
1585 onchain_backend: OnchainBackend::Bdk,
1586 ..Default::default()
1587 }),
1588 ..Default::default()
1589 };
1590
1591 let err = settings
1592 .validate_backend_pairing()
1593 .expect_err("fake LN with BDK onchain should fail");
1594
1595 assert!(err.contains("fakewallet"));
1596 assert!(err.contains("bdk"));
1597 }
1598
1599 #[cfg(all(feature = "fakewallet", feature = "cln"))]
1600 #[test]
1601 fn test_real_ln_with_fakewallet_onchain_rejected() {
1602 let settings = Settings {
1603 ln: vec![Ln {
1604 ln_backend: LnBackend::Cln,
1605 ..Default::default()
1606 }],
1607 onchain: Some(Onchain {
1608 onchain_backend: OnchainBackend::FakeWallet,
1609 ..Default::default()
1610 }),
1611 ..Default::default()
1612 };
1613
1614 let err = settings
1615 .validate_backend_pairing()
1616 .expect_err("real LN with fake onchain should fail");
1617
1618 assert!(err.contains("fakewallet"));
1619 assert!(err.contains("real Lightning"));
1620 }
1621
1622 #[cfg(feature = "fakewallet")]
1623 #[test]
1624 fn test_fakewallet_ln_with_fakewallet_onchain_accepted() {
1625 let settings = Settings {
1626 ln: vec![Ln {
1627 ln_backend: LnBackend::FakeWallet,
1628 ..Default::default()
1629 }],
1630 onchain: Some(Onchain {
1631 onchain_backend: OnchainBackend::FakeWallet,
1632 ..Default::default()
1633 }),
1634 ..Default::default()
1635 };
1636
1637 settings
1638 .validate_backend_pairing()
1639 .expect("fake-only backend pairing should pass");
1640 }
1641
1642 #[cfg(feature = "fakewallet")]
1643 #[test]
1644 fn test_fakewallet_ln_with_no_onchain_accepted() {
1645 let settings = Settings {
1646 ln: vec![Ln {
1647 ln_backend: LnBackend::FakeWallet,
1648 ..Default::default()
1649 }],
1650 onchain: Some(Onchain {
1651 onchain_backend: OnchainBackend::None,
1652 ..Default::default()
1653 }),
1654 ..Default::default()
1655 };
1656
1657 settings
1658 .validate_backend_pairing()
1659 .expect("fake LN without onchain should pass");
1660 }
1661
1662 #[cfg(feature = "fakewallet")]
1663 #[test]
1664 fn test_no_ln_with_fakewallet_onchain_accepted() {
1665 let settings = Settings {
1666 ln: vec![Ln {
1667 ln_backend: LnBackend::None,
1668 ..Default::default()
1669 }],
1670 onchain: Some(Onchain {
1671 onchain_backend: OnchainBackend::FakeWallet,
1672 ..Default::default()
1673 }),
1674 ..Default::default()
1675 };
1676
1677 settings
1678 .validate_backend_pairing()
1679 .expect("fake onchain-only backend pairing should pass");
1680 }
1681
1682 #[cfg(all(feature = "fakewallet", feature = "cln"))]
1683 #[test]
1684 fn test_fakewallet_ln_with_real_ln_rejected() {
1685 let settings = Settings {
1686 ln: vec![
1687 Ln {
1688 ln_backend: LnBackend::FakeWallet,
1689 ..Default::default()
1690 },
1691 Ln {
1692 ln_backend: LnBackend::Cln,
1693 ..Default::default()
1694 },
1695 ],
1696 ..Default::default()
1697 };
1698
1699 let err = settings
1700 .validate_backend_pairing()
1701 .expect_err("fake LN combined with real LN should fail");
1702
1703 assert!(err.contains("fakewallet"));
1704 assert!(err.contains("real Lightning"));
1705 }
1706
1707 #[cfg(feature = "fakewallet")]
1708 #[test]
1709 fn test_fakewallet_custom_payment_method_unit_matching() {
1710 let global = FakeWalletCustomPaymentMethod::Method("paypal".to_string());
1711 let usd_only = FakeWalletCustomPaymentMethod::MethodForUnit {
1712 method: "venmo".to_string(),
1713 unit: CurrencyUnit::Usd,
1714 };
1715
1716 assert!(global.applies_to_unit(&CurrencyUnit::Sat));
1717 assert!(global.applies_to_unit(&CurrencyUnit::Usd));
1718 assert!(!usd_only.applies_to_unit(&CurrencyUnit::Sat));
1719 assert!(usd_only.applies_to_unit(&CurrencyUnit::Usd));
1720 }
1721
1722 #[test]
1723 fn test_info_debug_with_special_chars() {
1724 let info = Info {
1726 url: "http://example.com".to_string(),
1727 listen_host: "127.0.0.1".to_string(),
1728 listen_port: 8080,
1729 mnemonic: Some("特殊字符 !@#$%^&*()".to_string()), ..Default::default()
1731 };
1732
1733 let debug_output = format!("{:?}", info);
1735
1736 assert!(!debug_output.contains("特殊字符 !@#$%^&*()"));
1738 assert!(debug_output.contains("<hashed: "));
1739 }
1740
1741 #[cfg(feature = "fakewallet")]
1742 #[test]
1743 fn test_multi_backend_config_parses() {
1744 use std::{env, fs};
1745
1746 let temp_dir = env::temp_dir().join("cdk_test_multi_backend_config");
1747 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1748 let config_path = temp_dir.join("config.toml");
1749
1750 let config_content = r#"
1751[[ln]]
1752ln_backend = "fakewallet"
1753unit = "sat"
1754min_mint = 1
1755max_mint = 500000
1756min_melt = 1
1757max_melt = 500000
1758
1759[[ln]]
1760ln_backend = "fakewallet"
1761unit = "eur"
1762min_mint = 1
1763max_mint = 1000
1764min_melt = 1
1765max_melt = 1000
1766"#;
1767 fs::write(&config_path, config_content).expect("Failed to write config file");
1768
1769 let settings = Settings::new(Some(&config_path));
1770
1771 assert_eq!(settings.ln.len(), 2);
1772
1773 assert_eq!(settings.ln[0].ln_backend, LnBackend::FakeWallet);
1774 assert_eq!(settings.ln[0].unit, CurrencyUnit::Sat);
1775 let max_mint_0: u64 = settings.ln[0].max_mint.into();
1776 assert_eq!(max_mint_0, 500_000);
1777
1778 assert_eq!(settings.ln[1].ln_backend, LnBackend::FakeWallet);
1779 assert_eq!(settings.ln[1].unit, CurrencyUnit::Eur);
1780 let max_mint_1: u64 = settings.ln[1].max_mint.into();
1781 assert_eq!(max_mint_1, 1_000);
1782
1783 let _ = fs::remove_dir_all(&temp_dir);
1784 }
1785
1786 #[cfg(feature = "fakewallet")]
1787 #[test]
1788 fn test_legacy_ln_block_parses() {
1789 use std::{env, fs};
1790
1791 let temp_dir = env::temp_dir().join("cdk_test_legacy_ln_block");
1792 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1793 let config_path = temp_dir.join("config.toml");
1794
1795 let config_content = r#"
1796[ln]
1797ln_backend = "fakewallet"
1798min_mint = 1
1799max_mint = 500000
1800min_melt = 1
1801max_melt = 500000
1802"#;
1803 fs::write(&config_path, config_content).expect("Failed to write config file");
1804
1805 let settings = Settings::new(Some(&config_path));
1806
1807 assert_eq!(settings.ln.len(), 1);
1808 assert_eq!(settings.ln[0].ln_backend, LnBackend::FakeWallet);
1809 assert_eq!(settings.ln[0].unit, CurrencyUnit::Sat);
1810
1811 let _ = fs::remove_dir_all(&temp_dir);
1812 }
1813
1814 #[cfg(feature = "fakewallet")]
1815 #[test]
1816 fn test_fakewallet_config_without_supported_units_parses() {
1817 use std::{env, fs};
1818
1819 let temp_dir = env::temp_dir().join("cdk_test_fakewallet_without_supported_units");
1820 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1821 let config_path = temp_dir.join("config.toml");
1822
1823 let config_content = r#"
1824[[ln]]
1825ln_backend = "fakewallet"
1826unit = "sat"
1827min_mint = 100
1828max_mint = 1000000
1829min_melt = 100
1830max_melt = 1000000
1831
1832[fake_wallet]
1833fee_percent = 0.02
1834reserve_fee_min = 1
1835min_delay_time = 1
1836max_delay_time = 3
1837"#;
1838 fs::write(&config_path, config_content).expect("Failed to write config file");
1839
1840 let settings = Settings::try_new(Some(&config_path)).expect("config should parse");
1841
1842 assert_eq!(settings.ln.len(), 1);
1843 assert_eq!(settings.ln[0].unit, CurrencyUnit::Sat);
1844 assert_eq!(
1845 settings
1846 .fake_wallet
1847 .expect("fake wallet section should parse")
1848 .supported_units,
1849 vec![CurrencyUnit::Sat]
1850 );
1851
1852 let _ = fs::remove_dir_all(&temp_dir);
1853 }
1854
1855 #[test]
1860 fn test_env_var_only_config_all_backends() {
1861 let _guard = config_env_lock();
1862
1863 #[cfg(feature = "lnd")]
1865 test_lnd_env_config();
1866
1867 #[cfg(feature = "cln")]
1868 test_cln_env_config();
1869
1870 #[cfg(feature = "lnbits")]
1871 test_lnbits_env_config();
1872
1873 #[cfg(feature = "fakewallet")]
1874 test_fakewallet_env_config();
1875
1876 #[cfg(feature = "grpc-processor")]
1877 test_grpc_processor_env_config();
1878
1879 #[cfg(feature = "ldk-node")]
1880 test_ldk_node_env_config();
1881 }
1882
1883 #[cfg(all(feature = "prometheus", feature = "fakewallet"))]
1884 #[test]
1885 fn test_prometheus_toml_config_survives_env_overlay() {
1886 use std::{env, fs};
1887
1888 let _guard = config_env_lock();
1889
1890 env::remove_var(crate::env_vars::ENV_LN_BACKEND);
1891 env::remove_var(crate::env_vars::ENV_PROMETHEUS_ENABLED);
1892 env::remove_var(crate::env_vars::ENV_PROMETHEUS_ADDRESS);
1893 env::remove_var(crate::env_vars::ENV_PROMETHEUS_PORT);
1894
1895 let temp_dir =
1896 env::temp_dir().join(format!("cdk_prometheus_config_{}", std::process::id()));
1897 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1898 let config_path = temp_dir.join("config.toml");
1899
1900 let config_content = r#"
1901[info]
1902url = "http://127.0.0.1:8085"
1903listen_host = "127.0.0.1"
1904listen_port = 8085
1905
1906[ln]
1907ln_backend = "fakewallet"
1908min_mint = 1
1909max_mint = 500000
1910min_melt = 1
1911max_melt = 500000
1912
1913[prometheus]
1914enabled = true
1915address = "0.0.0.0"
1916port = 9090
1917"#;
1918 fs::write(&config_path, config_content).expect("Failed to write config file");
1919
1920 let mut settings = Settings::new(Some(&config_path));
1921 settings.from_env().expect("Failed to apply env vars");
1922
1923 let prometheus = settings
1924 .prometheus
1925 .as_ref()
1926 .expect("Prometheus config should be loaded from TOML");
1927 assert!(prometheus.enabled);
1928 assert_eq!(prometheus.address.as_deref(), Some("0.0.0.0"));
1929 assert_eq!(prometheus.port, Some(9090));
1930
1931 let _ = fs::remove_dir_all(&temp_dir);
1932 }
1933
1934 #[cfg(feature = "lnd")]
1935 fn test_lnd_env_config() {
1936 use std::path::PathBuf;
1937 use std::{env, fs};
1938
1939 let temp_dir = env::temp_dir().join("cdk_test_env_vars");
1941 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1942 let config_path = temp_dir.join("config.toml");
1943
1944 let config_content = r#"
1946[ln]
1947backend = "lnd"
1948min_mint = 1
1949max_mint = 500000
1950min_melt = 1
1951max_melt = 500000
1952"#;
1953 fs::write(&config_path, config_content).expect("Failed to write config file");
1954
1955 env::set_var(crate::env_vars::ENV_LN_BACKEND, "lnd");
1957 env::set_var(crate::env_vars::ENV_LND_ADDRESS, "https://localhost:10009");
1958 env::set_var(crate::env_vars::ENV_LND_CERT_FILE, "/tmp/test_tls.cert");
1959 env::set_var(
1960 crate::env_vars::ENV_LND_MACAROON_FILE,
1961 "/tmp/test_admin.macaroon",
1962 );
1963 env::set_var(crate::env_vars::ENV_LND_FEE_PERCENT, "0.01");
1964 env::set_var(crate::env_vars::ENV_LND_RESERVE_FEE_MIN, "4");
1965
1966 let mut settings = Settings::new(Some(&config_path));
1968 settings.from_env().expect("Failed to apply env vars");
1969
1970 assert!(settings.lnd.is_some());
1972 let lnd_config = settings.lnd.as_ref().unwrap();
1973 assert_eq!(lnd_config.address, "https://localhost:10009");
1974 assert_eq!(lnd_config.cert_file, PathBuf::from("/tmp/test_tls.cert"));
1975 assert_eq!(
1976 lnd_config.macaroon_file,
1977 PathBuf::from("/tmp/test_admin.macaroon")
1978 );
1979 assert_eq!(lnd_config.fee_percent, 0.01);
1980 let reserve_fee_u64: u64 = lnd_config.reserve_fee_min.into();
1981 assert_eq!(reserve_fee_u64, 4);
1982
1983 env::remove_var(crate::env_vars::ENV_LN_BACKEND);
1985 env::remove_var(crate::env_vars::ENV_LND_ADDRESS);
1986 env::remove_var(crate::env_vars::ENV_LND_CERT_FILE);
1987 env::remove_var(crate::env_vars::ENV_LND_MACAROON_FILE);
1988 env::remove_var(crate::env_vars::ENV_LND_FEE_PERCENT);
1989 env::remove_var(crate::env_vars::ENV_LND_RESERVE_FEE_MIN);
1990
1991 let _ = fs::remove_dir_all(&temp_dir);
1993 }
1994
1995 #[cfg(feature = "cln")]
1996 fn test_cln_env_config() {
1997 use std::path::PathBuf;
1998 use std::{env, fs};
1999
2000 let temp_dir = env::temp_dir().join("cdk_test_env_vars_cln");
2002 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2003 let config_path = temp_dir.join("config.toml");
2004
2005 let config_content = r#"
2007[ln]
2008backend = "cln"
2009min_mint = 1
2010max_mint = 500000
2011min_melt = 1
2012max_melt = 500000
2013"#;
2014 fs::write(&config_path, config_content).expect("Failed to write config file");
2015
2016 env::set_var(crate::env_vars::ENV_LN_BACKEND, "cln");
2018 env::set_var(crate::env_vars::ENV_CLN_RPC_PATH, "/tmp/lightning-rpc");
2019 env::set_var(crate::env_vars::ENV_CLN_BOLT12, "false");
2020 env::set_var(crate::env_vars::ENV_CLN_FEE_PERCENT, "0.01");
2021 env::set_var(crate::env_vars::ENV_CLN_RESERVE_FEE_MIN, "4");
2022
2023 let mut settings = Settings::new(Some(&config_path));
2025 settings.from_env().expect("Failed to apply env vars");
2026
2027 assert!(settings.cln.is_some());
2029 let cln_config = settings.cln.as_ref().unwrap();
2030 assert_eq!(cln_config.rpc_path, PathBuf::from("/tmp/lightning-rpc"));
2031 assert!(!cln_config.bolt12);
2032 assert_eq!(cln_config.fee_percent, 0.01);
2033 let reserve_fee_u64: u64 = cln_config.reserve_fee_min.into();
2034 assert_eq!(reserve_fee_u64, 4);
2035
2036 env::remove_var(crate::env_vars::ENV_LN_BACKEND);
2038 env::remove_var(crate::env_vars::ENV_CLN_RPC_PATH);
2039 env::remove_var(crate::env_vars::ENV_CLN_BOLT12);
2040 env::remove_var(crate::env_vars::ENV_CLN_FEE_PERCENT);
2041 env::remove_var(crate::env_vars::ENV_CLN_RESERVE_FEE_MIN);
2042
2043 let _ = fs::remove_dir_all(&temp_dir);
2045 }
2046
2047 #[cfg(feature = "lnbits")]
2048 fn test_lnbits_env_config() {
2049 use std::{env, fs};
2050
2051 let temp_dir = env::temp_dir().join("cdk_test_env_vars_lnbits");
2053 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2054 let config_path = temp_dir.join("config.toml");
2055
2056 let config_content = r#"
2058[ln]
2059backend = "lnbits"
2060min_mint = 1
2061max_mint = 500000
2062min_melt = 1
2063max_melt = 500000
2064"#;
2065 fs::write(&config_path, config_content).expect("Failed to write config file");
2066
2067 env::set_var(crate::env_vars::ENV_LN_BACKEND, "lnbits");
2069 env::set_var(crate::env_vars::ENV_LNBITS_ADMIN_API_KEY, "test_admin_key");
2070 env::set_var(
2071 crate::env_vars::ENV_LNBITS_INVOICE_API_KEY,
2072 "test_invoice_key",
2073 );
2074 env::set_var(
2075 crate::env_vars::ENV_LNBITS_API,
2076 "https://lnbits.example.com",
2077 );
2078 env::set_var(crate::env_vars::ENV_LNBITS_FEE_PERCENT, "0.02");
2079 env::set_var(crate::env_vars::ENV_LNBITS_RESERVE_FEE_MIN, "5");
2080
2081 let mut settings = Settings::new(Some(&config_path));
2083 settings.from_env().expect("Failed to apply env vars");
2084
2085 assert!(settings.lnbits.is_some());
2087 let lnbits_config = settings.lnbits.as_ref().unwrap();
2088 assert_eq!(lnbits_config.admin_api_key, "test_admin_key");
2089 assert_eq!(lnbits_config.invoice_api_key, "test_invoice_key");
2090 assert_eq!(lnbits_config.lnbits_api, "https://lnbits.example.com");
2091 assert_eq!(lnbits_config.fee_percent, 0.02);
2092 let reserve_fee_u64: u64 = lnbits_config.reserve_fee_min.into();
2093 assert_eq!(reserve_fee_u64, 5);
2094
2095 env::remove_var(crate::env_vars::ENV_LN_BACKEND);
2097 env::remove_var(crate::env_vars::ENV_LNBITS_ADMIN_API_KEY);
2098 env::remove_var(crate::env_vars::ENV_LNBITS_INVOICE_API_KEY);
2099 env::remove_var(crate::env_vars::ENV_LNBITS_API);
2100 env::remove_var(crate::env_vars::ENV_LNBITS_FEE_PERCENT);
2101 env::remove_var(crate::env_vars::ENV_LNBITS_RESERVE_FEE_MIN);
2102
2103 let _ = fs::remove_dir_all(&temp_dir);
2105 }
2106
2107 #[cfg(feature = "fakewallet")]
2108 fn test_fakewallet_env_config() {
2109 use std::{env, fs};
2110
2111 let temp_dir = env::temp_dir().join("cdk_test_env_vars_fakewallet");
2113 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2114 let config_path = temp_dir.join("config.toml");
2115
2116 let config_content = r#"
2118[ln]
2119backend = "fakewallet"
2120min_mint = 1
2121max_mint = 500000
2122min_melt = 1
2123max_melt = 500000
2124"#;
2125 fs::write(&config_path, config_content).expect("Failed to write config file");
2126
2127 env::set_var(crate::env_vars::ENV_LN_BACKEND, "fakewallet");
2129 env::set_var(crate::env_vars::ENV_FAKE_WALLET_SUPPORTED_UNITS, "sat,msat");
2130 env::set_var(crate::env_vars::ENV_FAKE_WALLET_FEE_PERCENT, "0.0");
2131 env::set_var(crate::env_vars::ENV_FAKE_WALLET_RESERVE_FEE_MIN, "0");
2132 env::set_var(
2133 crate::env_vars::ENV_FAKE_WALLET_CUSTOM_PAYMENT_METHODS,
2134 "venmo:msat,cashapp:sat,paypal",
2135 );
2136 env::set_var(crate::env_vars::ENV_FAKE_WALLET_MIN_DELAY, "0");
2137 env::set_var(crate::env_vars::ENV_FAKE_WALLET_MAX_DELAY, "5");
2138
2139 let mut settings = Settings::new(Some(&config_path));
2141 settings.from_env().expect("Failed to apply env vars");
2142
2143 assert!(settings.fake_wallet.is_some());
2145 let fakewallet_config = settings.fake_wallet.as_ref().unwrap();
2146 assert_eq!(fakewallet_config.fee_percent, 0.0);
2147 let reserve_fee_u64: u64 = fakewallet_config.reserve_fee_min.into();
2148 assert_eq!(reserve_fee_u64, 0);
2149 assert_eq!(
2150 fakewallet_config.custom_payment_methods,
2151 vec![
2152 FakeWalletCustomPaymentMethod::MethodForUnit {
2153 method: "venmo".to_string(),
2154 unit: CurrencyUnit::Msat,
2155 },
2156 FakeWalletCustomPaymentMethod::MethodForUnit {
2157 method: "cashapp".to_string(),
2158 unit: CurrencyUnit::Sat,
2159 },
2160 FakeWalletCustomPaymentMethod::Method("paypal".to_string()),
2161 ]
2162 );
2163 assert_eq!(fakewallet_config.min_delay_time, 0);
2164 assert_eq!(fakewallet_config.max_delay_time, 5);
2165 assert_eq!(
2166 settings
2167 .ln
2168 .iter()
2169 .map(|ln| ln.unit.clone())
2170 .collect::<Vec<_>>(),
2171 vec![CurrencyUnit::Sat, CurrencyUnit::Msat]
2172 );
2173
2174 env::remove_var(crate::env_vars::ENV_LN_BACKEND);
2176 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_SUPPORTED_UNITS);
2177 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_FEE_PERCENT);
2178 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_RESERVE_FEE_MIN);
2179 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_CUSTOM_PAYMENT_METHODS);
2180 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_MIN_DELAY);
2181 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_MAX_DELAY);
2182
2183 let _ = fs::remove_dir_all(&temp_dir);
2185 }
2186
2187 #[cfg(feature = "grpc-processor")]
2188 fn test_grpc_processor_env_config() {
2189 use std::{env, fs};
2190
2191 let temp_dir = env::temp_dir().join("cdk_test_env_vars_grpc");
2193 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2194 let config_path = temp_dir.join("config.toml");
2195
2196 let config_content = r#"
2198[ln]
2199backend = "grpcprocessor"
2200min_mint = 1
2201max_mint = 500000
2202min_melt = 1
2203max_melt = 500000
2204"#;
2205 fs::write(&config_path, config_content).expect("Failed to write config file");
2206
2207 env::set_var(crate::env_vars::ENV_LN_BACKEND, "grpcprocessor");
2209 env::set_var(
2210 crate::env_vars::ENV_GRPC_PROCESSOR_SUPPORTED_UNITS,
2211 "sat,msat",
2212 );
2213 env::set_var(crate::env_vars::ENV_GRPC_PROCESSOR_ADDRESS, "localhost");
2214 env::set_var(crate::env_vars::ENV_GRPC_PROCESSOR_PORT, "50051");
2215
2216 let mut settings = Settings::new(Some(&config_path));
2218 settings.from_env().expect("Failed to apply env vars");
2219
2220 assert!(settings.grpc_processor.is_some());
2222 let grpc_config = settings.grpc_processor.as_ref().unwrap();
2223 assert_eq!(grpc_config.addr, "localhost");
2224 assert_eq!(grpc_config.port, 50051);
2225
2226 env::remove_var(crate::env_vars::ENV_LN_BACKEND);
2228 env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_SUPPORTED_UNITS);
2229 env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_ADDRESS);
2230 env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_PORT);
2231
2232 let _ = fs::remove_dir_all(&temp_dir);
2234 }
2235
2236 #[cfg(feature = "ldk-node")]
2237 fn test_ldk_node_env_config() {
2238 use std::{env, fs};
2239
2240 let temp_dir = env::temp_dir().join("cdk_test_env_vars_ldk");
2242 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2243 let config_path = temp_dir.join("config.toml");
2244
2245 let config_content = r#"
2247[ln]
2248backend = "ldknode"
2249min_mint = 1
2250max_mint = 500000
2251min_melt = 1
2252max_melt = 500000
2253"#;
2254 fs::write(&config_path, config_content).expect("Failed to write config file");
2255
2256 env::set_var(crate::env_vars::ENV_LN_BACKEND, "ldknode");
2258 env::set_var(crate::env_vars::LDK_NODE_FEE_PERCENT_ENV_VAR, "0.01");
2259 env::set_var(crate::env_vars::LDK_NODE_RESERVE_FEE_MIN_ENV_VAR, "4");
2260 env::set_var(crate::env_vars::LDK_NODE_BITCOIN_NETWORK_ENV_VAR, "regtest");
2261 env::set_var(
2262 crate::env_vars::LDK_NODE_CHAIN_SOURCE_TYPE_ENV_VAR,
2263 "esplora",
2264 );
2265 env::set_var(
2266 crate::env_vars::LDK_NODE_ESPLORA_URL_ENV_VAR,
2267 "http://localhost:3000",
2268 );
2269 env::set_var(
2270 crate::env_vars::LDK_NODE_STORAGE_DIR_PATH_ENV_VAR,
2271 "/tmp/ldk",
2272 );
2273
2274 let mut settings = Settings::new(Some(&config_path));
2276 settings.from_env().expect("Failed to apply env vars");
2277
2278 assert!(settings.ldk_node.is_some());
2280 let ldk_config = settings.ldk_node.as_ref().unwrap();
2281 assert_eq!(ldk_config.fee_percent, 0.01);
2282 let reserve_fee_u64: u64 = ldk_config.reserve_fee_min.into();
2283 assert_eq!(reserve_fee_u64, 4);
2284 assert_eq!(ldk_config.bitcoin_network, Some("regtest".to_string()));
2285 assert_eq!(ldk_config.chain_source_type, Some("esplora".to_string()));
2286 assert_eq!(
2287 ldk_config.esplora_url,
2288 Some("http://localhost:3000".to_string())
2289 );
2290 assert_eq!(ldk_config.storage_dir_path, Some("/tmp/ldk".to_string()));
2291
2292 env::remove_var(crate::env_vars::ENV_LN_BACKEND);
2294 env::remove_var(crate::env_vars::LDK_NODE_FEE_PERCENT_ENV_VAR);
2295 env::remove_var(crate::env_vars::LDK_NODE_RESERVE_FEE_MIN_ENV_VAR);
2296 env::remove_var(crate::env_vars::LDK_NODE_BITCOIN_NETWORK_ENV_VAR);
2297 env::remove_var(crate::env_vars::LDK_NODE_CHAIN_SOURCE_TYPE_ENV_VAR);
2298 env::remove_var(crate::env_vars::LDK_NODE_ESPLORA_URL_ENV_VAR);
2299 env::remove_var(crate::env_vars::LDK_NODE_STORAGE_DIR_PATH_ENV_VAR);
2300
2301 let _ = fs::remove_dir_all(&temp_dir);
2303 }
2304}