Skip to main content

cdk_mintd/
config.rs

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    /// Log to stderr only
15    Stderr,
16    /// Log to file only
17    File,
18    /// Log to both stderr and file (default)
19    #[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    /// Where to output logs: stdout, file, or both
41    #[serde(default)]
42    pub output: LoggingOutput,
43    /// Log level for console output (when stdout or both)
44    pub console_level: Option<String>,
45    /// Log level for file output (when file or both)
46    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    /// Overrides mnemonic
55    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    /// Use keyset v2
61    pub use_keyset_v2: Option<bool>,
62
63    pub http_cache: cache::Config,
64
65    /// Logging configuration
66    #[serde(default)]
67    pub logging: LoggingConfig,
68
69    /// When this is set to true, the mint exposes a very simple info page at `/`
70    /// showing the mint name and description.
71    ///
72    /// This requires `mintd` was built with the `info-page` feature flag.
73    pub enable_info_page: Option<bool>,
74
75    /// Optional persisted quote TTL values (seconds) to initialize the database with
76    /// when RPC is disabled or on first-run when RPC is enabled.
77    /// If not provided, defaults are used.
78    #[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, // Default to port 8091 instead of 0
88            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        // Use a fallback approach that won't panic
105        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    /// How often the batch processor wakes up to check for ready intents
263    #[serde(default = "default_bdk_poll_interval_secs")]
264    pub poll_interval_secs: u64,
265    /// Maximum number of intents to include in a single batch
266    #[serde(default = "default_bdk_max_batch_size")]
267    pub max_batch_size: usize,
268    /// Average block interval used to derive default delayed tier deadlines.
269    #[serde(default = "default_bdk_target_block_time_secs")]
270    pub target_block_time_secs: u64,
271    /// Optional override for how long standard-tier intents wait before being eligible
272    #[serde(default)]
273    pub standard_deadline_secs: Option<u64>,
274    /// Optional override for how long economy-tier intents wait before being eligible
275    #[serde(default)]
276    pub economy_deadline_secs: Option<u64>,
277    /// Fee tiers exposed in melt quotes. Order determines fee_index values.
278    #[serde(default = "default_bdk_fee_options")]
279    pub fee_options: Vec<String>,
280    /// Quote-time fallback fee rate used when chain estimation fails, in sat/vB.
281    #[serde(default = "default_bdk_fee_fallback_sat_per_vb")]
282    pub fee_fallback_sat_per_vb: f64,
283    /// Fee-rate cache TTL, in seconds.
284    #[serde(default = "default_bdk_fee_cache_ttl_secs")]
285    pub fee_cache_ttl_secs: u64,
286    /// Maximum input count reserved for a quote estimate.
287    #[serde(default = "default_bdk_quote_max_input_count")]
288    pub quote_max_input_count: usize,
289    /// Fixed safety margin added to quote-time fee estimates, in sats.
290    #[serde(default = "default_bdk_quote_fixed_safety_sat")]
291    pub quote_fixed_safety_sat: u64,
292    /// Multiplicative safety margin applied after the raw quote fee estimate.
293    #[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    /// Fee percentage (e.g., 0.02 for 2%)
320    #[serde(default = "default_fee_percent")]
321    pub fee_percent: f32,
322    /// Minimum reserve fee
323    #[serde(default = "default_reserve_fee_min")]
324    pub reserve_fee_min: Amount,
325    /// Bitcoin network (mainnet, testnet, signet, regtest)
326    pub network: Option<String>,
327    /// Chain source type ("esplora" or "bitcoinrpc"; defaults to "bitcoinrpc")
328    pub chain_source_type: Option<String>,
329    /// Esplora URL (when chain_source_type = "esplora")
330    pub esplora_url: Option<String>,
331    /// Number of parallel Esplora requests during wallet sync.
332    ///
333    /// Public Esplora servers often rate-limit bursty clients, so the default
334    /// is conservative. Increase only when using a private or higher-limit
335    /// Esplora server.
336    #[serde(default = "default_bdk_esplora_parallel_requests")]
337    pub esplora_parallel_requests: usize,
338    /// Bitcoin RPC host (when chain_source_type = "bitcoinrpc")
339    pub bitcoind_rpc_host: Option<String>,
340    /// Bitcoin RPC port
341    pub bitcoind_rpc_port: Option<u16>,
342    /// Bitcoin RPC user
343    pub bitcoind_rpc_user: Option<String>,
344    /// Bitcoin RPC password
345    pub bitcoind_rpc_password: Option<String>,
346    /// BIP-39 mnemonic for the BDK wallet
347    pub mnemonic: Option<String>,
348    /// Batch processor configuration
349    #[serde(default)]
350    pub batch_config: BatchConfig,
351    /// Number of confirmations required for incoming payments.
352    ///
353    /// Must be >= 1. A value of 0 is rejected at startup because the
354    /// confirmation check still requires the transaction to have an on-chain
355    /// anchor (i.e. 0 would mean "confirmed in any block", not "accept
356    /// unconfirmed"). Use 1 for "accept any confirmation".
357    #[serde(default = "default_bdk_num_confs")]
358    pub num_confs: u32,
359    /// Minimum receive amount in sats
360    #[serde(default = "default_bdk_min_receive_amount_sat")]
361    pub min_receive_amount_sat: u64,
362    /// Minimum send amount in sats
363    #[serde(default = "default_bdk_min_send_amount_sat")]
364    pub min_send_amount_sat: u64,
365    /// Wallet sync interval in seconds
366    #[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    /// Validate BDK settings that must be rejected before the backend starts.
397    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    /// Fee percentage (e.g., 0.02 for 2%)
606    #[serde(default = "default_ldk_fee_percent")]
607    pub fee_percent: f32,
608    /// Minimum reserve fee
609    #[serde(default = "default_ldk_reserve_fee_min")]
610    pub reserve_fee_min: Amount,
611    /// Bitcoin network (mainnet, testnet, signet, regtest)
612    pub bitcoin_network: Option<String>,
613    /// Chain source type (esplora or bitcoinrpc)
614    pub chain_source_type: Option<String>,
615    /// Esplora URL (when chain_source_type = "esplora")
616    pub esplora_url: Option<String>,
617    /// Bitcoin RPC configuration (when chain_source_type = "bitcoinrpc")
618    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    /// Storage directory path
623    pub storage_dir_path: Option<String>,
624    /// Log directory path (logging stdout if omitted)
625    pub log_dir_path: Option<String>,
626    /// LDK node listening host
627    pub ldk_node_host: Option<String>,
628    /// LDK node listening port
629    pub ldk_node_port: Option<u16>,
630    /// LDK node announcement addresses
631    pub ldk_node_announce_addresses: Option<Vec<String>>,
632    /// Gossip source type (p2p or rgs)
633    pub gossip_source_type: Option<String>,
634    /// Rapid Gossip Sync URL (when gossip_source_type = "rgs")
635    pub rgs_url: Option<String>,
636    /// Webserver host (defaults to 127.0.0.1)
637    #[serde(default = "default_webserver_host")]
638    pub webserver_host: Option<String>,
639    /// Webserver port
640    #[serde(default = "default_webserver_port")]
641    pub webserver_port: Option<u16>,
642    /// LDK node mnemonic
643    /// If not set, LDK node will use its default seed storage mechanism
644    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    /// Currency unit (e.g. "sat", "usd")
728    pub unit: CurrencyUnit,
729    /// Input fee in parts per thousand
730    #[serde(default)]
731    pub input_fee_ppk: u64,
732    /// Keyset version: "v1" (Version00) or "v2" (Version01)
733    #[serde(default = "default_keyset_version")]
734    pub version: String,
735    /// If true, the keyset will be created with a past expiry (expired)
736    #[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    /// Custom method available for every supported fake wallet unit
750    Method(String),
751    /// Custom method available only for one unit
752    MethodForUnit {
753        /// Payment method name (e.g. "paypal", "venmo")
754        method: String,
755        /// Currency unit for this method
756        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    /// Additional keyset rotations to create during mint build
793    #[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// Helper functions to provide default values
813// Common fee defaults for all backends
814#[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    /// Enable WebSocket authentication support
992    #[serde(default = "default_blind")]
993    pub websocket_auth: AuthType,
994}
995
996fn default_blind() -> AuthType {
997    AuthType::Blind
998}
999
1000/// CDK settings, derived from `config.toml`
1001#[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    /// Transaction limits for DoS protection
1009    #[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/// Transaction limits configuration
1043#[derive(Debug, Clone, Serialize, Deserialize)]
1044pub struct Limits {
1045    /// Maximum number of inputs allowed per transaction (swap/melt)
1046    #[serde(default = "default_max_inputs")]
1047    pub max_inputs: usize,
1048    /// Maximum number of outputs allowed per transaction (mint/swap/melt)
1049    #[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    /// name of the mint and should be recognizable
1073    pub name: String,
1074    /// hex pubkey of the mint
1075    pub pubkey: Option<PublicKey>,
1076    /// short description of the mint
1077    pub description: String,
1078    /// long description
1079    pub description_long: Option<String>,
1080    /// url to the mint icon
1081    pub icon_url: Option<String>,
1082    /// message of the day that the wallet must display to the user
1083    pub motd: Option<String>,
1084    /// Nostr publickey
1085    pub contact_nostr_public_key: Option<String>,
1086    /// Contact email
1087    pub contact_email: Option<String>,
1088    /// URL to the terms of service
1089    pub tos_url: Option<String>,
1090}
1091
1092#[cfg(feature = "management-rpc")]
1093#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1094pub struct MintManagementRpc {
1095    /// When this is set to `true` the mint use the config file for the initial set up on first start.
1096    /// Changes to the `[mint_info]` after this **MUST** be made via the RPC changes to the config file or env vars will be ignored.
1097    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    /// Validate payment backend combinations after config and env overrides are applied.
1105    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        // A fake Lightning backend cannot be combined with a real one.
1130        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        // attempt to construct settings with file
1165        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            // use defaults
1203            .add_source(Config::try_from(default)?)
1204            // override with file contents
1205            .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        // Create a sample Info struct with test data
1239        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        // Convert the Info struct to a debug string
1249        let debug_output = format!("{info:?}");
1250
1251        // Verify the debug output contains expected fields
1252        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        // The mnemonic should be hashed, not displayed in plaintext
1257        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        // Test with an empty mnemonic to ensure it doesn't panic
1266        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()), // Empty mnemonic
1271            ..Default::default()
1272        };
1273
1274        // This should not panic
1275        let debug_output = format!("{:?}", info);
1276
1277        // The empty mnemonic should still be hashed
1278        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        // Test with a mnemonic containing special characters
1725        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()), // Special characters
1730            ..Default::default()
1731        };
1732
1733        // This should not panic
1734        let debug_output = format!("{:?}", info);
1735
1736        // The mnemonic with special chars should be hashed
1737        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 that configuration can be loaded purely from environment variables
1856    /// without requiring a config.toml file with backend sections.
1857    ///
1858    /// This test runs sequentially for all enabled backends to avoid env var interference.
1859    #[test]
1860    fn test_env_var_only_config_all_backends() {
1861        let _guard = config_env_lock();
1862
1863        // Run each backend test sequentially
1864        #[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        // Create a temporary directory for config file
1940        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        // Create a minimal config.toml with backend set but NO [lnd] section
1945        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        // Set environment variables for LND configuration
1956        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        // Load settings and apply environment variables (same as production code)
1967        let mut settings = Settings::new(Some(&config_path));
1968        settings.from_env().expect("Failed to apply env vars");
1969
1970        // Verify that settings were populated from env vars
1971        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        // Cleanup env vars
1984        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        // Cleanup test file
1992        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        // Create a temporary directory for config file
2001        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        // Create a minimal config.toml with backend set but NO [cln] section
2006        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        // Set environment variables for CLN configuration
2017        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        // Load settings and apply environment variables (same as production code)
2024        let mut settings = Settings::new(Some(&config_path));
2025        settings.from_env().expect("Failed to apply env vars");
2026
2027        // Verify that settings were populated from env vars
2028        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        // Cleanup env vars
2037        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        // Cleanup test file
2044        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        // Create a temporary directory for config file
2052        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        // Create a minimal config.toml with backend set but NO [lnbits] section
2057        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        // Set environment variables for LNbits configuration
2068        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        // Load settings and apply environment variables (same as production code)
2082        let mut settings = Settings::new(Some(&config_path));
2083        settings.from_env().expect("Failed to apply env vars");
2084
2085        // Verify that settings were populated from env vars
2086        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        // Cleanup env vars
2096        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        // Cleanup test file
2104        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        // Create a temporary directory for config file
2112        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        // Create a minimal config.toml with backend set but NO [fake_wallet] section
2117        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        // Set environment variables for FakeWallet configuration
2128        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        // Load settings and apply environment variables (same as production code)
2140        let mut settings = Settings::new(Some(&config_path));
2141        settings.from_env().expect("Failed to apply env vars");
2142
2143        // Verify that settings were populated from env vars
2144        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        // Cleanup env vars
2175        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        // Cleanup test file
2184        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        // Create a temporary directory for config file
2192        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        // Create a minimal config.toml with backend set but NO [grpc_processor] section
2197        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        // Set environment variables for GRPC Processor configuration
2208        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        // Load settings and apply environment variables (same as production code)
2217        let mut settings = Settings::new(Some(&config_path));
2218        settings.from_env().expect("Failed to apply env vars");
2219
2220        // Verify that settings were populated from env vars
2221        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        // Cleanup env vars
2227        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        // Cleanup test file
2233        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        // Create a temporary directory for config file
2241        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        // Create a minimal config.toml with backend set but NO [ldk_node] section
2246        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        // Set environment variables for LDK Node configuration
2257        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        // Load settings and apply environment variables (same as production code)
2275        let mut settings = Settings::new(Some(&config_path));
2276        settings.from_env().expect("Failed to apply env vars");
2277
2278        // Verify that settings were populated from env vars
2279        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        // Cleanup env vars
2293        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        // Cleanup test file
2302        let _ = fs::remove_dir_all(&temp_dir);
2303    }
2304}