Skip to main content

cdk_mintd/
config.rs

1use std::fmt;
2use std::path::PathBuf;
3
4use bitcoin::hashes::{sha256, Hash};
5use cdk::nuts::{CurrencyUnit, PublicKey};
6use cdk::Amount;
7use cdk_axum::cache;
8use cdk_common::common::QuoteTTL;
9use cdk_common::redact::url_for_logs;
10use config::{Config, ConfigError, File, FileFormat};
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
14#[serde(rename_all = "lowercase")]
15pub enum LoggingOutput {
16    /// Log to stderr only
17    Stderr,
18    /// Log to file only
19    File,
20    /// Log to both stderr and file (default)
21    #[default]
22    Both,
23}
24
25impl std::str::FromStr for LoggingOutput {
26    type Err = String;
27
28    fn from_str(s: &str) -> Result<Self, Self::Err> {
29        match s.to_lowercase().as_str() {
30            "stderr" => Ok(LoggingOutput::Stderr),
31            "file" => Ok(LoggingOutput::File),
32            "both" => Ok(LoggingOutput::Both),
33            _ => Err(format!(
34                "Unknown logging output: {s}. Valid options: stdout, file, both"
35            )),
36        }
37    }
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, Default)]
41pub struct LoggingConfig {
42    /// Where to output logs: stdout, file, or both
43    #[serde(default)]
44    pub output: LoggingOutput,
45    /// Log level for console output (when stdout or both)
46    pub console_level: Option<String>,
47    /// Log level for file output (when file or both)
48    pub file_level: Option<String>,
49}
50
51#[derive(Clone, Serialize, Deserialize)]
52#[serde(default)]
53pub struct Info {
54    pub url: String,
55    pub listen_host: String,
56    pub listen_port: u16,
57    /// Overrides mnemonic
58    pub seed: Option<String>,
59    pub mnemonic: Option<String>,
60    pub input_fee_ppk: Option<u64>,
61    /// Use keyset v2
62    pub use_keyset_v2: Option<bool>,
63
64    pub http_cache: cache::Config,
65
66    /// Logging configuration
67    #[serde(default)]
68    pub logging: LoggingConfig,
69
70    /// When this is set to true, the mint exposes a very simple info page at `/`
71    /// showing the mint name and description.
72    ///
73    /// This requires `mintd` was built with the `info-page` feature flag.
74    pub enable_info_page: Option<bool>,
75
76    /// Optional persisted quote TTL values (seconds) to initialize the database with
77    /// when RPC is disabled or on first-run when RPC is enabled.
78    /// If not provided, defaults are used.
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub quote_ttl: Option<QuoteTTL>,
81}
82
83impl Default for Info {
84    fn default() -> Self {
85        Info {
86            url: String::new(),
87            listen_host: "127.0.0.1".to_string(),
88            listen_port: 8091, // Default to port 8091 instead of 0
89            seed: None,
90            mnemonic: None,
91            input_fee_ppk: None,
92            use_keyset_v2: None,
93            http_cache: cache::Config::default(),
94            enable_info_page: Some(true),
95            logging: LoggingConfig::default(),
96            quote_ttl: None,
97        }
98    }
99}
100
101impl std::fmt::Debug for Info {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        // Use a fallback approach that won't panic
104        let mnemonic_display: String = {
105            if let Some(mnemonic) = self.mnemonic.as_ref() {
106                let hash = sha256::Hash::hash(mnemonic.as_bytes());
107                format!("<hashed: {hash}>")
108            } else {
109                "<not set>".to_string()
110            }
111        };
112
113        f.debug_struct("Info")
114            .field("url", &self.url)
115            .field("listen_host", &self.listen_host)
116            .field("listen_port", &self.listen_port)
117            .field("mnemonic", &mnemonic_display)
118            .field("input_fee_ppk", &self.input_fee_ppk)
119            .field("use_keyset_v2", &self.use_keyset_v2)
120            .field("http_cache", &self.http_cache)
121            .field("logging", &self.logging)
122            .field("enable_info_page", &self.enable_info_page)
123            .finish()
124    }
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
128#[serde(default)]
129pub struct Signatory {
130    #[serde(default)]
131    pub enabled: bool,
132    #[serde(default = "default_signatory_address")]
133    pub address: String,
134    #[serde(default = "default_signatory_port")]
135    pub port: u16,
136    #[serde(default)]
137    pub tls_dir: Option<PathBuf>,
138    #[serde(default)]
139    pub allow_insecure: bool,
140}
141
142impl Default for Signatory {
143    fn default() -> Self {
144        Self {
145            enabled: false,
146            address: default_signatory_address(),
147            port: default_signatory_port(),
148            tls_dir: None,
149            allow_insecure: false,
150        }
151    }
152}
153
154fn default_signatory_address() -> String {
155    "127.0.0.1".to_string()
156}
157
158fn default_signatory_port() -> u16 {
159    15060
160}
161
162#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
163#[serde(rename_all = "lowercase")]
164pub enum PaymentBackendType {
165    #[default]
166    None,
167    #[cfg(feature = "cln")]
168    Cln,
169    #[cfg(feature = "fakewallet")]
170    FakeWallet,
171    #[cfg(feature = "lnd")]
172    Lnd,
173    #[cfg(feature = "ldk-node")]
174    #[serde(alias = "ldk-node")]
175    LdkNode,
176    #[cfg(feature = "grpc-processor")]
177    GrpcProcessor,
178}
179
180impl std::str::FromStr for PaymentBackendType {
181    type Err = String;
182
183    fn from_str(s: &str) -> Result<Self, Self::Err> {
184        match s.to_lowercase().as_str() {
185            #[cfg(feature = "cln")]
186            "cln" => Ok(PaymentBackendType::Cln),
187            #[cfg(feature = "fakewallet")]
188            "fakewallet" => Ok(PaymentBackendType::FakeWallet),
189            #[cfg(feature = "lnd")]
190            "lnd" => Ok(PaymentBackendType::Lnd),
191            #[cfg(feature = "ldk-node")]
192            "ldk-node" | "ldknode" => Ok(PaymentBackendType::LdkNode),
193            #[cfg(feature = "grpc-processor")]
194            "grpcprocessor" => Ok(PaymentBackendType::GrpcProcessor),
195            _ => Err(format!("Unknown payment backend: {s}")),
196        }
197    }
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
201#[serde(default)]
202pub struct PaymentBackend {
203    pub backend: PaymentBackendType,
204    #[serde(default)]
205    pub unit: CurrencyUnit,
206    pub invoice_description: Option<String>,
207    pub min_mint: Amount,
208    pub max_mint: Amount,
209    pub min_melt: Amount,
210    pub max_melt: Amount,
211}
212
213impl Default for PaymentBackend {
214    fn default() -> Self {
215        PaymentBackend {
216            backend: PaymentBackendType::default(),
217            unit: CurrencyUnit::default(),
218            invoice_description: None,
219            min_mint: 1.into(),
220            max_mint: 500_000.into(),
221            min_melt: 1.into(),
222            max_melt: 500_000.into(),
223        }
224    }
225}
226
227fn deserialize_payment_backend<'de, D>(deserializer: D) -> Result<Vec<PaymentBackend>, D::Error>
228where
229    D: serde::Deserializer<'de>,
230{
231    #[derive(Deserialize)]
232    #[serde(untagged)]
233    enum PaymentBackendOneOrMany {
234        Many(Vec<PaymentBackend>),
235        One(PaymentBackend),
236    }
237
238    match PaymentBackendOneOrMany::deserialize(deserializer)? {
239        PaymentBackendOneOrMany::Many(backends) => Ok(backends),
240        PaymentBackendOneOrMany::One(backend) => Ok(vec![backend]),
241    }
242}
243
244#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
245#[serde(rename_all = "lowercase")]
246pub enum OnchainBackend {
247    #[default]
248    None,
249    #[cfg(feature = "bdk")]
250    Bdk,
251    #[cfg(feature = "fakewallet")]
252    FakeWallet,
253}
254
255impl std::str::FromStr for OnchainBackend {
256    type Err = String;
257
258    fn from_str(s: &str) -> Result<Self, Self::Err> {
259        match s.to_lowercase().as_str() {
260            "none" => Ok(OnchainBackend::None),
261            #[cfg(feature = "bdk")]
262            "bdk" => Ok(OnchainBackend::Bdk),
263            #[cfg(feature = "fakewallet")]
264            "fakewallet" => Ok(OnchainBackend::FakeWallet),
265            _ => Err(format!("Unknown Onchain backend: {s}")),
266        }
267    }
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
271#[serde(default)]
272pub struct Onchain {
273    pub onchain_backend: OnchainBackend,
274    pub min_mint: Amount,
275    pub max_mint: Amount,
276    pub min_melt: Amount,
277    pub max_melt: Amount,
278}
279
280impl Default for Onchain {
281    fn default() -> Self {
282        Onchain {
283            onchain_backend: OnchainBackend::default(),
284            min_mint: 1.into(),
285            max_mint: 500_000.into(),
286            min_melt: 1.into(),
287            max_melt: 500_000.into(),
288        }
289    }
290}
291
292#[cfg(feature = "bdk")]
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct BatchConfig {
295    /// How often the batch processor wakes up to check for ready intents
296    #[serde(default = "default_bdk_poll_interval_secs")]
297    pub poll_interval_secs: u64,
298    /// Maximum number of intents to include in a single batch
299    #[serde(default = "default_bdk_max_batch_size")]
300    pub max_batch_size: usize,
301    /// Average block interval used to derive default delayed tier deadlines.
302    #[serde(default = "default_bdk_target_block_time_secs")]
303    pub target_block_time_secs: u64,
304    /// Optional override for how long standard-tier intents wait before being eligible
305    #[serde(default)]
306    pub standard_deadline_secs: Option<u64>,
307    /// Optional override for how long economy-tier intents wait before being eligible
308    #[serde(default)]
309    pub economy_deadline_secs: Option<u64>,
310    /// Fee tiers exposed in melt quotes. Order determines fee_index values.
311    #[serde(default = "default_bdk_fee_options")]
312    pub fee_options: Vec<String>,
313    /// Quote-time fallback fee rate used when chain estimation fails, in sat/vB.
314    #[serde(default = "default_bdk_fee_fallback_sat_per_vb")]
315    pub fee_fallback_sat_per_vb: f64,
316    /// Fee-rate cache TTL, in seconds.
317    #[serde(default = "default_bdk_fee_cache_ttl_secs")]
318    pub fee_cache_ttl_secs: u64,
319    /// Maximum input count reserved for a quote estimate.
320    #[serde(default = "default_bdk_quote_max_input_count")]
321    pub quote_max_input_count: usize,
322    /// Fixed safety margin added to quote-time fee estimates, in sats.
323    #[serde(default = "default_bdk_quote_fixed_safety_sat")]
324    pub quote_fixed_safety_sat: u64,
325    /// Multiplicative safety margin applied after the raw quote fee estimate.
326    #[serde(default = "default_bdk_quote_safety_multiplier")]
327    pub quote_safety_multiplier: f64,
328}
329
330#[cfg(feature = "bdk")]
331impl Default for BatchConfig {
332    fn default() -> Self {
333        Self {
334            poll_interval_secs: default_bdk_poll_interval_secs(),
335            max_batch_size: default_bdk_max_batch_size(),
336            target_block_time_secs: default_bdk_target_block_time_secs(),
337            standard_deadline_secs: None,
338            economy_deadline_secs: None,
339            fee_options: default_bdk_fee_options(),
340            fee_fallback_sat_per_vb: default_bdk_fee_fallback_sat_per_vb(),
341            fee_cache_ttl_secs: default_bdk_fee_cache_ttl_secs(),
342            quote_max_input_count: default_bdk_quote_max_input_count(),
343            quote_fixed_safety_sat: default_bdk_quote_fixed_safety_sat(),
344            quote_safety_multiplier: default_bdk_quote_safety_multiplier(),
345        }
346    }
347}
348
349#[cfg(feature = "bdk")]
350#[derive(Clone, Serialize, Deserialize)]
351pub struct Bdk {
352    /// Fee percentage (e.g., 0.02 for 2%)
353    #[serde(default = "default_fee_percent")]
354    pub fee_percent: f32,
355    /// Minimum reserve fee
356    #[serde(default = "default_reserve_fee_min")]
357    pub reserve_fee_min: Amount,
358    /// Bitcoin network (mainnet, testnet, signet, regtest)
359    pub network: Option<String>,
360    /// Chain source type ("esplora", "electrum", or "bitcoinrpc"; defaults to "bitcoinrpc")
361    pub chain_source_type: Option<String>,
362    /// Esplora URL (when chain_source_type = "esplora")
363    pub esplora_url: Option<String>,
364    /// Number of parallel Esplora requests during wallet sync.
365    ///
366    /// Public Esplora servers often rate-limit bursty clients, so the default
367    /// is conservative. Increase only when using a private or higher-limit
368    /// Esplora server.
369    #[serde(default = "default_bdk_esplora_parallel_requests")]
370    pub esplora_parallel_requests: usize,
371    /// Electrum URL (when chain_source_type = "electrum")
372    pub electrum_url: Option<String>,
373    /// Number of scripts to request in each Electrum sync batch
374    #[serde(default = "default_bdk_electrum_batch_size")]
375    pub electrum_batch_size: usize,
376    /// Bitcoin RPC host (when chain_source_type = "bitcoinrpc")
377    pub bitcoind_rpc_host: Option<String>,
378    /// Bitcoin RPC port
379    pub bitcoind_rpc_port: Option<u16>,
380    /// Bitcoin RPC user
381    pub bitcoind_rpc_user: Option<String>,
382    /// Bitcoin RPC password
383    pub bitcoind_rpc_password: Option<String>,
384    /// Optional birthday height used when creating a fresh Bitcoin RPC wallet.
385    ///
386    /// If unset, the wallet starts at the current chain tip. Set this when
387    /// restoring from a mnemonic to rescan from a known height. Existing
388    /// wallets are not rewound.
389    pub wallet_rescan_from_height: Option<u32>,
390    /// BIP-39 mnemonic for the BDK wallet
391    pub mnemonic: Option<String>,
392    /// Batch processor configuration
393    #[serde(default)]
394    pub batch_config: BatchConfig,
395    /// Number of confirmations required for incoming payments.
396    ///
397    /// Must be >= 1. A value of 0 is rejected at startup because the
398    /// confirmation check still requires the transaction to have an on-chain
399    /// anchor (i.e. 0 would mean "confirmed in any block", not "accept
400    /// unconfirmed"). Use 1 for "accept any confirmation".
401    #[serde(default = "default_bdk_num_confs")]
402    pub num_confs: u32,
403    /// Minimum receive amount in sats
404    #[serde(default = "default_bdk_min_receive_amount_sat")]
405    pub min_receive_amount_sat: u64,
406    /// Minimum send amount in sats
407    #[serde(default = "default_bdk_min_send_amount_sat")]
408    pub min_send_amount_sat: u64,
409    /// Wallet sync interval in seconds
410    #[serde(default = "default_bdk_sync_interval_secs")]
411    pub sync_interval_secs: u64,
412}
413
414#[cfg(feature = "bdk")]
415impl fmt::Debug for Bdk {
416    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
417        f.debug_struct("Bdk")
418            .field("fee_percent", &self.fee_percent)
419            .field("reserve_fee_min", &self.reserve_fee_min)
420            .field("network", &self.network)
421            .field("chain_source_type", &self.chain_source_type)
422            .field(
423                "esplora_url",
424                &self.esplora_url.as_deref().map(url_for_logs),
425            )
426            .field("esplora_parallel_requests", &self.esplora_parallel_requests)
427            .field(
428                "electrum_url",
429                &self.electrum_url.as_deref().map(url_for_logs),
430            )
431            .field("electrum_batch_size", &self.electrum_batch_size)
432            .field("bitcoind_rpc_host", &self.bitcoind_rpc_host)
433            .field("bitcoind_rpc_port", &self.bitcoind_rpc_port)
434            .field("bitcoind_rpc_user", &self.bitcoind_rpc_user)
435            .field("bitcoind_rpc_password", &"[REDACTED]")
436            .field("wallet_rescan_from_height", &self.wallet_rescan_from_height)
437            .field("mnemonic", &"[REDACTED]")
438            .field("batch_config", &self.batch_config)
439            .field("num_confs", &self.num_confs)
440            .field("min_receive_amount_sat", &self.min_receive_amount_sat)
441            .field("min_send_amount_sat", &self.min_send_amount_sat)
442            .field("sync_interval_secs", &self.sync_interval_secs)
443            .finish()
444    }
445}
446
447#[cfg(feature = "bdk")]
448impl Default for Bdk {
449    fn default() -> Self {
450        Self {
451            fee_percent: default_fee_percent(),
452            reserve_fee_min: default_reserve_fee_min(),
453            network: None,
454            chain_source_type: None,
455            esplora_url: None,
456            esplora_parallel_requests: default_bdk_esplora_parallel_requests(),
457            electrum_url: None,
458            electrum_batch_size: default_bdk_electrum_batch_size(),
459            bitcoind_rpc_host: None,
460            bitcoind_rpc_port: None,
461            bitcoind_rpc_user: None,
462            bitcoind_rpc_password: None,
463            wallet_rescan_from_height: None,
464            mnemonic: None,
465            batch_config: BatchConfig::default(),
466            num_confs: default_bdk_num_confs(),
467            min_receive_amount_sat: default_bdk_min_receive_amount_sat(),
468            min_send_amount_sat: default_bdk_min_send_amount_sat(),
469            sync_interval_secs: default_bdk_sync_interval_secs(),
470        }
471    }
472}
473
474#[cfg(feature = "bdk")]
475impl Bdk {
476    /// Validate BDK settings that must be rejected before the backend starts.
477    pub fn validate(&self) -> Result<(), String> {
478        if self.num_confs == 0 {
479            return Err(
480                "BDK num_confs must be >= 1 (0 is rejected because it still \
481                 requires an on-chain anchor and is almost never intended; \
482                 use 1 for 'any confirmation')"
483                    .to_string(),
484            );
485        }
486
487        if self.min_send_amount_sat == 0 {
488            return Err("BDK min_send_amount_sat must be >= 1".to_string());
489        }
490
491        if self.batch_config.target_block_time_secs == 0 {
492            return Err("BDK batch_config.target_block_time_secs must be >= 1".to_string());
493        }
494
495        validate_bdk_fee_options(&self.batch_config.fee_options)?;
496
497        Ok(())
498    }
499}
500
501#[cfg(feature = "bdk")]
502fn default_bdk_num_confs() -> u32 {
503    6
504}
505
506#[cfg(feature = "bdk")]
507fn default_bdk_min_receive_amount_sat() -> u64 {
508    1000
509}
510
511#[cfg(feature = "bdk")]
512fn default_bdk_min_send_amount_sat() -> u64 {
513    546
514}
515
516#[cfg(feature = "bdk")]
517fn default_bdk_sync_interval_secs() -> u64 {
518    30
519}
520
521#[cfg(feature = "bdk")]
522fn default_bdk_esplora_parallel_requests() -> usize {
523    1
524}
525
526#[cfg(feature = "bdk")]
527fn default_bdk_electrum_batch_size() -> usize {
528    5
529}
530
531#[cfg(feature = "bdk")]
532fn default_bdk_poll_interval_secs() -> u64 {
533    30
534}
535
536#[cfg(feature = "bdk")]
537fn default_bdk_max_batch_size() -> usize {
538    50
539}
540
541#[cfg(feature = "bdk")]
542fn default_bdk_target_block_time_secs() -> u64 {
543    cdk_bdk::DEFAULT_TARGET_BLOCK_TIME_SECS
544}
545
546#[cfg(feature = "bdk")]
547fn default_bdk_fee_options() -> Vec<String> {
548    vec!["immediate".to_string()]
549}
550
551#[cfg(feature = "bdk")]
552fn validate_bdk_fee_options(fee_options: &[String]) -> Result<(), String> {
553    let tiers = fee_options
554        .iter()
555        .map(|tier| {
556            cdk_bdk::PaymentTier::from_config_name(tier).ok_or_else(|| {
557                format!(
558                    "Unknown BDK batch_config.fee_options tier '{tier}'; expected immediate, standard, or economy"
559                )
560            })
561        })
562        .collect::<Result<Vec<_>, _>>()?;
563
564    cdk_bdk::types::validate_fee_options(&tiers)
565}
566
567#[cfg(feature = "bdk")]
568fn default_bdk_fee_fallback_sat_per_vb() -> f64 {
569    2.0
570}
571
572#[cfg(feature = "bdk")]
573fn default_bdk_fee_cache_ttl_secs() -> u64 {
574    60
575}
576
577#[cfg(feature = "bdk")]
578fn default_bdk_quote_max_input_count() -> usize {
579    24
580}
581
582#[cfg(feature = "bdk")]
583fn default_bdk_quote_fixed_safety_sat() -> u64 {
584    500
585}
586
587#[cfg(feature = "bdk")]
588fn default_bdk_quote_safety_multiplier() -> f64 {
589    1.25
590}
591
592#[cfg(feature = "cln")]
593#[derive(Debug, Clone, Serialize, Deserialize)]
594#[serde(default)]
595pub struct Cln {
596    pub rpc_path: PathBuf,
597    #[serde(default = "default_cln_bolt12")]
598    pub bolt12: bool,
599    #[serde(default)]
600    pub expose_private_channels: bool,
601    #[serde(default = "default_fee_percent")]
602    pub fee_percent: f32,
603    #[serde(default = "default_reserve_fee_min")]
604    pub reserve_fee_min: Amount,
605}
606
607#[cfg(feature = "cln")]
608impl Default for Cln {
609    fn default() -> Self {
610        Self {
611            rpc_path: PathBuf::new(),
612            bolt12: true,
613            expose_private_channels: false,
614            fee_percent: 0.02,
615            reserve_fee_min: 2.into(),
616        }
617    }
618}
619
620#[cfg(feature = "cln")]
621fn default_cln_bolt12() -> bool {
622    true
623}
624
625#[cfg(feature = "lnd")]
626#[derive(Debug, Clone, Serialize, Deserialize)]
627#[serde(default)]
628pub struct Lnd {
629    pub address: String,
630    pub cert_file: PathBuf,
631    pub macaroon_file: PathBuf,
632    #[serde(default = "default_fee_percent")]
633    pub fee_percent: f32,
634    #[serde(default = "default_reserve_fee_min")]
635    pub reserve_fee_min: Amount,
636}
637
638#[cfg(feature = "lnd")]
639impl Default for Lnd {
640    fn default() -> Self {
641        Self {
642            address: String::new(),
643            cert_file: PathBuf::new(),
644            macaroon_file: PathBuf::new(),
645            fee_percent: 0.02,
646            reserve_fee_min: 2.into(),
647        }
648    }
649}
650
651#[cfg(feature = "ldk-node")]
652#[derive(Clone, Serialize, Deserialize)]
653#[serde(default)]
654pub struct LdkNode {
655    /// Fee percentage (e.g., 0.02 for 2%)
656    #[serde(default = "default_ldk_fee_percent")]
657    pub fee_percent: f32,
658    /// Minimum reserve fee
659    #[serde(default = "default_ldk_reserve_fee_min")]
660    pub reserve_fee_min: Amount,
661    /// Bitcoin network (mainnet, testnet, signet, regtest)
662    pub bitcoin_network: Option<String>,
663    /// Chain source type (esplora, electrum, or bitcoinrpc)
664    pub chain_source_type: Option<String>,
665    /// Esplora URL (when chain_source_type = "esplora")
666    pub esplora_url: Option<String>,
667    /// Electrum URL (when chain_source_type = "electrum")
668    pub electrum_url: Option<String>,
669    /// Bitcoin RPC configuration (when chain_source_type = "bitcoinrpc")
670    pub bitcoind_rpc_host: Option<String>,
671    pub bitcoind_rpc_port: Option<u16>,
672    pub bitcoind_rpc_user: Option<String>,
673    pub bitcoind_rpc_password: Option<String>,
674    /// Storage directory path
675    pub storage_dir_path: Option<String>,
676    /// Log directory path (logging stdout if omitted)
677    pub log_dir_path: Option<String>,
678    /// LDK node listening host
679    pub ldk_node_host: Option<String>,
680    /// LDK node listening port
681    pub ldk_node_port: Option<u16>,
682    /// LDK node announcement addresses
683    pub ldk_node_announce_addresses: Option<Vec<String>>,
684    /// Gossip source type (p2p or rgs)
685    pub gossip_source_type: Option<String>,
686    /// Rapid Gossip Sync URL (when gossip_source_type = "rgs")
687    pub rgs_url: Option<String>,
688    /// Webserver host (defaults to 127.0.0.1)
689    #[serde(default = "default_webserver_host")]
690    pub webserver_host: Option<String>,
691    /// Webserver port
692    #[serde(default = "default_webserver_port")]
693    pub webserver_port: Option<u16>,
694    /// LDK node mnemonic
695    /// If not set, LDK node will use its default seed storage mechanism
696    pub ldk_node_mnemonic: Option<String>,
697}
698
699#[cfg(feature = "ldk-node")]
700impl Default for LdkNode {
701    fn default() -> Self {
702        Self {
703            fee_percent: default_ldk_fee_percent(),
704            reserve_fee_min: default_ldk_reserve_fee_min(),
705            bitcoin_network: None,
706            chain_source_type: None,
707            esplora_url: None,
708            electrum_url: None,
709            bitcoind_rpc_host: None,
710            bitcoind_rpc_port: None,
711            bitcoind_rpc_user: None,
712            ldk_node_announce_addresses: None,
713            bitcoind_rpc_password: None,
714            storage_dir_path: None,
715            ldk_node_host: None,
716            log_dir_path: None,
717            ldk_node_port: None,
718            gossip_source_type: None,
719            rgs_url: None,
720            webserver_host: default_webserver_host(),
721            webserver_port: default_webserver_port(),
722            ldk_node_mnemonic: None,
723        }
724    }
725}
726
727#[cfg(feature = "ldk-node")]
728impl fmt::Debug for LdkNode {
729    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
730        f.debug_struct("LdkNode")
731            .field("fee_percent", &self.fee_percent)
732            .field("reserve_fee_min", &self.reserve_fee_min)
733            .field("bitcoin_network", &self.bitcoin_network)
734            .field("chain_source_type", &self.chain_source_type)
735            .field(
736                "esplora_url",
737                &self.esplora_url.as_deref().map(url_for_logs),
738            )
739            .field(
740                "electrum_url",
741                &self.electrum_url.as_deref().map(url_for_logs),
742            )
743            .field("bitcoind_rpc_host", &self.bitcoind_rpc_host)
744            .field("bitcoind_rpc_port", &self.bitcoind_rpc_port)
745            .field("bitcoind_rpc_user", &self.bitcoind_rpc_user)
746            .field("bitcoind_rpc_password", &"[REDACTED]")
747            .field("storage_dir_path", &self.storage_dir_path)
748            .field("log_dir_path", &self.log_dir_path)
749            .field("ldk_node_host", &self.ldk_node_host)
750            .field("ldk_node_port", &self.ldk_node_port)
751            .field(
752                "ldk_node_announce_addresses",
753                &self.ldk_node_announce_addresses,
754            )
755            .field("gossip_source_type", &self.gossip_source_type)
756            .field("rgs_url", &self.rgs_url.as_deref().map(url_for_logs))
757            .field("webserver_host", &self.webserver_host)
758            .field("webserver_port", &self.webserver_port)
759            .field("ldk_node_mnemonic", &"[REDACTED]")
760            .finish()
761    }
762}
763
764#[cfg(feature = "ldk-node")]
765fn default_ldk_fee_percent() -> f32 {
766    0.04
767}
768
769#[cfg(feature = "ldk-node")]
770fn default_ldk_reserve_fee_min() -> Amount {
771    4.into()
772}
773
774#[cfg(feature = "ldk-node")]
775fn default_webserver_host() -> Option<String> {
776    Some("127.0.0.1".to_string())
777}
778
779#[cfg(feature = "ldk-node")]
780fn default_webserver_port() -> Option<u16> {
781    Some(8091)
782}
783
784#[cfg(feature = "fakewallet")]
785#[derive(Debug, Clone, Serialize, Deserialize)]
786pub struct FakeWalletKeysetRotation {
787    /// Currency unit (e.g. "sat", "usd")
788    pub unit: CurrencyUnit,
789    /// Input fee in parts per thousand
790    #[serde(default)]
791    pub input_fee_ppk: u64,
792    /// Keyset version: "v1" (Version00) or "v2" (Version01)
793    #[serde(default = "default_keyset_version")]
794    pub version: String,
795    /// If true, the keyset will be created with a past expiry (expired)
796    #[serde(default)]
797    pub expired: bool,
798}
799
800#[cfg(feature = "fakewallet")]
801fn default_keyset_version() -> String {
802    "v1".to_string()
803}
804
805#[cfg(feature = "fakewallet")]
806#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
807#[serde(untagged)]
808pub enum FakeWalletCustomPaymentMethod {
809    /// Custom method available for every supported fake wallet unit
810    Method(String),
811    /// Custom method available only for one unit
812    MethodForUnit {
813        /// Payment method name (e.g. "paypal", "venmo")
814        method: String,
815        /// Currency unit for this method
816        unit: CurrencyUnit,
817    },
818}
819
820#[cfg(feature = "fakewallet")]
821impl FakeWalletCustomPaymentMethod {
822    pub fn method(&self) -> &str {
823        match self {
824            Self::Method(method) => method,
825            Self::MethodForUnit { method, .. } => method,
826        }
827    }
828
829    pub fn applies_to_unit(&self, unit: &CurrencyUnit) -> bool {
830        match self {
831            Self::Method(_) => true,
832            Self::MethodForUnit {
833                unit: method_unit, ..
834            } => method_unit == unit,
835        }
836    }
837}
838
839#[cfg(feature = "fakewallet")]
840#[derive(Debug, Clone, Serialize, Deserialize)]
841#[serde(default)]
842pub struct FakeWallet {
843    #[serde(default = "default_fake_wallet_supported_units")]
844    pub supported_units: Vec<CurrencyUnit>,
845    pub fee_percent: f32,
846    pub reserve_fee_min: Amount,
847    #[serde(default = "default_fake_wallet_custom_payment_methods")]
848    pub custom_payment_methods: Vec<FakeWalletCustomPaymentMethod>,
849    #[serde(default = "default_min_delay_time")]
850    pub min_delay_time: u64,
851    #[serde(default = "default_max_delay_time")]
852    pub max_delay_time: u64,
853    /// Additional keyset rotations to create during mint build
854    #[serde(default)]
855    pub keyset_rotations: Vec<FakeWalletKeysetRotation>,
856}
857
858#[cfg(feature = "fakewallet")]
859impl Default for FakeWallet {
860    fn default() -> Self {
861        Self {
862            supported_units: vec![CurrencyUnit::Sat],
863            fee_percent: 0.02,
864            reserve_fee_min: 2.into(),
865            custom_payment_methods: default_fake_wallet_custom_payment_methods(),
866            min_delay_time: 1,
867            max_delay_time: 3,
868            keyset_rotations: Vec::new(),
869        }
870    }
871}
872
873// Helper functions to provide default values
874// Common fee defaults for all backends
875#[cfg(any(feature = "cln", feature = "lnd"))]
876fn default_fee_percent() -> f32 {
877    0.02
878}
879
880#[cfg(any(feature = "cln", feature = "lnd"))]
881fn default_reserve_fee_min() -> Amount {
882    2.into()
883}
884
885#[cfg(feature = "fakewallet")]
886fn default_min_delay_time() -> u64 {
887    1
888}
889
890#[cfg(feature = "fakewallet")]
891fn default_max_delay_time() -> u64 {
892    3
893}
894
895#[cfg(feature = "fakewallet")]
896fn default_fake_wallet_custom_payment_methods() -> Vec<FakeWalletCustomPaymentMethod> {
897    vec![FakeWalletCustomPaymentMethod::Method("paypal".to_string())]
898}
899
900#[cfg(feature = "fakewallet")]
901fn default_fake_wallet_supported_units() -> Vec<CurrencyUnit> {
902    vec![CurrencyUnit::Sat]
903}
904
905#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
906#[serde(default)]
907pub struct GrpcProcessor {
908    #[serde(default)]
909    pub supported_units: Vec<CurrencyUnit>,
910    #[serde(default = "default_grpc_address", alias = "addr")]
911    pub address: String,
912    #[serde(default = "default_grpc_port")]
913    pub port: u16,
914    #[serde(default)]
915    pub tls_dir: Option<PathBuf>,
916    #[serde(default)]
917    pub allow_insecure: bool,
918}
919
920impl Default for GrpcProcessor {
921    fn default() -> Self {
922        Self {
923            supported_units: Vec::new(),
924            address: default_grpc_address(),
925            port: default_grpc_port(),
926            tls_dir: None,
927            allow_insecure: false,
928        }
929    }
930}
931
932fn default_grpc_address() -> String {
933    "127.0.0.1".to_string()
934}
935
936fn default_grpc_port() -> u16 {
937    50051
938}
939
940#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
941#[serde(rename_all = "lowercase")]
942pub enum DatabaseEngine {
943    #[default]
944    Sqlite,
945    Postgres,
946}
947
948impl std::str::FromStr for DatabaseEngine {
949    type Err = String;
950
951    fn from_str(s: &str) -> Result<Self, Self::Err> {
952        match s.to_lowercase().as_str() {
953            "sqlite" => Ok(DatabaseEngine::Sqlite),
954            "postgres" => Ok(DatabaseEngine::Postgres),
955            _ => Err(format!("Unknown database engine: {s}")),
956        }
957    }
958}
959
960#[derive(Debug, Clone, Serialize, Deserialize, Default)]
961#[serde(default)]
962pub struct Database {
963    pub engine: DatabaseEngine,
964    pub postgres: Option<PostgresConfig>,
965}
966
967#[derive(Debug, Clone, Serialize, Deserialize, Default)]
968#[serde(default)]
969pub struct AuthDatabase {
970    pub postgres: Option<PostgresAuthConfig>,
971}
972
973#[derive(Clone, Serialize, Deserialize)]
974#[serde(default)]
975pub struct PostgresAuthConfig {
976    pub url: String,
977    pub tls_mode: Option<String>,
978    pub max_connections: Option<usize>,
979    pub connection_timeout_seconds: Option<u64>,
980}
981
982impl fmt::Debug for PostgresAuthConfig {
983    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
984        f.debug_struct("PostgresAuthConfig")
985            .field("url", &url_for_logs(&self.url))
986            .field("tls_mode", &self.tls_mode)
987            .field("max_connections", &self.max_connections)
988            .field(
989                "connection_timeout_seconds",
990                &self.connection_timeout_seconds,
991            )
992            .finish()
993    }
994}
995
996impl Default for PostgresAuthConfig {
997    fn default() -> Self {
998        Self {
999            url: String::new(),
1000            tls_mode: Some("disable".to_string()),
1001            max_connections: Some(20),
1002            connection_timeout_seconds: Some(10),
1003        }
1004    }
1005}
1006
1007#[derive(Clone, Serialize, Deserialize)]
1008#[serde(default)]
1009pub struct PostgresConfig {
1010    pub url: String,
1011    pub tls_mode: Option<String>,
1012    pub max_connections: Option<usize>,
1013    pub connection_timeout_seconds: Option<u64>,
1014}
1015
1016impl fmt::Debug for PostgresConfig {
1017    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1018        f.debug_struct("PostgresConfig")
1019            .field("url", &url_for_logs(&self.url))
1020            .field("tls_mode", &self.tls_mode)
1021            .field("max_connections", &self.max_connections)
1022            .field(
1023                "connection_timeout_seconds",
1024                &self.connection_timeout_seconds,
1025            )
1026            .finish()
1027    }
1028}
1029
1030impl Default for PostgresConfig {
1031    fn default() -> Self {
1032        Self {
1033            url: String::new(),
1034            tls_mode: Some("disable".to_string()),
1035            max_connections: Some(20),
1036            connection_timeout_seconds: Some(10),
1037        }
1038    }
1039}
1040
1041#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
1042#[serde(rename_all = "lowercase")]
1043pub enum AuthType {
1044    Clear,
1045    Blind,
1046    #[default]
1047    None,
1048}
1049
1050impl std::str::FromStr for AuthType {
1051    type Err = String;
1052
1053    fn from_str(s: &str) -> Result<Self, Self::Err> {
1054        match s.to_lowercase().as_str() {
1055            "clear" => Ok(AuthType::Clear),
1056            "blind" => Ok(AuthType::Blind),
1057            "none" => Ok(AuthType::None),
1058            _ => Err(format!("Unknown auth type: {s}")),
1059        }
1060    }
1061}
1062
1063#[derive(Debug, Clone, Serialize, Deserialize)]
1064#[serde(default)]
1065pub struct Auth {
1066    #[serde(default)]
1067    pub auth_enabled: bool,
1068    pub openid_discovery: String,
1069    pub openid_client_id: String,
1070    pub mint_max_bat: u64,
1071    #[serde(default = "default_blind")]
1072    pub mint: AuthType,
1073    #[serde(default)]
1074    pub get_mint_quote: AuthType,
1075    #[serde(default)]
1076    pub check_mint_quote: AuthType,
1077    #[serde(default)]
1078    pub melt: AuthType,
1079    #[serde(default)]
1080    pub get_melt_quote: AuthType,
1081    #[serde(default)]
1082    pub check_melt_quote: AuthType,
1083    #[serde(default = "default_blind")]
1084    pub swap: AuthType,
1085    #[serde(default = "default_blind")]
1086    pub restore: AuthType,
1087    #[serde(default)]
1088    pub check_proof_state: AuthType,
1089    /// Enable WebSocket authentication support
1090    #[serde(default = "default_blind")]
1091    pub websocket_auth: AuthType,
1092}
1093
1094fn default_blind() -> AuthType {
1095    AuthType::Blind
1096}
1097
1098impl Default for Auth {
1099    fn default() -> Self {
1100        Self {
1101            auth_enabled: false,
1102            openid_discovery: String::new(),
1103            openid_client_id: String::new(),
1104            mint_max_bat: 0,
1105            mint: default_blind(),
1106            get_mint_quote: AuthType::default(),
1107            check_mint_quote: AuthType::default(),
1108            melt: AuthType::default(),
1109            get_melt_quote: AuthType::default(),
1110            check_melt_quote: AuthType::default(),
1111            swap: default_blind(),
1112            restore: default_blind(),
1113            check_proof_state: AuthType::default(),
1114            websocket_auth: default_blind(),
1115        }
1116    }
1117}
1118
1119/// CDK settings, derived from `config.toml`
1120#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1121#[serde(default)]
1122pub struct Settings {
1123    pub info: Info,
1124    pub signatory: Option<Signatory>,
1125    pub mint_info: MintInfo,
1126    #[serde(default, deserialize_with = "deserialize_payment_backend")]
1127    pub payment_backend: Vec<PaymentBackend>,
1128    pub onchain: Option<Onchain>,
1129    /// Transaction limits for DoS protection
1130    #[serde(default)]
1131    pub limits: Limits,
1132    #[cfg(feature = "cln")]
1133    pub cln: Option<Cln>,
1134    #[cfg(feature = "lnd")]
1135    pub lnd: Option<Lnd>,
1136    #[cfg(feature = "ldk-node")]
1137    pub ldk_node: Option<LdkNode>,
1138    #[cfg(feature = "fakewallet")]
1139    pub fake_wallet: Option<FakeWallet>,
1140    pub grpc_processor: Option<GrpcProcessor>,
1141    #[cfg(feature = "bdk")]
1142    pub bdk: Option<Bdk>,
1143    pub database: Database,
1144    pub auth_database: Option<AuthDatabase>,
1145    #[cfg(feature = "management-rpc")]
1146    pub mint_management_rpc: Option<MintManagementRpc>,
1147    pub auth: Option<Auth>,
1148    #[cfg(feature = "prometheus")]
1149    #[serde(default, skip_serializing_if = "Option::is_none")]
1150    pub prometheus: Option<Prometheus>,
1151}
1152
1153#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1154#[cfg(feature = "prometheus")]
1155#[serde(default)]
1156pub struct Prometheus {
1157    pub enabled: bool,
1158    pub address: Option<String>,
1159    pub port: Option<u16>,
1160}
1161
1162/// Transaction limits configuration
1163#[derive(Debug, Clone, Serialize, Deserialize)]
1164pub struct Limits {
1165    /// Maximum number of inputs allowed per transaction (swap/melt)
1166    #[serde(default = "default_max_inputs")]
1167    pub max_inputs: usize,
1168    /// Maximum number of outputs allowed per transaction (mint/swap/melt)
1169    #[serde(default = "default_max_outputs")]
1170    pub max_outputs: usize,
1171}
1172
1173impl Default for Limits {
1174    fn default() -> Self {
1175        Self {
1176            max_inputs: 1000,
1177            max_outputs: 1000,
1178        }
1179    }
1180}
1181
1182fn default_max_inputs() -> usize {
1183    1000
1184}
1185
1186fn default_max_outputs() -> usize {
1187    1000
1188}
1189
1190#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1191#[serde(default)]
1192pub struct MintInfo {
1193    /// name of the mint and should be recognizable
1194    pub name: String,
1195    /// hex pubkey of the mint
1196    pub pubkey: Option<PublicKey>,
1197    /// short description of the mint
1198    pub description: String,
1199    /// long description
1200    pub description_long: Option<String>,
1201    /// url to the mint icon
1202    pub icon_url: Option<String>,
1203    /// message of the day that the wallet must display to the user
1204    pub motd: Option<String>,
1205    /// Nostr publickey
1206    pub contact_nostr_public_key: Option<String>,
1207    /// Contact email
1208    pub contact_email: Option<String>,
1209    /// URL to the terms of service
1210    pub tos_url: Option<String>,
1211}
1212
1213#[cfg(feature = "management-rpc")]
1214#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1215#[serde(default)]
1216pub struct MintManagementRpc {
1217    /// When this is set to `true` the mint use the config file for the initial set up on first start.
1218    /// Changes to the `[mint_info]` after this **MUST** be made via the RPC changes to the config file or env vars will be ignored.
1219    pub enabled: bool,
1220    pub address: Option<String>,
1221    pub port: Option<u16>,
1222    pub tls_dir: Option<PathBuf>,
1223    #[serde(default)]
1224    pub allow_insecure: bool,
1225    /// Allow management RPC callers to use mint quote state override endpoints.
1226    ///
1227    /// This includes marking quotes paid without backend confirmation and is
1228    /// disabled by default.
1229    #[serde(default)]
1230    pub allow_mint_quote_payment_override: bool,
1231}
1232
1233impl Settings {
1234    /// Parses settings from an in-memory TOML import document.
1235    pub fn try_from_toml(document: &str) -> Result<Self, ConfigError> {
1236        Self::try_from_toml_allowing(document, &[])
1237    }
1238
1239    /// Parses settings while allowing a narrow set of fields owned by the
1240    /// legacy migration layer.
1241    pub(crate) fn try_from_toml_allowing(
1242        document: &str,
1243        allowed_unknown_fields: &[&str],
1244    ) -> Result<Self, ConfigError> {
1245        let defaults = Self::default();
1246        let configuration = Config::builder()
1247            .add_source(Config::try_from(&defaults)?)
1248            .add_source(File::from_str(document, FileFormat::Toml))
1249            .build()?;
1250        Self::deserialize_configuration(configuration, allowed_unknown_fields)
1251    }
1252
1253    fn deserialize_configuration(
1254        configuration: Config,
1255        allowed_unknown_fields: &[&str],
1256    ) -> Result<Self, ConfigError> {
1257        let mut unknown_fields = Vec::new();
1258        let settings = serde_ignored::deserialize(configuration, |path| {
1259            let path = path.to_string().replace(".?.", ".");
1260            if !allowed_unknown_fields.contains(&path.as_str()) {
1261                unknown_fields.push(path);
1262            }
1263        })?;
1264        unknown_fields.sort();
1265        unknown_fields.dedup();
1266        if unknown_fields.is_empty() {
1267            Ok(settings)
1268        } else {
1269            let mut message = format!(
1270                "unknown configuration field(s): {}",
1271                unknown_fields.join(", ")
1272            );
1273            if unknown_fields.iter().any(|field| field == "ln") {
1274                message.push_str(
1275                    "; legacy `[ln]`/`[[ln]]` configuration must be migrated to \
1276                     `[payment_backend]`/`[[payment_backend]]`; run `cdk-mintd config migrate \
1277                     --file <old> --output <new>`",
1278                );
1279            }
1280            Err(ConfigError::Message(message))
1281        }
1282    }
1283
1284    pub fn validate_backend_pairing(&self) -> Result<(), String> {
1285        #[cfg(feature = "fakewallet")]
1286        self.validate_fake_wallet_backend_pairing()?;
1287
1288        Ok(())
1289    }
1290
1291    #[cfg(feature = "fakewallet")]
1292    fn validate_fake_wallet_backend_pairing(&self) -> Result<(), String> {
1293        let onchain_backend = self
1294            .onchain
1295            .as_ref()
1296            .map(|onchain| &onchain.onchain_backend)
1297            .unwrap_or(&OnchainBackend::None);
1298
1299        let has_fake_wallet_backend = self
1300            .payment_backend
1301            .iter()
1302            .any(|backend| backend.backend == PaymentBackendType::FakeWallet);
1303        let has_real_backend = self.payment_backend.iter().any(|backend| {
1304            !matches!(
1305                backend.backend,
1306                PaymentBackendType::None | PaymentBackendType::FakeWallet
1307            )
1308        });
1309
1310        if has_fake_wallet_backend && has_real_backend {
1311            return Err("backend = \"fakewallet\" cannot be combined with a real \
1312                 payment backend; use only fakewallet backends or only real backends"
1313                .to_string());
1314        }
1315
1316        match onchain_backend {
1317            #[cfg(feature = "bdk")]
1318            OnchainBackend::Bdk if has_fake_wallet_backend => {
1319                return Err("backend = \"fakewallet\" cannot be combined with \
1320                     onchain_backend = \"bdk\"; use onchain_backend = \
1321                     \"fakewallet\" or \"none\""
1322                    .to_string());
1323            }
1324            OnchainBackend::FakeWallet if has_real_backend => {
1325                return Err("onchain_backend = \"fakewallet\" cannot be combined with \
1326                     a real payment backend; use backend = \"fakewallet\" \
1327                     or \"none\""
1328                    .to_string());
1329            }
1330            _ => {}
1331        }
1332
1333        Ok(())
1334    }
1335
1336    pub fn try_new<P>(config_file_name: Option<P>) -> Result<Self, ConfigError>
1337    where
1338        P: Into<PathBuf>,
1339    {
1340        let default_settings = Self::default();
1341        Self::new_from_default(&default_settings, config_file_name)
1342    }
1343
1344    /// Loads settings from defaults and an optional config file.
1345    ///
1346    /// New code should use [`Self::try_new`] so configuration errors can be
1347    /// reported to the caller. This method retains its historical fallback
1348    /// behavior for API compatibility.
1349    #[deprecated(note = "use Settings::try_new to handle configuration errors")]
1350    #[must_use]
1351    pub fn new<P>(config_file_name: Option<P>) -> Self
1352    where
1353        P: Into<PathBuf>,
1354    {
1355        match Self::try_new(config_file_name) {
1356            Ok(settings) => settings,
1357            Err(err) => {
1358                tracing::error!("Error reading config file, falling back to defaults: {err}");
1359                Self::default()
1360            }
1361        }
1362    }
1363
1364    fn new_from_default<P>(
1365        default: &Settings,
1366        config_file_name: Option<P>,
1367    ) -> Result<Self, ConfigError>
1368    where
1369        P: Into<PathBuf>,
1370    {
1371        let config: String = match config_file_name {
1372            Some(value) => value.into().to_string_lossy().to_string(),
1373            None => {
1374                let mut default_config_file_name = home::home_dir()
1375                    .ok_or(ConfigError::NotFound("Config Path".to_string()))?
1376                    .join("cashu-rs-mint");
1377                default_config_file_name.push("config.toml");
1378                default_config_file_name.to_string_lossy().to_string()
1379            }
1380        };
1381        let builder = Config::builder();
1382        let config: Config = builder
1383            // use defaults
1384            .add_source(Config::try_from(default)?)
1385            // override with file contents
1386            .add_source(File::with_name(&config))
1387            .build()?;
1388        Self::deserialize_configuration(config, &[])
1389    }
1390
1391    pub(crate) fn enabled_signatory(&self) -> Option<&Signatory> {
1392        self.signatory
1393            .as_ref()
1394            .filter(|signatory| signatory.enabled)
1395    }
1396}
1397
1398#[cfg(test)]
1399mod tests {
1400
1401    use super::*;
1402
1403    #[test]
1404    fn postgres_config_debug_redacts_connection_credentials() {
1405        let secret = "postgres-password-secret";
1406        let url = format!("postgres://mint:{secret}@db.example.com/cdk?token={secret}");
1407        let database = Database {
1408            engine: DatabaseEngine::Postgres,
1409            postgres: Some(PostgresConfig {
1410                url: url.clone(),
1411                ..Default::default()
1412            }),
1413        };
1414        let auth_database = AuthDatabase {
1415            postgres: Some(PostgresAuthConfig {
1416                url,
1417                ..Default::default()
1418            }),
1419        };
1420
1421        let database_debug = format!("{database:?}");
1422        let auth_debug = format!("{auth_database:?}");
1423
1424        assert!(database_debug.contains("postgres://db.example.com/cdk"));
1425        assert!(auth_debug.contains("postgres://db.example.com/cdk"));
1426        assert!(!database_debug.contains(secret));
1427        assert!(!auth_debug.contains(secret));
1428    }
1429
1430    #[cfg(feature = "management-rpc")]
1431    #[test]
1432    fn mint_quote_payment_override_is_explicitly_opt_in() {
1433        let default_settings =
1434            Settings::try_from_toml("").expect("default settings should deserialize");
1435        assert!(
1436            !default_settings
1437                .mint_management_rpc
1438                .unwrap_or_default()
1439                .allow_mint_quote_payment_override
1440        );
1441
1442        let enabled_settings = Settings::try_from_toml(
1443            r#"
1444[mint_management_rpc]
1445allow_mint_quote_payment_override = true
1446"#,
1447        )
1448        .expect("management RPC payment override should deserialize");
1449        assert!(
1450            enabled_settings
1451                .mint_management_rpc
1452                .expect("management RPC settings should be present")
1453                .allow_mint_quote_payment_override
1454        );
1455    }
1456
1457    #[cfg(feature = "ldk-node")]
1458    #[test]
1459    fn ldk_node_backend_accepts_supported_spellings() {
1460        for backend in ["ldk-node", "ldknode"] {
1461            let document = format!(
1462                r#"
1463[[payment_backend]]
1464backend = "{backend}"
1465unit = "sat"
1466"#
1467            );
1468
1469            let settings =
1470                Settings::try_from_toml(&document).expect("LDK Node backend should deserialize");
1471
1472            assert_eq!(
1473                settings
1474                    .payment_backend
1475                    .first()
1476                    .expect("config should contain one payment backend")
1477                    .backend,
1478                PaymentBackendType::LdkNode
1479            );
1480        }
1481    }
1482
1483    #[test]
1484    fn toml_parser_rejects_unknown_fields_with_full_paths() {
1485        let error = Settings::try_from_toml(
1486            r#"
1487[info]
1488listen_por = 8085
1489
1490[database]
1491engin = "sqlite"
1492"#,
1493        )
1494        .expect_err("misspelled fields must be rejected");
1495        let message = error.to_string();
1496        assert!(message.contains("database.engin"));
1497        assert!(message.contains("info.listen_por"));
1498    }
1499
1500    #[test]
1501    fn toml_parser_reports_legacy_ln_migration() {
1502        let error = Settings::try_from_toml(
1503            r#"
1504[ln]
1505ln_backend = "fakewallet"
1506"#,
1507        )
1508        .expect_err("legacy [ln] configuration must be rejected");
1509        let message = error.to_string();
1510        assert!(message.contains("legacy `[ln]`/`[[ln]]` configuration must be migrated"));
1511        assert!(message.contains("`[payment_backend]`/`[[payment_backend]]`"));
1512        assert!(message.contains("`cdk-mintd config migrate --file <old> --output <new>`"));
1513    }
1514
1515    #[test]
1516    fn legacy_parser_allowlist_does_not_hide_unrelated_typos() {
1517        let error = Settings::try_from_toml_allowing(
1518            r#"
1519[info]
1520signatory_url = "http://127.0.0.1:10009"
1521listen_por = 8085
1522"#,
1523            &["info.signatory_url"],
1524        )
1525        .expect_err("only exact legacy fields may be ignored");
1526        let message = error.to_string();
1527        assert!(message.contains("info.listen_por"));
1528        assert!(!message.contains("info.signatory_url"));
1529    }
1530
1531    fn config_env_lock() -> std::sync::MutexGuard<'static, ()> {
1532        // Share the single process-wide env lock with the rest of the crate's
1533        // tests. `std::env` is global, so config.rs and lib.rs tests must
1534        // serialize on the *same* mutex or they race over env vars.
1535        crate::test_utils::env_lock()
1536    }
1537
1538    #[cfg(feature = "bdk")]
1539    fn clear_bdk_env_vars() {
1540        std::env::remove_var(crate::env_vars::BDK_MNEMONIC_ENV_VAR);
1541        std::env::remove_var(crate::env_vars::BDK_NETWORK_ENV_VAR);
1542        std::env::remove_var(crate::env_vars::BDK_CHAIN_SOURCE_TYPE_ENV_VAR);
1543        std::env::remove_var(crate::env_vars::BDK_ESPLORA_URL_ENV_VAR);
1544        std::env::remove_var(crate::env_vars::BDK_ELECTRUM_URL_ENV_VAR);
1545        std::env::remove_var(crate::env_vars::BDK_ELECTRUM_BATCH_SIZE_ENV_VAR);
1546        std::env::remove_var(crate::env_vars::BDK_WALLET_RESCAN_FROM_HEIGHT_ENV_VAR);
1547        std::env::remove_var(crate::env_vars::BDK_MIN_SEND_AMOUNT_SAT_ENV_VAR);
1548        std::env::remove_var(crate::env_vars::BDK_TARGET_BLOCK_TIME_SECS_ENV_VAR);
1549        std::env::remove_var(crate::env_vars::BDK_FEE_OPTIONS_ENV_VAR);
1550        std::env::remove_var(crate::env_vars::ENV_ONCHAIN_BACKEND);
1551    }
1552
1553    #[test]
1554    fn test_info_debug_impl() {
1555        // Create a sample Info struct with test data
1556        let info = Info {
1557            url: "http://example.com".to_string(),
1558            listen_host: "127.0.0.1".to_string(),
1559            listen_port: 8080,
1560            mnemonic: Some("test secret mnemonic phrase".to_string()),
1561            input_fee_ppk: Some(100),
1562            ..Default::default()
1563        };
1564
1565        // Convert the Info struct to a debug string
1566        let debug_output = format!("{info:?}");
1567
1568        // Verify the debug output contains expected fields
1569        assert!(debug_output.contains("url: \"http://example.com\""));
1570        assert!(debug_output.contains("listen_host: \"127.0.0.1\""));
1571        assert!(debug_output.contains("listen_port: 8080"));
1572
1573        // The mnemonic should be hashed, not displayed in plaintext
1574        assert!(!debug_output.contains("test secret mnemonic phrase"));
1575        assert!(debug_output.contains("<hashed: "));
1576
1577        assert!(debug_output.contains("input_fee_ppk: Some(100)"));
1578    }
1579
1580    #[test]
1581    fn test_info_debug_with_empty_mnemonic() {
1582        // Test with an empty mnemonic to ensure it doesn't panic
1583        let info = Info {
1584            url: "http://example.com".to_string(),
1585            listen_host: "127.0.0.1".to_string(),
1586            listen_port: 8080,
1587            mnemonic: Some("".to_string()), // Empty mnemonic
1588            ..Default::default()
1589        };
1590
1591        // This should not panic
1592        let debug_output = format!("{:?}", info);
1593
1594        // The empty mnemonic should still be hashed
1595        assert!(debug_output.contains("<hashed: "));
1596    }
1597
1598    #[cfg(feature = "bdk")]
1599    #[test]
1600    fn test_bdk_default_min_send_amount_sat() {
1601        assert_eq!(Bdk::default().min_send_amount_sat, 546);
1602    }
1603
1604    #[cfg(feature = "bdk")]
1605    #[test]
1606    fn test_bdk_default_electrum_batch_size() {
1607        assert_eq!(Bdk::default().electrum_batch_size, 5);
1608    }
1609
1610    #[cfg(feature = "bdk")]
1611    #[test]
1612    fn test_bdk_debug_redacts_secrets_and_url_credentials() {
1613        let config = Bdk {
1614            network: Some("regtest".to_string()),
1615            esplora_url: Some(
1616                "https://esplora-user:esplora-secret@example.com/esplora".to_string(),
1617            ),
1618            electrum_url: Some("ssl://electrum-user:electrum-secret@example.com:50002".to_string()),
1619            bitcoind_rpc_password: Some("rpc-password-secret".to_string()),
1620            mnemonic: Some("mnemonic-secret-words".to_string()),
1621            ..Default::default()
1622        };
1623
1624        let debug = format!("{config:?}");
1625
1626        assert!(debug.contains("regtest"));
1627        assert!(debug.contains("https://example.com/esplora"));
1628        assert!(debug.contains("ssl://example.com:50002"));
1629        assert!(debug.contains("[REDACTED]"));
1630        assert!(!debug.contains("esplora-user"));
1631        assert!(!debug.contains("esplora-secret"));
1632        assert!(!debug.contains("electrum-user"));
1633        assert!(!debug.contains("electrum-secret"));
1634        assert!(!debug.contains("rpc-password-secret"));
1635        assert!(!debug.contains("mnemonic-secret-words"));
1636    }
1637
1638    #[cfg(feature = "ldk-node")]
1639    #[test]
1640    fn test_ldk_node_debug_redacts_secrets_and_url_credentials() {
1641        let config = LdkNode {
1642            bitcoin_network: Some("regtest".to_string()),
1643            esplora_url: Some(
1644                "https://esplora-user:esplora-secret@example.com/esplora".to_string(),
1645            ),
1646            electrum_url: Some("ssl://electrum-user:electrum-secret@example.com:50002".to_string()),
1647            bitcoind_rpc_password: Some("rpc-password-secret".to_string()),
1648            rgs_url: Some("https://rgs-user:rgs-secret@example.com/snapshot".to_string()),
1649            ldk_node_mnemonic: Some("mnemonic-secret-words".to_string()),
1650            ..Default::default()
1651        };
1652
1653        let debug = format!("{config:?}");
1654
1655        assert!(debug.contains("regtest"));
1656        assert!(debug.contains("https://example.com/esplora"));
1657        assert!(debug.contains("ssl://example.com:50002"));
1658        assert!(debug.contains("https://example.com/snapshot"));
1659        assert!(debug.contains("[REDACTED]"));
1660        assert!(!debug.contains("esplora-user"));
1661        assert!(!debug.contains("esplora-secret"));
1662        assert!(!debug.contains("electrum-user"));
1663        assert!(!debug.contains("electrum-secret"));
1664        assert!(!debug.contains("rgs-user"));
1665        assert!(!debug.contains("rgs-secret"));
1666        assert!(!debug.contains("rpc-password-secret"));
1667        assert!(!debug.contains("mnemonic-secret-words"));
1668    }
1669
1670    #[cfg(feature = "bdk")]
1671    #[test]
1672    fn test_bdk_electrum_toml_config() {
1673        use std::{env, fs};
1674
1675        let temp_dir = env::temp_dir().join("cdk_test_bdk_electrum_config");
1676        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1677        let config_path = temp_dir.join("config.toml");
1678
1679        let config_content = r#"
1680[bdk]
1681network = "regtest"
1682chain_source_type = "electrum"
1683electrum_url = "tcp://127.0.0.1:50001"
1684electrum_batch_size = 11
1685"#;
1686        fs::write(&config_path, config_content).expect("Failed to write config file");
1687
1688        let settings = Settings::try_new(Some(&config_path))
1689            .expect("electrum config should parse successfully");
1690        let bdk = settings.bdk.expect("bdk config should be present");
1691
1692        assert_eq!(bdk.chain_source_type, Some("electrum".to_string()));
1693        assert_eq!(bdk.electrum_url, Some("tcp://127.0.0.1:50001".to_string()));
1694        assert_eq!(bdk.electrum_batch_size, 11);
1695
1696        let _ = fs::remove_dir_all(&temp_dir);
1697    }
1698
1699    #[cfg(feature = "bdk")]
1700    #[test]
1701    fn test_bdk_config_min_send_amount_sat_override() {
1702        use std::{env, fs};
1703
1704        let temp_dir = env::temp_dir().join("cdk_test_bdk_min_send_config");
1705        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1706        let config_path = temp_dir.join("config.toml");
1707
1708        let config_content = r#"
1709[bdk]
1710min_send_amount_sat = 1200
1711"#;
1712        fs::write(&config_path, config_content).expect("Failed to write config file");
1713
1714        let settings = Settings::try_new(Some(&config_path)).expect("config should load");
1715
1716        assert_eq!(
1717            settings
1718                .bdk
1719                .as_ref()
1720                .expect("bdk config should be present")
1721                .min_send_amount_sat,
1722            1200
1723        );
1724
1725        let _ = fs::remove_dir_all(&temp_dir);
1726    }
1727
1728    #[cfg(feature = "bdk")]
1729    #[test]
1730    fn test_bdk_env_min_send_amount_sat_override() {
1731        let _guard = config_env_lock();
1732        clear_bdk_env_vars();
1733        std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1734        std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1735        std::env::set_var(crate::env_vars::BDK_MIN_SEND_AMOUNT_SAT_ENV_VAR, "777");
1736
1737        let mut settings = Settings::default();
1738        settings.from_env().expect("Failed to apply env vars");
1739
1740        assert_eq!(
1741            settings
1742                .bdk
1743                .as_ref()
1744                .expect("bdk config should be present")
1745                .min_send_amount_sat,
1746            777
1747        );
1748
1749        clear_bdk_env_vars();
1750    }
1751
1752    #[cfg(feature = "bdk")]
1753    #[test]
1754    fn test_bdk_env_wallet_rescan_height_override() {
1755        let _guard = config_env_lock();
1756        clear_bdk_env_vars();
1757        std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1758        std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1759        std::env::set_var(
1760            crate::env_vars::BDK_WALLET_RESCAN_FROM_HEIGHT_ENV_VAR,
1761            "850000",
1762        );
1763
1764        let mut settings = Settings::default();
1765        settings.from_env().expect("Failed to apply env vars");
1766
1767        assert_eq!(
1768            settings
1769                .bdk
1770                .as_ref()
1771                .expect("bdk config should be present")
1772                .wallet_rescan_from_height,
1773            Some(850000)
1774        );
1775
1776        clear_bdk_env_vars();
1777    }
1778
1779    #[cfg(feature = "bdk")]
1780    #[test]
1781    fn test_bdk_env_electrum_config() {
1782        let _guard = config_env_lock();
1783        clear_bdk_env_vars();
1784        std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1785        std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1786        std::env::set_var(crate::env_vars::BDK_CHAIN_SOURCE_TYPE_ENV_VAR, "electrum");
1787        std::env::set_var(
1788            crate::env_vars::BDK_ELECTRUM_URL_ENV_VAR,
1789            "tcp://127.0.0.1:50001",
1790        );
1791        std::env::set_var(crate::env_vars::BDK_ELECTRUM_BATCH_SIZE_ENV_VAR, "9");
1792
1793        let mut settings = Settings::default();
1794        settings.from_env().expect("Failed to apply env vars");
1795
1796        let bdk = settings.bdk.expect("bdk config should be present");
1797        assert_eq!(bdk.chain_source_type, Some("electrum".to_string()));
1798        assert_eq!(bdk.electrum_url, Some("tcp://127.0.0.1:50001".to_string()));
1799        assert_eq!(bdk.electrum_batch_size, 9);
1800
1801        clear_bdk_env_vars();
1802    }
1803
1804    #[cfg(feature = "bdk")]
1805    #[test]
1806    fn test_bdk_default_fee_options_immediate_only() {
1807        assert_eq!(
1808            Bdk::default().batch_config.fee_options,
1809            vec!["immediate".to_string()]
1810        );
1811    }
1812
1813    #[cfg(feature = "bdk")]
1814    #[test]
1815    fn test_bdk_default_batch_deadlines_derive_from_target_block_time() {
1816        let batch_config: cdk_bdk::BatchConfig = Bdk::default().batch_config.into();
1817
1818        assert_eq!(
1819            batch_config.target_block_time,
1820            std::time::Duration::from_secs(600)
1821        );
1822        assert_eq!(
1823            batch_config.standard_deadline,
1824            std::time::Duration::from_secs(3600)
1825        );
1826        assert_eq!(
1827            batch_config.economy_deadline,
1828            std::time::Duration::from_secs(86_400)
1829        );
1830        assert_eq!(
1831            batch_config.max_intent_age,
1832            Some(std::time::Duration::from_secs(86_430))
1833        );
1834    }
1835
1836    #[cfg(feature = "bdk")]
1837    #[test]
1838    fn test_bdk_config_fee_options_override() {
1839        use std::{env, fs};
1840
1841        let temp_dir = env::temp_dir().join("cdk_test_bdk_fee_options_config");
1842        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1843        let config_path = temp_dir.join("config.toml");
1844
1845        let config_content = r#"
1846[bdk.batch_config]
1847fee_options = ["immediate", "economy"]
1848"#;
1849        fs::write(&config_path, config_content).expect("Failed to write config file");
1850
1851        let settings = Settings::try_new(Some(&config_path)).expect("config should load");
1852
1853        assert_eq!(
1854            settings
1855                .bdk
1856                .as_ref()
1857                .expect("bdk config should be present")
1858                .batch_config
1859                .fee_options,
1860            vec!["immediate".to_string(), "economy".to_string()]
1861        );
1862
1863        let _ = fs::remove_dir_all(&temp_dir);
1864    }
1865
1866    #[cfg(feature = "bdk")]
1867    #[test]
1868    fn test_bdk_config_target_block_time_derives_deadlines() {
1869        use std::{env, fs};
1870
1871        let temp_dir = env::temp_dir().join("cdk_test_bdk_target_block_time_config");
1872        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1873        let config_path = temp_dir.join("config.toml");
1874
1875        let config_content = r#"
1876[bdk.batch_config]
1877target_block_time_secs = 300
1878"#;
1879        fs::write(&config_path, config_content).expect("Failed to write config file");
1880
1881        let settings = Settings::try_new(Some(&config_path)).expect("config should load");
1882        let batch_config: cdk_bdk::BatchConfig = settings
1883            .bdk
1884            .as_ref()
1885            .expect("bdk config should be present")
1886            .batch_config
1887            .clone()
1888            .into();
1889
1890        assert_eq!(
1891            batch_config.target_block_time,
1892            std::time::Duration::from_secs(300)
1893        );
1894        assert_eq!(
1895            batch_config.standard_deadline,
1896            std::time::Duration::from_secs(1800)
1897        );
1898        assert_eq!(
1899            batch_config.economy_deadline,
1900            std::time::Duration::from_secs(43_200)
1901        );
1902        assert_eq!(
1903            batch_config.max_intent_age,
1904            Some(std::time::Duration::from_secs(43_230))
1905        );
1906
1907        let _ = fs::remove_dir_all(&temp_dir);
1908    }
1909
1910    #[cfg(feature = "bdk")]
1911    #[test]
1912    fn test_bdk_env_fee_options_override() {
1913        let _guard = config_env_lock();
1914        clear_bdk_env_vars();
1915        std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1916        std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1917        std::env::set_var(
1918            crate::env_vars::BDK_FEE_OPTIONS_ENV_VAR,
1919            "immediate,standard,economy",
1920        );
1921
1922        let mut settings = Settings::default();
1923        settings.from_env().expect("Failed to apply env vars");
1924
1925        assert_eq!(
1926            settings
1927                .bdk
1928                .as_ref()
1929                .expect("bdk config should be present")
1930                .batch_config
1931                .fee_options,
1932            vec![
1933                "immediate".to_string(),
1934                "standard".to_string(),
1935                "economy".to_string()
1936            ]
1937        );
1938
1939        clear_bdk_env_vars();
1940    }
1941
1942    #[cfg(feature = "bdk")]
1943    #[test]
1944    fn test_bdk_env_target_block_time_override() {
1945        let _guard = config_env_lock();
1946        clear_bdk_env_vars();
1947        std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1948        std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1949        std::env::set_var(crate::env_vars::BDK_TARGET_BLOCK_TIME_SECS_ENV_VAR, "120");
1950
1951        let mut settings = Settings::default();
1952        settings.from_env().expect("Failed to apply env vars");
1953        let batch_config: cdk_bdk::BatchConfig = settings
1954            .bdk
1955            .as_ref()
1956            .expect("bdk config should be present")
1957            .batch_config
1958            .clone()
1959            .into();
1960
1961        assert_eq!(
1962            batch_config.target_block_time,
1963            std::time::Duration::from_secs(120)
1964        );
1965        assert_eq!(
1966            batch_config.standard_deadline,
1967            std::time::Duration::from_secs(720)
1968        );
1969        assert_eq!(
1970            batch_config.economy_deadline,
1971            std::time::Duration::from_secs(17_280)
1972        );
1973        assert_eq!(
1974            batch_config.max_intent_age,
1975            Some(std::time::Duration::from_secs(17_310))
1976        );
1977
1978        clear_bdk_env_vars();
1979    }
1980
1981    #[cfg(feature = "bdk")]
1982    #[test]
1983    fn test_bdk_invalid_fee_options_rejected() {
1984        for fee_options in [
1985            Vec::new(),
1986            vec!["immediate".to_string(), "immediate".to_string()],
1987            vec!["urgent".to_string()],
1988            vec![
1989                "immediate".to_string(),
1990                "standard".to_string(),
1991                "economy".to_string(),
1992                "immediate".to_string(),
1993            ],
1994        ] {
1995            let bdk = Bdk {
1996                batch_config: BatchConfig {
1997                    fee_options,
1998                    ..BatchConfig::default()
1999                },
2000                ..Default::default()
2001            };
2002
2003            let err = bdk.validate().expect_err("invalid fee options should fail");
2004
2005            assert!(err.contains("fee_options"));
2006        }
2007    }
2008
2009    #[cfg(feature = "bdk")]
2010    #[test]
2011    fn test_bdk_target_block_time_zero_rejected() {
2012        let bdk = Bdk {
2013            batch_config: BatchConfig {
2014                target_block_time_secs: 0,
2015                ..BatchConfig::default()
2016            },
2017            ..Default::default()
2018        };
2019
2020        let err = bdk
2021            .validate()
2022            .expect_err("zero target block time should fail");
2023
2024        assert!(err.contains("target_block_time_secs"));
2025    }
2026
2027    #[cfg(feature = "bdk")]
2028    #[test]
2029    fn test_bdk_min_send_amount_sat_zero_rejected() {
2030        let bdk = Bdk {
2031            min_send_amount_sat: 0,
2032            ..Default::default()
2033        };
2034
2035        let err = bdk.validate().expect_err("zero send minimum should fail");
2036
2037        assert!(err.contains("min_send_amount_sat"));
2038    }
2039
2040    #[cfg(all(feature = "fakewallet", feature = "bdk"))]
2041    #[test]
2042    fn test_fakewallet_backend_with_bdk_onchain_rejected() {
2043        let settings = Settings {
2044            payment_backend: vec![PaymentBackend {
2045                backend: PaymentBackendType::FakeWallet,
2046                ..Default::default()
2047            }],
2048            onchain: Some(Onchain {
2049                onchain_backend: OnchainBackend::Bdk,
2050                ..Default::default()
2051            }),
2052            ..Default::default()
2053        };
2054
2055        let err = settings
2056            .validate_backend_pairing()
2057            .expect_err("fake payment backend with BDK onchain should fail");
2058
2059        assert!(err.contains("fakewallet"));
2060        assert!(err.contains("bdk"));
2061    }
2062
2063    #[cfg(all(feature = "fakewallet", feature = "cln"))]
2064    #[test]
2065    fn test_real_backend_with_fakewallet_onchain_rejected() {
2066        let settings = Settings {
2067            payment_backend: vec![PaymentBackend {
2068                backend: PaymentBackendType::Cln,
2069                ..Default::default()
2070            }],
2071            onchain: Some(Onchain {
2072                onchain_backend: OnchainBackend::FakeWallet,
2073                ..Default::default()
2074            }),
2075            ..Default::default()
2076        };
2077
2078        let err = settings
2079            .validate_backend_pairing()
2080            .expect_err("real payment backend with fake onchain should fail");
2081
2082        assert!(err.contains("fakewallet"));
2083        assert!(err.contains("real payment backend"));
2084    }
2085
2086    #[cfg(feature = "fakewallet")]
2087    #[test]
2088    fn test_fakewallet_backend_with_fakewallet_onchain_accepted() {
2089        let settings = Settings {
2090            payment_backend: vec![PaymentBackend {
2091                backend: PaymentBackendType::FakeWallet,
2092                ..Default::default()
2093            }],
2094            onchain: Some(Onchain {
2095                onchain_backend: OnchainBackend::FakeWallet,
2096                ..Default::default()
2097            }),
2098            ..Default::default()
2099        };
2100
2101        settings
2102            .validate_backend_pairing()
2103            .expect("fake-only backend pairing should pass");
2104    }
2105
2106    #[cfg(feature = "fakewallet")]
2107    #[test]
2108    fn test_fakewallet_backend_with_no_onchain_accepted() {
2109        let settings = Settings {
2110            payment_backend: vec![PaymentBackend {
2111                backend: PaymentBackendType::FakeWallet,
2112                ..Default::default()
2113            }],
2114            onchain: Some(Onchain {
2115                onchain_backend: OnchainBackend::None,
2116                ..Default::default()
2117            }),
2118            ..Default::default()
2119        };
2120
2121        settings
2122            .validate_backend_pairing()
2123            .expect("fake payment backend without onchain should pass");
2124    }
2125
2126    #[cfg(feature = "fakewallet")]
2127    #[test]
2128    fn test_no_payment_backend_with_fakewallet_onchain_accepted() {
2129        let settings = Settings {
2130            payment_backend: vec![PaymentBackend {
2131                backend: PaymentBackendType::None,
2132                ..Default::default()
2133            }],
2134            onchain: Some(Onchain {
2135                onchain_backend: OnchainBackend::FakeWallet,
2136                ..Default::default()
2137            }),
2138            ..Default::default()
2139        };
2140
2141        settings
2142            .validate_backend_pairing()
2143            .expect("fake onchain-only backend pairing should pass");
2144    }
2145
2146    #[cfg(all(feature = "fakewallet", feature = "cln"))]
2147    #[test]
2148    fn test_fakewallet_backend_with_real_backend_rejected() {
2149        let settings = Settings {
2150            payment_backend: vec![
2151                PaymentBackend {
2152                    backend: PaymentBackendType::FakeWallet,
2153                    ..Default::default()
2154                },
2155                PaymentBackend {
2156                    backend: PaymentBackendType::Cln,
2157                    ..Default::default()
2158                },
2159            ],
2160            ..Default::default()
2161        };
2162
2163        let err = settings
2164            .validate_backend_pairing()
2165            .expect_err("fake payment backend combined with real backend should fail");
2166
2167        assert!(err.contains("fakewallet"));
2168        assert!(err.contains("real payment backend"));
2169    }
2170
2171    #[cfg(feature = "fakewallet")]
2172    #[test]
2173    fn test_fakewallet_custom_payment_method_unit_matching() {
2174        let global = FakeWalletCustomPaymentMethod::Method("paypal".to_string());
2175        let usd_only = FakeWalletCustomPaymentMethod::MethodForUnit {
2176            method: "venmo".to_string(),
2177            unit: CurrencyUnit::Usd,
2178        };
2179
2180        assert!(global.applies_to_unit(&CurrencyUnit::Sat));
2181        assert!(global.applies_to_unit(&CurrencyUnit::Usd));
2182        assert!(!usd_only.applies_to_unit(&CurrencyUnit::Sat));
2183        assert!(usd_only.applies_to_unit(&CurrencyUnit::Usd));
2184    }
2185
2186    #[test]
2187    fn test_info_debug_with_special_chars() {
2188        // Test with a mnemonic containing special characters
2189        let info = Info {
2190            url: "http://example.com".to_string(),
2191            listen_host: "127.0.0.1".to_string(),
2192            listen_port: 8080,
2193            mnemonic: Some("特殊字符 !@#$%^&*()".to_string()), // Special characters
2194            ..Default::default()
2195        };
2196
2197        // This should not panic
2198        let debug_output = format!("{:?}", info);
2199
2200        // The mnemonic with special chars should be hashed
2201        assert!(!debug_output.contains("特殊字符 !@#$%^&*()"));
2202        assert!(debug_output.contains("<hashed: "));
2203    }
2204
2205    #[cfg(feature = "fakewallet")]
2206    #[test]
2207    fn test_multi_backend_config_parses() {
2208        use std::{env, fs};
2209
2210        let temp_dir = env::temp_dir().join("cdk_test_multi_backend_config");
2211        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2212        let config_path = temp_dir.join("config.toml");
2213
2214        let config_content = r#"
2215[[payment_backend]]
2216backend = "fakewallet"
2217unit = "sat"
2218min_mint = 1
2219max_mint = 500000
2220min_melt = 1
2221max_melt = 500000
2222
2223[[payment_backend]]
2224backend = "fakewallet"
2225unit = "eur"
2226min_mint = 1
2227max_mint = 1000
2228min_melt = 1
2229max_melt = 1000
2230"#;
2231        fs::write(&config_path, config_content).expect("Failed to write config file");
2232
2233        let settings = Settings::try_new(Some(&config_path)).expect("config should load");
2234
2235        assert_eq!(settings.payment_backend.len(), 2);
2236
2237        assert_eq!(
2238            settings.payment_backend[0].backend,
2239            PaymentBackendType::FakeWallet
2240        );
2241        assert_eq!(settings.payment_backend[0].unit, CurrencyUnit::Sat);
2242        let max_mint_0: u64 = settings.payment_backend[0].max_mint.into();
2243        assert_eq!(max_mint_0, 500_000);
2244
2245        assert_eq!(
2246            settings.payment_backend[1].backend,
2247            PaymentBackendType::FakeWallet
2248        );
2249        assert_eq!(settings.payment_backend[1].unit, CurrencyUnit::Eur);
2250        let max_mint_1: u64 = settings.payment_backend[1].max_mint.into();
2251        assert_eq!(max_mint_1, 1_000);
2252
2253        let _ = fs::remove_dir_all(&temp_dir);
2254    }
2255
2256    #[cfg(feature = "fakewallet")]
2257    #[test]
2258    fn test_single_payment_backend_block_parses() {
2259        use std::{env, fs};
2260
2261        let temp_dir = env::temp_dir().join("cdk_test_single_payment_backend_block");
2262        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2263        let config_path = temp_dir.join("config.toml");
2264
2265        let config_content = r#"
2266[payment_backend]
2267backend = "fakewallet"
2268min_mint = 1
2269max_mint = 500000
2270min_melt = 1
2271max_melt = 500000
2272"#;
2273        fs::write(&config_path, config_content).expect("Failed to write config file");
2274
2275        let settings = Settings::try_new(Some(&config_path)).expect("config should load");
2276
2277        assert_eq!(settings.payment_backend.len(), 1);
2278        assert_eq!(
2279            settings.payment_backend[0].backend,
2280            PaymentBackendType::FakeWallet
2281        );
2282        assert_eq!(settings.payment_backend[0].unit, CurrencyUnit::Sat);
2283
2284        let _ = fs::remove_dir_all(&temp_dir);
2285    }
2286
2287    #[cfg(feature = "fakewallet")]
2288    #[test]
2289    fn test_fakewallet_config_without_supported_units_parses() {
2290        use std::{env, fs};
2291
2292        let temp_dir = env::temp_dir().join("cdk_test_fakewallet_without_supported_units");
2293        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2294        let config_path = temp_dir.join("config.toml");
2295
2296        let config_content = r#"
2297[[payment_backend]]
2298backend = "fakewallet"
2299unit = "sat"
2300min_mint = 100
2301max_mint = 1000000
2302min_melt = 100
2303max_melt = 1000000
2304
2305[fake_wallet]
2306fee_percent = 0.02
2307reserve_fee_min = 1
2308min_delay_time = 1
2309max_delay_time = 3
2310"#;
2311        fs::write(&config_path, config_content).expect("Failed to write config file");
2312
2313        let settings = Settings::try_new(Some(&config_path)).expect("config should parse");
2314
2315        assert_eq!(settings.payment_backend.len(), 1);
2316        assert_eq!(settings.payment_backend[0].unit, CurrencyUnit::Sat);
2317        assert_eq!(
2318            settings
2319                .fake_wallet
2320                .expect("fake wallet section should parse")
2321                .supported_units,
2322            vec![CurrencyUnit::Sat]
2323        );
2324
2325        let _ = fs::remove_dir_all(&temp_dir);
2326    }
2327
2328    /// Test that configuration can be loaded purely from environment variables
2329    /// without requiring a config.toml file with backend sections.
2330    ///
2331    /// This test runs sequentially for all enabled backends to avoid env var interference.
2332    #[test]
2333    fn test_env_var_only_config_all_backends() {
2334        let _guard = config_env_lock();
2335
2336        // Run each backend test sequentially
2337        #[cfg(feature = "lnd")]
2338        test_lnd_env_config();
2339
2340        #[cfg(feature = "cln")]
2341        test_cln_env_config();
2342
2343        #[cfg(feature = "fakewallet")]
2344        test_fakewallet_env_config();
2345
2346        #[cfg(feature = "grpc-processor")]
2347        test_grpc_processor_env_config();
2348
2349        #[cfg(feature = "ldk-node")]
2350        test_ldk_node_env_config();
2351    }
2352
2353    #[cfg(all(feature = "prometheus", feature = "fakewallet"))]
2354    #[test]
2355    fn test_prometheus_toml_config_survives_env_overlay() {
2356        use std::{env, fs};
2357
2358        let _guard = config_env_lock();
2359
2360        env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2361        env::remove_var(crate::env_vars::ENV_PROMETHEUS_ENABLED);
2362        env::remove_var(crate::env_vars::ENV_PROMETHEUS_ADDRESS);
2363        env::remove_var(crate::env_vars::ENV_PROMETHEUS_PORT);
2364
2365        let temp_dir =
2366            env::temp_dir().join(format!("cdk_prometheus_config_{}", std::process::id()));
2367        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2368        let config_path = temp_dir.join("config.toml");
2369
2370        let config_content = r#"
2371[info]
2372url = "http://127.0.0.1:8085"
2373listen_host = "127.0.0.1"
2374listen_port = 8085
2375
2376[payment_backend]
2377backend = "fakewallet"
2378min_mint = 1
2379max_mint = 500000
2380min_melt = 1
2381max_melt = 500000
2382
2383[prometheus]
2384enabled = true
2385address = "0.0.0.0"
2386port = 9090
2387"#;
2388        fs::write(&config_path, config_content).expect("Failed to write config file");
2389
2390        let mut settings = Settings::try_new(Some(&config_path)).expect("config should load");
2391        settings.from_env().expect("Failed to apply env vars");
2392
2393        let prometheus = settings
2394            .prometheus
2395            .as_ref()
2396            .expect("Prometheus config should be loaded from TOML");
2397        assert!(prometheus.enabled);
2398        assert_eq!(prometheus.address.as_deref(), Some("0.0.0.0"));
2399        assert_eq!(prometheus.port, Some(9090));
2400
2401        let _ = fs::remove_dir_all(&temp_dir);
2402    }
2403
2404    #[cfg(feature = "lnd")]
2405    fn test_lnd_env_config() {
2406        use std::path::PathBuf;
2407        use std::{env, fs};
2408
2409        // Create a temporary directory for config file
2410        let temp_dir = env::temp_dir().join("cdk_test_env_vars");
2411        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2412        let config_path = temp_dir.join("config.toml");
2413
2414        // Create a minimal config.toml with backend set but NO [lnd] section
2415        let config_content = r#"
2416[payment_backend]
2417backend = "lnd"
2418min_mint = 1
2419max_mint = 500000
2420min_melt = 1
2421max_melt = 500000
2422"#;
2423        fs::write(&config_path, config_content).expect("Failed to write config file");
2424
2425        // Set environment variables for LND configuration
2426        env::set_var(crate::env_vars::ENV_PAYMENT_BACKEND, "lnd");
2427        env::set_var(crate::env_vars::ENV_LND_ADDRESS, "https://localhost:10009");
2428        env::set_var(crate::env_vars::ENV_LND_CERT_FILE, "/tmp/test_tls.cert");
2429        env::set_var(
2430            crate::env_vars::ENV_LND_MACAROON_FILE,
2431            "/tmp/test_admin.macaroon",
2432        );
2433        env::set_var(crate::env_vars::ENV_LND_FEE_PERCENT, "0.01");
2434        env::set_var(crate::env_vars::ENV_LND_RESERVE_FEE_MIN, "4");
2435
2436        // Load settings and apply environment variables (same as production code)
2437        let mut settings = Settings::try_new(Some(&config_path)).expect("Failed to load config");
2438        settings.from_env().expect("Failed to apply env vars");
2439
2440        // Verify that settings were populated from env vars
2441        assert!(settings.lnd.is_some());
2442        let lnd_config = settings.lnd.as_ref().unwrap();
2443        assert_eq!(lnd_config.address, "https://localhost:10009");
2444        assert_eq!(lnd_config.cert_file, PathBuf::from("/tmp/test_tls.cert"));
2445        assert_eq!(
2446            lnd_config.macaroon_file,
2447            PathBuf::from("/tmp/test_admin.macaroon")
2448        );
2449        assert_eq!(lnd_config.fee_percent, 0.01);
2450        let reserve_fee_u64: u64 = lnd_config.reserve_fee_min.into();
2451        assert_eq!(reserve_fee_u64, 4);
2452
2453        // Cleanup env vars
2454        env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2455        env::remove_var(crate::env_vars::ENV_LND_ADDRESS);
2456        env::remove_var(crate::env_vars::ENV_LND_CERT_FILE);
2457        env::remove_var(crate::env_vars::ENV_LND_MACAROON_FILE);
2458        env::remove_var(crate::env_vars::ENV_LND_FEE_PERCENT);
2459        env::remove_var(crate::env_vars::ENV_LND_RESERVE_FEE_MIN);
2460
2461        // Cleanup test file
2462        let _ = fs::remove_dir_all(&temp_dir);
2463    }
2464
2465    #[cfg(feature = "cln")]
2466    fn test_cln_env_config() {
2467        use std::path::PathBuf;
2468        use std::{env, fs};
2469
2470        // Create a temporary directory for config file
2471        let temp_dir = env::temp_dir().join("cdk_test_env_vars_cln");
2472        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2473        let config_path = temp_dir.join("config.toml");
2474
2475        // Create a minimal config.toml with backend set but NO [cln] section
2476        let config_content = r#"
2477[payment_backend]
2478backend = "cln"
2479min_mint = 1
2480max_mint = 500000
2481min_melt = 1
2482max_melt = 500000
2483"#;
2484        fs::write(&config_path, config_content).expect("Failed to write config file");
2485
2486        // Set environment variables for CLN configuration
2487        env::set_var(crate::env_vars::ENV_PAYMENT_BACKEND, "cln");
2488        env::set_var(crate::env_vars::ENV_CLN_RPC_PATH, "/tmp/lightning-rpc");
2489        env::set_var(crate::env_vars::ENV_CLN_BOLT12, "false");
2490        env::set_var(crate::env_vars::ENV_CLN_FEE_PERCENT, "0.01");
2491        env::set_var(crate::env_vars::ENV_CLN_RESERVE_FEE_MIN, "4");
2492
2493        // Load settings and apply environment variables (same as production code)
2494        let mut settings = Settings::try_new(Some(&config_path)).expect("Failed to load config");
2495        settings.from_env().expect("Failed to apply env vars");
2496
2497        // Verify that settings were populated from env vars
2498        assert!(settings.cln.is_some());
2499        let cln_config = settings.cln.as_ref().unwrap();
2500        assert_eq!(cln_config.rpc_path, PathBuf::from("/tmp/lightning-rpc"));
2501        assert!(!cln_config.bolt12);
2502        assert_eq!(cln_config.fee_percent, 0.01);
2503        let reserve_fee_u64: u64 = cln_config.reserve_fee_min.into();
2504        assert_eq!(reserve_fee_u64, 4);
2505
2506        // Cleanup env vars
2507        env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2508        env::remove_var(crate::env_vars::ENV_CLN_RPC_PATH);
2509        env::remove_var(crate::env_vars::ENV_CLN_BOLT12);
2510        env::remove_var(crate::env_vars::ENV_CLN_FEE_PERCENT);
2511        env::remove_var(crate::env_vars::ENV_CLN_RESERVE_FEE_MIN);
2512
2513        // Cleanup test file
2514        let _ = fs::remove_dir_all(&temp_dir);
2515    }
2516
2517    #[cfg(feature = "fakewallet")]
2518    fn test_fakewallet_env_config() {
2519        use std::{env, fs};
2520
2521        // Create a temporary directory for config file
2522        let temp_dir = env::temp_dir().join("cdk_test_env_vars_fakewallet");
2523        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2524        let config_path = temp_dir.join("config.toml");
2525
2526        // Create a minimal config.toml with backend set but NO [fake_wallet] section
2527        let config_content = r#"
2528[payment_backend]
2529backend = "fakewallet"
2530min_mint = 1
2531max_mint = 500000
2532min_melt = 1
2533max_melt = 500000
2534"#;
2535        fs::write(&config_path, config_content).expect("Failed to write config file");
2536
2537        // Set environment variables for FakeWallet configuration
2538        env::set_var(crate::env_vars::ENV_PAYMENT_BACKEND, "fakewallet");
2539        env::set_var(crate::env_vars::ENV_FAKE_WALLET_SUPPORTED_UNITS, "sat,msat");
2540        env::set_var(crate::env_vars::ENV_FAKE_WALLET_FEE_PERCENT, "0.0");
2541        env::set_var(crate::env_vars::ENV_FAKE_WALLET_RESERVE_FEE_MIN, "0");
2542        env::set_var(
2543            crate::env_vars::ENV_FAKE_WALLET_CUSTOM_PAYMENT_METHODS,
2544            "venmo:msat,cashapp:sat,paypal",
2545        );
2546        env::set_var(crate::env_vars::ENV_FAKE_WALLET_MIN_DELAY, "0");
2547        env::set_var(crate::env_vars::ENV_FAKE_WALLET_MAX_DELAY, "5");
2548
2549        // Load settings and apply environment variables (same as production code)
2550        let mut settings = Settings::try_new(Some(&config_path)).expect("Failed to load config");
2551        settings.from_env().expect("Failed to apply env vars");
2552
2553        // Verify that settings were populated from env vars
2554        assert!(settings.fake_wallet.is_some());
2555        let fakewallet_config = settings.fake_wallet.as_ref().unwrap();
2556        assert_eq!(fakewallet_config.fee_percent, 0.0);
2557        let reserve_fee_u64: u64 = fakewallet_config.reserve_fee_min.into();
2558        assert_eq!(reserve_fee_u64, 0);
2559        assert_eq!(
2560            fakewallet_config.custom_payment_methods,
2561            vec![
2562                FakeWalletCustomPaymentMethod::MethodForUnit {
2563                    method: "venmo".to_string(),
2564                    unit: CurrencyUnit::Msat,
2565                },
2566                FakeWalletCustomPaymentMethod::MethodForUnit {
2567                    method: "cashapp".to_string(),
2568                    unit: CurrencyUnit::Sat,
2569                },
2570                FakeWalletCustomPaymentMethod::Method("paypal".to_string()),
2571            ]
2572        );
2573        assert_eq!(fakewallet_config.min_delay_time, 0);
2574        assert_eq!(fakewallet_config.max_delay_time, 5);
2575        assert_eq!(
2576            settings
2577                .payment_backend
2578                .iter()
2579                .map(|backend| backend.unit.clone())
2580                .collect::<Vec<_>>(),
2581            vec![CurrencyUnit::Sat, CurrencyUnit::Msat]
2582        );
2583
2584        // Cleanup env vars
2585        env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2586        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_SUPPORTED_UNITS);
2587        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_FEE_PERCENT);
2588        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_RESERVE_FEE_MIN);
2589        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_CUSTOM_PAYMENT_METHODS);
2590        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_MIN_DELAY);
2591        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_MAX_DELAY);
2592
2593        // Cleanup test file
2594        let _ = fs::remove_dir_all(&temp_dir);
2595    }
2596
2597    #[cfg(feature = "grpc-processor")]
2598    fn test_grpc_processor_env_config() {
2599        use std::{env, fs};
2600
2601        // Create a temporary directory for config file
2602        let temp_dir = env::temp_dir().join("cdk_test_env_vars_grpc");
2603        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2604        let config_path = temp_dir.join("config.toml");
2605
2606        // Create a minimal config.toml with backend set but NO [grpc_processor] section
2607        let config_content = r#"
2608[payment_backend]
2609backend = "grpcprocessor"
2610min_mint = 1
2611max_mint = 500000
2612min_melt = 1
2613max_melt = 500000
2614"#;
2615        fs::write(&config_path, config_content).expect("Failed to write config file");
2616
2617        // Set environment variables for GRPC Processor configuration
2618        env::set_var(crate::env_vars::ENV_PAYMENT_BACKEND, "grpcprocessor");
2619        env::set_var(
2620            crate::env_vars::ENV_GRPC_PROCESSOR_SUPPORTED_UNITS,
2621            "sat,msat",
2622        );
2623        env::set_var(crate::env_vars::ENV_GRPC_PROCESSOR_ADDRESS, "localhost");
2624        env::set_var(crate::env_vars::ENV_GRPC_PROCESSOR_PORT, "50051");
2625
2626        // Load settings and apply environment variables (same as production code)
2627        let mut settings = Settings::try_new(Some(&config_path)).expect("Failed to load config");
2628        settings.from_env().expect("Failed to apply env vars");
2629
2630        // Verify that settings were populated from env vars
2631        assert!(settings.grpc_processor.is_some());
2632        let grpc_config = settings.grpc_processor.as_ref().unwrap();
2633        assert_eq!(grpc_config.address, "localhost");
2634        assert_eq!(grpc_config.port, 50051);
2635
2636        // Cleanup env vars
2637        env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2638        env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_SUPPORTED_UNITS);
2639        env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_ADDRESS);
2640        env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_PORT);
2641
2642        // Cleanup test file
2643        let _ = fs::remove_dir_all(&temp_dir);
2644    }
2645
2646    #[cfg(feature = "ldk-node")]
2647    fn test_ldk_node_env_config() {
2648        use std::{env, fs};
2649
2650        // Create a temporary directory for config file
2651        let temp_dir = env::temp_dir().join("cdk_test_env_vars_ldk");
2652        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2653        let config_path = temp_dir.join("config.toml");
2654
2655        // Create a minimal config.toml with backend set but NO [ldk_node] section
2656        let config_content = r#"
2657[payment_backend]
2658backend = "ldknode"
2659min_mint = 1
2660max_mint = 500000
2661min_melt = 1
2662max_melt = 500000
2663"#;
2664        fs::write(&config_path, config_content).expect("Failed to write config file");
2665
2666        // Set environment variables for LDK Node configuration
2667        env::set_var(crate::env_vars::ENV_PAYMENT_BACKEND, "ldknode");
2668        env::set_var(crate::env_vars::LDK_NODE_FEE_PERCENT_ENV_VAR, "0.01");
2669        env::set_var(crate::env_vars::LDK_NODE_RESERVE_FEE_MIN_ENV_VAR, "4");
2670        env::set_var(crate::env_vars::LDK_NODE_BITCOIN_NETWORK_ENV_VAR, "regtest");
2671        env::set_var(
2672            crate::env_vars::LDK_NODE_CHAIN_SOURCE_TYPE_ENV_VAR,
2673            "esplora",
2674        );
2675        env::set_var(
2676            crate::env_vars::LDK_NODE_ESPLORA_URL_ENV_VAR,
2677            "http://localhost:3000",
2678        );
2679        env::set_var(
2680            crate::env_vars::LDK_NODE_ELECTRUM_URL_ENV_VAR,
2681            "tcp://localhost:50001",
2682        );
2683        env::set_var(
2684            crate::env_vars::LDK_NODE_STORAGE_DIR_PATH_ENV_VAR,
2685            "/tmp/ldk",
2686        );
2687
2688        // Load settings and apply environment variables (same as production code)
2689        let mut settings = Settings::try_new(Some(&config_path)).expect("Failed to load config");
2690        settings.from_env().expect("Failed to apply env vars");
2691
2692        // Verify that settings were populated from env vars
2693        assert!(settings.ldk_node.is_some());
2694        let ldk_config = settings.ldk_node.as_ref().unwrap();
2695        assert_eq!(ldk_config.fee_percent, 0.01);
2696        let reserve_fee_u64: u64 = ldk_config.reserve_fee_min.into();
2697        assert_eq!(reserve_fee_u64, 4);
2698        assert_eq!(ldk_config.bitcoin_network, Some("regtest".to_string()));
2699        assert_eq!(ldk_config.chain_source_type, Some("esplora".to_string()));
2700        assert_eq!(
2701            ldk_config.esplora_url,
2702            Some("http://localhost:3000".to_string())
2703        );
2704        assert_eq!(
2705            ldk_config.electrum_url,
2706            Some("tcp://localhost:50001".to_string())
2707        );
2708        assert_eq!(ldk_config.storage_dir_path, Some("/tmp/ldk".to_string()));
2709
2710        // Cleanup env vars
2711        env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2712        env::remove_var(crate::env_vars::LDK_NODE_FEE_PERCENT_ENV_VAR);
2713        env::remove_var(crate::env_vars::LDK_NODE_RESERVE_FEE_MIN_ENV_VAR);
2714        env::remove_var(crate::env_vars::LDK_NODE_BITCOIN_NETWORK_ENV_VAR);
2715        env::remove_var(crate::env_vars::LDK_NODE_CHAIN_SOURCE_TYPE_ENV_VAR);
2716        env::remove_var(crate::env_vars::LDK_NODE_ESPLORA_URL_ENV_VAR);
2717        env::remove_var(crate::env_vars::LDK_NODE_ELECTRUM_URL_ENV_VAR);
2718        env::remove_var(crate::env_vars::LDK_NODE_STORAGE_DIR_PATH_ENV_VAR);
2719
2720        // Cleanup test file
2721        let _ = fs::remove_dir_all(&temp_dir);
2722    }
2723}