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: None,
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: None,
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_tls_defaults_preserve_connection_url_policy() {
1405        let input = r#"url = "postgres://localhost/db?sslmode=verify-full""#;
1406        let postgres: PostgresConfig = toml::from_str(input).expect("PostgreSQL config");
1407        let auth: PostgresAuthConfig = toml::from_str(input).expect("auth PostgreSQL config");
1408        assert!(postgres.tls_mode.is_none());
1409        assert!(auth.tls_mode.is_none());
1410
1411        let explicit = format!("{input}\ntls_mode = \"require\"");
1412        let postgres: PostgresConfig =
1413            toml::from_str(&explicit).expect("explicit PostgreSQL config");
1414        let auth: PostgresAuthConfig =
1415            toml::from_str(&explicit).expect("explicit auth PostgreSQL config");
1416        assert_eq!(postgres.tls_mode.as_deref(), Some("require"));
1417        assert_eq!(auth.tls_mode.as_deref(), Some("require"));
1418    }
1419
1420    #[test]
1421    fn postgres_config_debug_redacts_connection_credentials() {
1422        let secret = "postgres-password-secret";
1423        let url = format!("postgres://mint:{secret}@db.example.com/cdk?token={secret}");
1424        let database = Database {
1425            engine: DatabaseEngine::Postgres,
1426            postgres: Some(PostgresConfig {
1427                url: url.clone(),
1428                ..Default::default()
1429            }),
1430        };
1431        let auth_database = AuthDatabase {
1432            postgres: Some(PostgresAuthConfig {
1433                url,
1434                ..Default::default()
1435            }),
1436        };
1437
1438        let database_debug = format!("{database:?}");
1439        let auth_debug = format!("{auth_database:?}");
1440
1441        assert!(database_debug.contains("postgres://db.example.com/cdk"));
1442        assert!(auth_debug.contains("postgres://db.example.com/cdk"));
1443        assert!(!database_debug.contains(secret));
1444        assert!(!auth_debug.contains(secret));
1445    }
1446
1447    #[cfg(feature = "management-rpc")]
1448    #[test]
1449    fn mint_quote_payment_override_is_explicitly_opt_in() {
1450        let default_settings =
1451            Settings::try_from_toml("").expect("default settings should deserialize");
1452        assert!(
1453            !default_settings
1454                .mint_management_rpc
1455                .unwrap_or_default()
1456                .allow_mint_quote_payment_override
1457        );
1458
1459        let enabled_settings = Settings::try_from_toml(
1460            r#"
1461[mint_management_rpc]
1462allow_mint_quote_payment_override = true
1463"#,
1464        )
1465        .expect("management RPC payment override should deserialize");
1466        assert!(
1467            enabled_settings
1468                .mint_management_rpc
1469                .expect("management RPC settings should be present")
1470                .allow_mint_quote_payment_override
1471        );
1472    }
1473
1474    #[cfg(feature = "ldk-node")]
1475    #[test]
1476    fn ldk_node_backend_accepts_supported_spellings() {
1477        for backend in ["ldk-node", "ldknode"] {
1478            let document = format!(
1479                r#"
1480[[payment_backend]]
1481backend = "{backend}"
1482unit = "sat"
1483"#
1484            );
1485
1486            let settings =
1487                Settings::try_from_toml(&document).expect("LDK Node backend should deserialize");
1488
1489            assert_eq!(
1490                settings
1491                    .payment_backend
1492                    .first()
1493                    .expect("config should contain one payment backend")
1494                    .backend,
1495                PaymentBackendType::LdkNode
1496            );
1497        }
1498    }
1499
1500    #[test]
1501    fn toml_parser_rejects_unknown_fields_with_full_paths() {
1502        let error = Settings::try_from_toml(
1503            r#"
1504[info]
1505listen_por = 8085
1506
1507[database]
1508engin = "sqlite"
1509"#,
1510        )
1511        .expect_err("misspelled fields must be rejected");
1512        let message = error.to_string();
1513        assert!(message.contains("database.engin"));
1514        assert!(message.contains("info.listen_por"));
1515    }
1516
1517    #[test]
1518    fn toml_parser_reports_legacy_ln_migration() {
1519        let error = Settings::try_from_toml(
1520            r#"
1521[ln]
1522ln_backend = "fakewallet"
1523"#,
1524        )
1525        .expect_err("legacy [ln] configuration must be rejected");
1526        let message = error.to_string();
1527        assert!(message.contains("legacy `[ln]`/`[[ln]]` configuration must be migrated"));
1528        assert!(message.contains("`[payment_backend]`/`[[payment_backend]]`"));
1529        assert!(message.contains("`cdk-mintd config migrate --file <old> --output <new>`"));
1530    }
1531
1532    #[test]
1533    fn legacy_parser_allowlist_does_not_hide_unrelated_typos() {
1534        let error = Settings::try_from_toml_allowing(
1535            r#"
1536[info]
1537signatory_url = "http://127.0.0.1:10009"
1538listen_por = 8085
1539"#,
1540            &["info.signatory_url"],
1541        )
1542        .expect_err("only exact legacy fields may be ignored");
1543        let message = error.to_string();
1544        assert!(message.contains("info.listen_por"));
1545        assert!(!message.contains("info.signatory_url"));
1546    }
1547
1548    fn config_env_lock() -> std::sync::MutexGuard<'static, ()> {
1549        // Share the single process-wide env lock with the rest of the crate's
1550        // tests. `std::env` is global, so config.rs and lib.rs tests must
1551        // serialize on the *same* mutex or they race over env vars.
1552        crate::test_utils::env_lock()
1553    }
1554
1555    #[cfg(feature = "bdk")]
1556    fn clear_bdk_env_vars() {
1557        std::env::remove_var(crate::env_vars::BDK_MNEMONIC_ENV_VAR);
1558        std::env::remove_var(crate::env_vars::BDK_NETWORK_ENV_VAR);
1559        std::env::remove_var(crate::env_vars::BDK_CHAIN_SOURCE_TYPE_ENV_VAR);
1560        std::env::remove_var(crate::env_vars::BDK_ESPLORA_URL_ENV_VAR);
1561        std::env::remove_var(crate::env_vars::BDK_ELECTRUM_URL_ENV_VAR);
1562        std::env::remove_var(crate::env_vars::BDK_ELECTRUM_BATCH_SIZE_ENV_VAR);
1563        std::env::remove_var(crate::env_vars::BDK_WALLET_RESCAN_FROM_HEIGHT_ENV_VAR);
1564        std::env::remove_var(crate::env_vars::BDK_MIN_SEND_AMOUNT_SAT_ENV_VAR);
1565        std::env::remove_var(crate::env_vars::BDK_TARGET_BLOCK_TIME_SECS_ENV_VAR);
1566        std::env::remove_var(crate::env_vars::BDK_FEE_OPTIONS_ENV_VAR);
1567        std::env::remove_var(crate::env_vars::ENV_ONCHAIN_BACKEND);
1568    }
1569
1570    #[test]
1571    fn test_info_debug_impl() {
1572        // Create a sample Info struct with test data
1573        let info = Info {
1574            url: "http://example.com".to_string(),
1575            listen_host: "127.0.0.1".to_string(),
1576            listen_port: 8080,
1577            mnemonic: Some("test secret mnemonic phrase".to_string()),
1578            input_fee_ppk: Some(100),
1579            ..Default::default()
1580        };
1581
1582        // Convert the Info struct to a debug string
1583        let debug_output = format!("{info:?}");
1584
1585        // Verify the debug output contains expected fields
1586        assert!(debug_output.contains("url: \"http://example.com\""));
1587        assert!(debug_output.contains("listen_host: \"127.0.0.1\""));
1588        assert!(debug_output.contains("listen_port: 8080"));
1589
1590        // The mnemonic should be hashed, not displayed in plaintext
1591        assert!(!debug_output.contains("test secret mnemonic phrase"));
1592        assert!(debug_output.contains("<hashed: "));
1593
1594        assert!(debug_output.contains("input_fee_ppk: Some(100)"));
1595    }
1596
1597    #[test]
1598    fn test_info_debug_with_empty_mnemonic() {
1599        // Test with an empty mnemonic to ensure it doesn't panic
1600        let info = Info {
1601            url: "http://example.com".to_string(),
1602            listen_host: "127.0.0.1".to_string(),
1603            listen_port: 8080,
1604            mnemonic: Some("".to_string()), // Empty mnemonic
1605            ..Default::default()
1606        };
1607
1608        // This should not panic
1609        let debug_output = format!("{:?}", info);
1610
1611        // The empty mnemonic should still be hashed
1612        assert!(debug_output.contains("<hashed: "));
1613    }
1614
1615    #[cfg(feature = "bdk")]
1616    #[test]
1617    fn test_bdk_default_min_send_amount_sat() {
1618        assert_eq!(Bdk::default().min_send_amount_sat, 546);
1619    }
1620
1621    #[cfg(feature = "bdk")]
1622    #[test]
1623    fn test_bdk_default_electrum_batch_size() {
1624        assert_eq!(Bdk::default().electrum_batch_size, 5);
1625    }
1626
1627    #[cfg(feature = "bdk")]
1628    #[test]
1629    fn test_bdk_debug_redacts_secrets_and_url_credentials() {
1630        let config = Bdk {
1631            network: Some("regtest".to_string()),
1632            esplora_url: Some(
1633                "https://esplora-user:esplora-secret@example.com/esplora".to_string(),
1634            ),
1635            electrum_url: Some("ssl://electrum-user:electrum-secret@example.com:50002".to_string()),
1636            bitcoind_rpc_password: Some("rpc-password-secret".to_string()),
1637            mnemonic: Some("mnemonic-secret-words".to_string()),
1638            ..Default::default()
1639        };
1640
1641        let debug = format!("{config:?}");
1642
1643        assert!(debug.contains("regtest"));
1644        assert!(debug.contains("https://example.com/esplora"));
1645        assert!(debug.contains("ssl://example.com:50002"));
1646        assert!(debug.contains("[REDACTED]"));
1647        assert!(!debug.contains("esplora-user"));
1648        assert!(!debug.contains("esplora-secret"));
1649        assert!(!debug.contains("electrum-user"));
1650        assert!(!debug.contains("electrum-secret"));
1651        assert!(!debug.contains("rpc-password-secret"));
1652        assert!(!debug.contains("mnemonic-secret-words"));
1653    }
1654
1655    #[cfg(feature = "ldk-node")]
1656    #[test]
1657    fn test_ldk_node_debug_redacts_secrets_and_url_credentials() {
1658        let config = LdkNode {
1659            bitcoin_network: Some("regtest".to_string()),
1660            esplora_url: Some(
1661                "https://esplora-user:esplora-secret@example.com/esplora".to_string(),
1662            ),
1663            electrum_url: Some("ssl://electrum-user:electrum-secret@example.com:50002".to_string()),
1664            bitcoind_rpc_password: Some("rpc-password-secret".to_string()),
1665            rgs_url: Some("https://rgs-user:rgs-secret@example.com/snapshot".to_string()),
1666            ldk_node_mnemonic: Some("mnemonic-secret-words".to_string()),
1667            ..Default::default()
1668        };
1669
1670        let debug = format!("{config:?}");
1671
1672        assert!(debug.contains("regtest"));
1673        assert!(debug.contains("https://example.com/esplora"));
1674        assert!(debug.contains("ssl://example.com:50002"));
1675        assert!(debug.contains("https://example.com/snapshot"));
1676        assert!(debug.contains("[REDACTED]"));
1677        assert!(!debug.contains("esplora-user"));
1678        assert!(!debug.contains("esplora-secret"));
1679        assert!(!debug.contains("electrum-user"));
1680        assert!(!debug.contains("electrum-secret"));
1681        assert!(!debug.contains("rgs-user"));
1682        assert!(!debug.contains("rgs-secret"));
1683        assert!(!debug.contains("rpc-password-secret"));
1684        assert!(!debug.contains("mnemonic-secret-words"));
1685    }
1686
1687    #[cfg(feature = "bdk")]
1688    #[test]
1689    fn test_bdk_electrum_toml_config() {
1690        use std::{env, fs};
1691
1692        let temp_dir = env::temp_dir().join("cdk_test_bdk_electrum_config");
1693        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1694        let config_path = temp_dir.join("config.toml");
1695
1696        let config_content = r#"
1697[bdk]
1698network = "regtest"
1699chain_source_type = "electrum"
1700electrum_url = "tcp://127.0.0.1:50001"
1701electrum_batch_size = 11
1702"#;
1703        fs::write(&config_path, config_content).expect("Failed to write config file");
1704
1705        let settings = Settings::try_new(Some(&config_path))
1706            .expect("electrum config should parse successfully");
1707        let bdk = settings.bdk.expect("bdk config should be present");
1708
1709        assert_eq!(bdk.chain_source_type, Some("electrum".to_string()));
1710        assert_eq!(bdk.electrum_url, Some("tcp://127.0.0.1:50001".to_string()));
1711        assert_eq!(bdk.electrum_batch_size, 11);
1712
1713        let _ = fs::remove_dir_all(&temp_dir);
1714    }
1715
1716    #[cfg(feature = "bdk")]
1717    #[test]
1718    fn test_bdk_config_min_send_amount_sat_override() {
1719        use std::{env, fs};
1720
1721        let temp_dir = env::temp_dir().join("cdk_test_bdk_min_send_config");
1722        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1723        let config_path = temp_dir.join("config.toml");
1724
1725        let config_content = r#"
1726[bdk]
1727min_send_amount_sat = 1200
1728"#;
1729        fs::write(&config_path, config_content).expect("Failed to write config file");
1730
1731        let settings = Settings::try_new(Some(&config_path)).expect("config should load");
1732
1733        assert_eq!(
1734            settings
1735                .bdk
1736                .as_ref()
1737                .expect("bdk config should be present")
1738                .min_send_amount_sat,
1739            1200
1740        );
1741
1742        let _ = fs::remove_dir_all(&temp_dir);
1743    }
1744
1745    #[cfg(feature = "bdk")]
1746    #[test]
1747    fn test_bdk_env_min_send_amount_sat_override() {
1748        let _guard = config_env_lock();
1749        clear_bdk_env_vars();
1750        std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1751        std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1752        std::env::set_var(crate::env_vars::BDK_MIN_SEND_AMOUNT_SAT_ENV_VAR, "777");
1753
1754        let mut settings = Settings::default();
1755        settings.from_env().expect("Failed to apply env vars");
1756
1757        assert_eq!(
1758            settings
1759                .bdk
1760                .as_ref()
1761                .expect("bdk config should be present")
1762                .min_send_amount_sat,
1763            777
1764        );
1765
1766        clear_bdk_env_vars();
1767    }
1768
1769    #[cfg(feature = "bdk")]
1770    #[test]
1771    fn test_bdk_env_wallet_rescan_height_override() {
1772        let _guard = config_env_lock();
1773        clear_bdk_env_vars();
1774        std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1775        std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1776        std::env::set_var(
1777            crate::env_vars::BDK_WALLET_RESCAN_FROM_HEIGHT_ENV_VAR,
1778            "850000",
1779        );
1780
1781        let mut settings = Settings::default();
1782        settings.from_env().expect("Failed to apply env vars");
1783
1784        assert_eq!(
1785            settings
1786                .bdk
1787                .as_ref()
1788                .expect("bdk config should be present")
1789                .wallet_rescan_from_height,
1790            Some(850000)
1791        );
1792
1793        clear_bdk_env_vars();
1794    }
1795
1796    #[cfg(feature = "bdk")]
1797    #[test]
1798    fn test_bdk_env_electrum_config() {
1799        let _guard = config_env_lock();
1800        clear_bdk_env_vars();
1801        std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1802        std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1803        std::env::set_var(crate::env_vars::BDK_CHAIN_SOURCE_TYPE_ENV_VAR, "electrum");
1804        std::env::set_var(
1805            crate::env_vars::BDK_ELECTRUM_URL_ENV_VAR,
1806            "tcp://127.0.0.1:50001",
1807        );
1808        std::env::set_var(crate::env_vars::BDK_ELECTRUM_BATCH_SIZE_ENV_VAR, "9");
1809
1810        let mut settings = Settings::default();
1811        settings.from_env().expect("Failed to apply env vars");
1812
1813        let bdk = settings.bdk.expect("bdk config should be present");
1814        assert_eq!(bdk.chain_source_type, Some("electrum".to_string()));
1815        assert_eq!(bdk.electrum_url, Some("tcp://127.0.0.1:50001".to_string()));
1816        assert_eq!(bdk.electrum_batch_size, 9);
1817
1818        clear_bdk_env_vars();
1819    }
1820
1821    #[cfg(feature = "bdk")]
1822    #[test]
1823    fn test_bdk_default_fee_options_immediate_only() {
1824        assert_eq!(
1825            Bdk::default().batch_config.fee_options,
1826            vec!["immediate".to_string()]
1827        );
1828    }
1829
1830    #[cfg(feature = "bdk")]
1831    #[test]
1832    fn test_bdk_default_batch_deadlines_derive_from_target_block_time() {
1833        let batch_config: cdk_bdk::BatchConfig = Bdk::default().batch_config.into();
1834
1835        assert_eq!(
1836            batch_config.target_block_time,
1837            std::time::Duration::from_secs(600)
1838        );
1839        assert_eq!(
1840            batch_config.standard_deadline,
1841            std::time::Duration::from_secs(3600)
1842        );
1843        assert_eq!(
1844            batch_config.economy_deadline,
1845            std::time::Duration::from_secs(86_400)
1846        );
1847        assert_eq!(
1848            batch_config.max_intent_age,
1849            Some(std::time::Duration::from_secs(86_430))
1850        );
1851    }
1852
1853    #[cfg(feature = "bdk")]
1854    #[test]
1855    fn test_bdk_config_fee_options_override() {
1856        use std::{env, fs};
1857
1858        let temp_dir = env::temp_dir().join("cdk_test_bdk_fee_options_config");
1859        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1860        let config_path = temp_dir.join("config.toml");
1861
1862        let config_content = r#"
1863[bdk.batch_config]
1864fee_options = ["immediate", "economy"]
1865"#;
1866        fs::write(&config_path, config_content).expect("Failed to write config file");
1867
1868        let settings = Settings::try_new(Some(&config_path)).expect("config should load");
1869
1870        assert_eq!(
1871            settings
1872                .bdk
1873                .as_ref()
1874                .expect("bdk config should be present")
1875                .batch_config
1876                .fee_options,
1877            vec!["immediate".to_string(), "economy".to_string()]
1878        );
1879
1880        let _ = fs::remove_dir_all(&temp_dir);
1881    }
1882
1883    #[cfg(feature = "bdk")]
1884    #[test]
1885    fn test_bdk_config_target_block_time_derives_deadlines() {
1886        use std::{env, fs};
1887
1888        let temp_dir = env::temp_dir().join("cdk_test_bdk_target_block_time_config");
1889        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1890        let config_path = temp_dir.join("config.toml");
1891
1892        let config_content = r#"
1893[bdk.batch_config]
1894target_block_time_secs = 300
1895"#;
1896        fs::write(&config_path, config_content).expect("Failed to write config file");
1897
1898        let settings = Settings::try_new(Some(&config_path)).expect("config should load");
1899        let batch_config: cdk_bdk::BatchConfig = settings
1900            .bdk
1901            .as_ref()
1902            .expect("bdk config should be present")
1903            .batch_config
1904            .clone()
1905            .into();
1906
1907        assert_eq!(
1908            batch_config.target_block_time,
1909            std::time::Duration::from_secs(300)
1910        );
1911        assert_eq!(
1912            batch_config.standard_deadline,
1913            std::time::Duration::from_secs(1800)
1914        );
1915        assert_eq!(
1916            batch_config.economy_deadline,
1917            std::time::Duration::from_secs(43_200)
1918        );
1919        assert_eq!(
1920            batch_config.max_intent_age,
1921            Some(std::time::Duration::from_secs(43_230))
1922        );
1923
1924        let _ = fs::remove_dir_all(&temp_dir);
1925    }
1926
1927    #[cfg(feature = "bdk")]
1928    #[test]
1929    fn test_bdk_env_fee_options_override() {
1930        let _guard = config_env_lock();
1931        clear_bdk_env_vars();
1932        std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1933        std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1934        std::env::set_var(
1935            crate::env_vars::BDK_FEE_OPTIONS_ENV_VAR,
1936            "immediate,standard,economy",
1937        );
1938
1939        let mut settings = Settings::default();
1940        settings.from_env().expect("Failed to apply env vars");
1941
1942        assert_eq!(
1943            settings
1944                .bdk
1945                .as_ref()
1946                .expect("bdk config should be present")
1947                .batch_config
1948                .fee_options,
1949            vec![
1950                "immediate".to_string(),
1951                "standard".to_string(),
1952                "economy".to_string()
1953            ]
1954        );
1955
1956        clear_bdk_env_vars();
1957    }
1958
1959    #[cfg(feature = "bdk")]
1960    #[test]
1961    fn test_bdk_env_target_block_time_override() {
1962        let _guard = config_env_lock();
1963        clear_bdk_env_vars();
1964        std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1965        std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1966        std::env::set_var(crate::env_vars::BDK_TARGET_BLOCK_TIME_SECS_ENV_VAR, "120");
1967
1968        let mut settings = Settings::default();
1969        settings.from_env().expect("Failed to apply env vars");
1970        let batch_config: cdk_bdk::BatchConfig = settings
1971            .bdk
1972            .as_ref()
1973            .expect("bdk config should be present")
1974            .batch_config
1975            .clone()
1976            .into();
1977
1978        assert_eq!(
1979            batch_config.target_block_time,
1980            std::time::Duration::from_secs(120)
1981        );
1982        assert_eq!(
1983            batch_config.standard_deadline,
1984            std::time::Duration::from_secs(720)
1985        );
1986        assert_eq!(
1987            batch_config.economy_deadline,
1988            std::time::Duration::from_secs(17_280)
1989        );
1990        assert_eq!(
1991            batch_config.max_intent_age,
1992            Some(std::time::Duration::from_secs(17_310))
1993        );
1994
1995        clear_bdk_env_vars();
1996    }
1997
1998    #[cfg(feature = "bdk")]
1999    #[test]
2000    fn test_bdk_invalid_fee_options_rejected() {
2001        for fee_options in [
2002            Vec::new(),
2003            vec!["immediate".to_string(), "immediate".to_string()],
2004            vec!["urgent".to_string()],
2005            vec![
2006                "immediate".to_string(),
2007                "standard".to_string(),
2008                "economy".to_string(),
2009                "immediate".to_string(),
2010            ],
2011        ] {
2012            let bdk = Bdk {
2013                batch_config: BatchConfig {
2014                    fee_options,
2015                    ..BatchConfig::default()
2016                },
2017                ..Default::default()
2018            };
2019
2020            let err = bdk.validate().expect_err("invalid fee options should fail");
2021
2022            assert!(err.contains("fee_options"));
2023        }
2024    }
2025
2026    #[cfg(feature = "bdk")]
2027    #[test]
2028    fn test_bdk_target_block_time_zero_rejected() {
2029        let bdk = Bdk {
2030            batch_config: BatchConfig {
2031                target_block_time_secs: 0,
2032                ..BatchConfig::default()
2033            },
2034            ..Default::default()
2035        };
2036
2037        let err = bdk
2038            .validate()
2039            .expect_err("zero target block time should fail");
2040
2041        assert!(err.contains("target_block_time_secs"));
2042    }
2043
2044    #[cfg(feature = "bdk")]
2045    #[test]
2046    fn test_bdk_min_send_amount_sat_zero_rejected() {
2047        let bdk = Bdk {
2048            min_send_amount_sat: 0,
2049            ..Default::default()
2050        };
2051
2052        let err = bdk.validate().expect_err("zero send minimum should fail");
2053
2054        assert!(err.contains("min_send_amount_sat"));
2055    }
2056
2057    #[cfg(all(feature = "fakewallet", feature = "bdk"))]
2058    #[test]
2059    fn test_fakewallet_backend_with_bdk_onchain_rejected() {
2060        let settings = Settings {
2061            payment_backend: vec![PaymentBackend {
2062                backend: PaymentBackendType::FakeWallet,
2063                ..Default::default()
2064            }],
2065            onchain: Some(Onchain {
2066                onchain_backend: OnchainBackend::Bdk,
2067                ..Default::default()
2068            }),
2069            ..Default::default()
2070        };
2071
2072        let err = settings
2073            .validate_backend_pairing()
2074            .expect_err("fake payment backend with BDK onchain should fail");
2075
2076        assert!(err.contains("fakewallet"));
2077        assert!(err.contains("bdk"));
2078    }
2079
2080    #[cfg(all(feature = "fakewallet", feature = "cln"))]
2081    #[test]
2082    fn test_real_backend_with_fakewallet_onchain_rejected() {
2083        let settings = Settings {
2084            payment_backend: vec![PaymentBackend {
2085                backend: PaymentBackendType::Cln,
2086                ..Default::default()
2087            }],
2088            onchain: Some(Onchain {
2089                onchain_backend: OnchainBackend::FakeWallet,
2090                ..Default::default()
2091            }),
2092            ..Default::default()
2093        };
2094
2095        let err = settings
2096            .validate_backend_pairing()
2097            .expect_err("real payment backend with fake onchain should fail");
2098
2099        assert!(err.contains("fakewallet"));
2100        assert!(err.contains("real payment backend"));
2101    }
2102
2103    #[cfg(feature = "fakewallet")]
2104    #[test]
2105    fn test_fakewallet_backend_with_fakewallet_onchain_accepted() {
2106        let settings = Settings {
2107            payment_backend: vec![PaymentBackend {
2108                backend: PaymentBackendType::FakeWallet,
2109                ..Default::default()
2110            }],
2111            onchain: Some(Onchain {
2112                onchain_backend: OnchainBackend::FakeWallet,
2113                ..Default::default()
2114            }),
2115            ..Default::default()
2116        };
2117
2118        settings
2119            .validate_backend_pairing()
2120            .expect("fake-only backend pairing should pass");
2121    }
2122
2123    #[cfg(feature = "fakewallet")]
2124    #[test]
2125    fn test_fakewallet_backend_with_no_onchain_accepted() {
2126        let settings = Settings {
2127            payment_backend: vec![PaymentBackend {
2128                backend: PaymentBackendType::FakeWallet,
2129                ..Default::default()
2130            }],
2131            onchain: Some(Onchain {
2132                onchain_backend: OnchainBackend::None,
2133                ..Default::default()
2134            }),
2135            ..Default::default()
2136        };
2137
2138        settings
2139            .validate_backend_pairing()
2140            .expect("fake payment backend without onchain should pass");
2141    }
2142
2143    #[cfg(feature = "fakewallet")]
2144    #[test]
2145    fn test_no_payment_backend_with_fakewallet_onchain_accepted() {
2146        let settings = Settings {
2147            payment_backend: vec![PaymentBackend {
2148                backend: PaymentBackendType::None,
2149                ..Default::default()
2150            }],
2151            onchain: Some(Onchain {
2152                onchain_backend: OnchainBackend::FakeWallet,
2153                ..Default::default()
2154            }),
2155            ..Default::default()
2156        };
2157
2158        settings
2159            .validate_backend_pairing()
2160            .expect("fake onchain-only backend pairing should pass");
2161    }
2162
2163    #[cfg(all(feature = "fakewallet", feature = "cln"))]
2164    #[test]
2165    fn test_fakewallet_backend_with_real_backend_rejected() {
2166        let settings = Settings {
2167            payment_backend: vec![
2168                PaymentBackend {
2169                    backend: PaymentBackendType::FakeWallet,
2170                    ..Default::default()
2171                },
2172                PaymentBackend {
2173                    backend: PaymentBackendType::Cln,
2174                    ..Default::default()
2175                },
2176            ],
2177            ..Default::default()
2178        };
2179
2180        let err = settings
2181            .validate_backend_pairing()
2182            .expect_err("fake payment backend combined with real backend should fail");
2183
2184        assert!(err.contains("fakewallet"));
2185        assert!(err.contains("real payment backend"));
2186    }
2187
2188    #[cfg(feature = "fakewallet")]
2189    #[test]
2190    fn test_fakewallet_custom_payment_method_unit_matching() {
2191        let global = FakeWalletCustomPaymentMethod::Method("paypal".to_string());
2192        let usd_only = FakeWalletCustomPaymentMethod::MethodForUnit {
2193            method: "venmo".to_string(),
2194            unit: CurrencyUnit::Usd,
2195        };
2196
2197        assert!(global.applies_to_unit(&CurrencyUnit::Sat));
2198        assert!(global.applies_to_unit(&CurrencyUnit::Usd));
2199        assert!(!usd_only.applies_to_unit(&CurrencyUnit::Sat));
2200        assert!(usd_only.applies_to_unit(&CurrencyUnit::Usd));
2201    }
2202
2203    #[test]
2204    fn test_info_debug_with_special_chars() {
2205        // Test with a mnemonic containing special characters
2206        let info = Info {
2207            url: "http://example.com".to_string(),
2208            listen_host: "127.0.0.1".to_string(),
2209            listen_port: 8080,
2210            mnemonic: Some("特殊字符 !@#$%^&*()".to_string()), // Special characters
2211            ..Default::default()
2212        };
2213
2214        // This should not panic
2215        let debug_output = format!("{:?}", info);
2216
2217        // The mnemonic with special chars should be hashed
2218        assert!(!debug_output.contains("特殊字符 !@#$%^&*()"));
2219        assert!(debug_output.contains("<hashed: "));
2220    }
2221
2222    #[cfg(feature = "fakewallet")]
2223    #[test]
2224    fn test_multi_backend_config_parses() {
2225        use std::{env, fs};
2226
2227        let temp_dir = env::temp_dir().join("cdk_test_multi_backend_config");
2228        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2229        let config_path = temp_dir.join("config.toml");
2230
2231        let config_content = r#"
2232[[payment_backend]]
2233backend = "fakewallet"
2234unit = "sat"
2235min_mint = 1
2236max_mint = 500000
2237min_melt = 1
2238max_melt = 500000
2239
2240[[payment_backend]]
2241backend = "fakewallet"
2242unit = "eur"
2243min_mint = 1
2244max_mint = 1000
2245min_melt = 1
2246max_melt = 1000
2247"#;
2248        fs::write(&config_path, config_content).expect("Failed to write config file");
2249
2250        let settings = Settings::try_new(Some(&config_path)).expect("config should load");
2251
2252        assert_eq!(settings.payment_backend.len(), 2);
2253
2254        assert_eq!(
2255            settings.payment_backend[0].backend,
2256            PaymentBackendType::FakeWallet
2257        );
2258        assert_eq!(settings.payment_backend[0].unit, CurrencyUnit::Sat);
2259        let max_mint_0: u64 = settings.payment_backend[0].max_mint.into();
2260        assert_eq!(max_mint_0, 500_000);
2261
2262        assert_eq!(
2263            settings.payment_backend[1].backend,
2264            PaymentBackendType::FakeWallet
2265        );
2266        assert_eq!(settings.payment_backend[1].unit, CurrencyUnit::Eur);
2267        let max_mint_1: u64 = settings.payment_backend[1].max_mint.into();
2268        assert_eq!(max_mint_1, 1_000);
2269
2270        let _ = fs::remove_dir_all(&temp_dir);
2271    }
2272
2273    #[cfg(feature = "fakewallet")]
2274    #[test]
2275    fn test_single_payment_backend_block_parses() {
2276        use std::{env, fs};
2277
2278        let temp_dir = env::temp_dir().join("cdk_test_single_payment_backend_block");
2279        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2280        let config_path = temp_dir.join("config.toml");
2281
2282        let config_content = r#"
2283[payment_backend]
2284backend = "fakewallet"
2285min_mint = 1
2286max_mint = 500000
2287min_melt = 1
2288max_melt = 500000
2289"#;
2290        fs::write(&config_path, config_content).expect("Failed to write config file");
2291
2292        let settings = Settings::try_new(Some(&config_path)).expect("config should load");
2293
2294        assert_eq!(settings.payment_backend.len(), 1);
2295        assert_eq!(
2296            settings.payment_backend[0].backend,
2297            PaymentBackendType::FakeWallet
2298        );
2299        assert_eq!(settings.payment_backend[0].unit, CurrencyUnit::Sat);
2300
2301        let _ = fs::remove_dir_all(&temp_dir);
2302    }
2303
2304    #[cfg(feature = "fakewallet")]
2305    #[test]
2306    fn test_fakewallet_config_without_supported_units_parses() {
2307        use std::{env, fs};
2308
2309        let temp_dir = env::temp_dir().join("cdk_test_fakewallet_without_supported_units");
2310        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2311        let config_path = temp_dir.join("config.toml");
2312
2313        let config_content = r#"
2314[[payment_backend]]
2315backend = "fakewallet"
2316unit = "sat"
2317min_mint = 100
2318max_mint = 1000000
2319min_melt = 100
2320max_melt = 1000000
2321
2322[fake_wallet]
2323fee_percent = 0.02
2324reserve_fee_min = 1
2325min_delay_time = 1
2326max_delay_time = 3
2327"#;
2328        fs::write(&config_path, config_content).expect("Failed to write config file");
2329
2330        let settings = Settings::try_new(Some(&config_path)).expect("config should parse");
2331
2332        assert_eq!(settings.payment_backend.len(), 1);
2333        assert_eq!(settings.payment_backend[0].unit, CurrencyUnit::Sat);
2334        assert_eq!(
2335            settings
2336                .fake_wallet
2337                .expect("fake wallet section should parse")
2338                .supported_units,
2339            vec![CurrencyUnit::Sat]
2340        );
2341
2342        let _ = fs::remove_dir_all(&temp_dir);
2343    }
2344
2345    /// Test that configuration can be loaded purely from environment variables
2346    /// without requiring a config.toml file with backend sections.
2347    ///
2348    /// This test runs sequentially for all enabled backends to avoid env var interference.
2349    #[test]
2350    fn test_env_var_only_config_all_backends() {
2351        let _guard = config_env_lock();
2352
2353        // Run each backend test sequentially
2354        #[cfg(feature = "lnd")]
2355        test_lnd_env_config();
2356
2357        #[cfg(feature = "cln")]
2358        test_cln_env_config();
2359
2360        #[cfg(feature = "fakewallet")]
2361        test_fakewallet_env_config();
2362
2363        #[cfg(feature = "grpc-processor")]
2364        test_grpc_processor_env_config();
2365
2366        #[cfg(feature = "ldk-node")]
2367        test_ldk_node_env_config();
2368    }
2369
2370    #[cfg(all(feature = "prometheus", feature = "fakewallet"))]
2371    #[test]
2372    fn test_prometheus_toml_config_survives_env_overlay() {
2373        use std::{env, fs};
2374
2375        let _guard = config_env_lock();
2376
2377        env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2378        env::remove_var(crate::env_vars::ENV_PROMETHEUS_ENABLED);
2379        env::remove_var(crate::env_vars::ENV_PROMETHEUS_ADDRESS);
2380        env::remove_var(crate::env_vars::ENV_PROMETHEUS_PORT);
2381
2382        let temp_dir =
2383            env::temp_dir().join(format!("cdk_prometheus_config_{}", std::process::id()));
2384        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2385        let config_path = temp_dir.join("config.toml");
2386
2387        let config_content = r#"
2388[info]
2389url = "http://127.0.0.1:8085"
2390listen_host = "127.0.0.1"
2391listen_port = 8085
2392
2393[payment_backend]
2394backend = "fakewallet"
2395min_mint = 1
2396max_mint = 500000
2397min_melt = 1
2398max_melt = 500000
2399
2400[prometheus]
2401enabled = true
2402address = "0.0.0.0"
2403port = 9090
2404"#;
2405        fs::write(&config_path, config_content).expect("Failed to write config file");
2406
2407        let mut settings = Settings::try_new(Some(&config_path)).expect("config should load");
2408        settings.from_env().expect("Failed to apply env vars");
2409
2410        let prometheus = settings
2411            .prometheus
2412            .as_ref()
2413            .expect("Prometheus config should be loaded from TOML");
2414        assert!(prometheus.enabled);
2415        assert_eq!(prometheus.address.as_deref(), Some("0.0.0.0"));
2416        assert_eq!(prometheus.port, Some(9090));
2417
2418        let _ = fs::remove_dir_all(&temp_dir);
2419    }
2420
2421    #[cfg(feature = "lnd")]
2422    fn test_lnd_env_config() {
2423        use std::path::PathBuf;
2424        use std::{env, fs};
2425
2426        // Create a temporary directory for config file
2427        let temp_dir = env::temp_dir().join("cdk_test_env_vars");
2428        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2429        let config_path = temp_dir.join("config.toml");
2430
2431        // Create a minimal config.toml with backend set but NO [lnd] section
2432        let config_content = r#"
2433[payment_backend]
2434backend = "lnd"
2435min_mint = 1
2436max_mint = 500000
2437min_melt = 1
2438max_melt = 500000
2439"#;
2440        fs::write(&config_path, config_content).expect("Failed to write config file");
2441
2442        // Set environment variables for LND configuration
2443        env::set_var(crate::env_vars::ENV_PAYMENT_BACKEND, "lnd");
2444        env::set_var(crate::env_vars::ENV_LND_ADDRESS, "https://localhost:10009");
2445        env::set_var(crate::env_vars::ENV_LND_CERT_FILE, "/tmp/test_tls.cert");
2446        env::set_var(
2447            crate::env_vars::ENV_LND_MACAROON_FILE,
2448            "/tmp/test_admin.macaroon",
2449        );
2450        env::set_var(crate::env_vars::ENV_LND_FEE_PERCENT, "0.01");
2451        env::set_var(crate::env_vars::ENV_LND_RESERVE_FEE_MIN, "4");
2452
2453        // Load settings and apply environment variables (same as production code)
2454        let mut settings = Settings::try_new(Some(&config_path)).expect("Failed to load config");
2455        settings.from_env().expect("Failed to apply env vars");
2456
2457        // Verify that settings were populated from env vars
2458        assert!(settings.lnd.is_some());
2459        let lnd_config = settings.lnd.as_ref().unwrap();
2460        assert_eq!(lnd_config.address, "https://localhost:10009");
2461        assert_eq!(lnd_config.cert_file, PathBuf::from("/tmp/test_tls.cert"));
2462        assert_eq!(
2463            lnd_config.macaroon_file,
2464            PathBuf::from("/tmp/test_admin.macaroon")
2465        );
2466        assert_eq!(lnd_config.fee_percent, 0.01);
2467        let reserve_fee_u64: u64 = lnd_config.reserve_fee_min.into();
2468        assert_eq!(reserve_fee_u64, 4);
2469
2470        // Cleanup env vars
2471        env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2472        env::remove_var(crate::env_vars::ENV_LND_ADDRESS);
2473        env::remove_var(crate::env_vars::ENV_LND_CERT_FILE);
2474        env::remove_var(crate::env_vars::ENV_LND_MACAROON_FILE);
2475        env::remove_var(crate::env_vars::ENV_LND_FEE_PERCENT);
2476        env::remove_var(crate::env_vars::ENV_LND_RESERVE_FEE_MIN);
2477
2478        // Cleanup test file
2479        let _ = fs::remove_dir_all(&temp_dir);
2480    }
2481
2482    #[cfg(feature = "cln")]
2483    fn test_cln_env_config() {
2484        use std::path::PathBuf;
2485        use std::{env, fs};
2486
2487        // Create a temporary directory for config file
2488        let temp_dir = env::temp_dir().join("cdk_test_env_vars_cln");
2489        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2490        let config_path = temp_dir.join("config.toml");
2491
2492        // Create a minimal config.toml with backend set but NO [cln] section
2493        let config_content = r#"
2494[payment_backend]
2495backend = "cln"
2496min_mint = 1
2497max_mint = 500000
2498min_melt = 1
2499max_melt = 500000
2500"#;
2501        fs::write(&config_path, config_content).expect("Failed to write config file");
2502
2503        // Set environment variables for CLN configuration
2504        env::set_var(crate::env_vars::ENV_PAYMENT_BACKEND, "cln");
2505        env::set_var(crate::env_vars::ENV_CLN_RPC_PATH, "/tmp/lightning-rpc");
2506        env::set_var(crate::env_vars::ENV_CLN_BOLT12, "false");
2507        env::set_var(crate::env_vars::ENV_CLN_FEE_PERCENT, "0.01");
2508        env::set_var(crate::env_vars::ENV_CLN_RESERVE_FEE_MIN, "4");
2509
2510        // Load settings and apply environment variables (same as production code)
2511        let mut settings = Settings::try_new(Some(&config_path)).expect("Failed to load config");
2512        settings.from_env().expect("Failed to apply env vars");
2513
2514        // Verify that settings were populated from env vars
2515        assert!(settings.cln.is_some());
2516        let cln_config = settings.cln.as_ref().unwrap();
2517        assert_eq!(cln_config.rpc_path, PathBuf::from("/tmp/lightning-rpc"));
2518        assert!(!cln_config.bolt12);
2519        assert_eq!(cln_config.fee_percent, 0.01);
2520        let reserve_fee_u64: u64 = cln_config.reserve_fee_min.into();
2521        assert_eq!(reserve_fee_u64, 4);
2522
2523        // Cleanup env vars
2524        env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2525        env::remove_var(crate::env_vars::ENV_CLN_RPC_PATH);
2526        env::remove_var(crate::env_vars::ENV_CLN_BOLT12);
2527        env::remove_var(crate::env_vars::ENV_CLN_FEE_PERCENT);
2528        env::remove_var(crate::env_vars::ENV_CLN_RESERVE_FEE_MIN);
2529
2530        // Cleanup test file
2531        let _ = fs::remove_dir_all(&temp_dir);
2532    }
2533
2534    #[cfg(feature = "fakewallet")]
2535    fn test_fakewallet_env_config() {
2536        use std::{env, fs};
2537
2538        // Create a temporary directory for config file
2539        let temp_dir = env::temp_dir().join("cdk_test_env_vars_fakewallet");
2540        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2541        let config_path = temp_dir.join("config.toml");
2542
2543        // Create a minimal config.toml with backend set but NO [fake_wallet] section
2544        let config_content = r#"
2545[payment_backend]
2546backend = "fakewallet"
2547min_mint = 1
2548max_mint = 500000
2549min_melt = 1
2550max_melt = 500000
2551"#;
2552        fs::write(&config_path, config_content).expect("Failed to write config file");
2553
2554        // Set environment variables for FakeWallet configuration
2555        env::set_var(crate::env_vars::ENV_PAYMENT_BACKEND, "fakewallet");
2556        env::set_var(crate::env_vars::ENV_FAKE_WALLET_SUPPORTED_UNITS, "sat,msat");
2557        env::set_var(crate::env_vars::ENV_FAKE_WALLET_FEE_PERCENT, "0.0");
2558        env::set_var(crate::env_vars::ENV_FAKE_WALLET_RESERVE_FEE_MIN, "0");
2559        env::set_var(
2560            crate::env_vars::ENV_FAKE_WALLET_CUSTOM_PAYMENT_METHODS,
2561            "venmo:msat,cashapp:sat,paypal",
2562        );
2563        env::set_var(crate::env_vars::ENV_FAKE_WALLET_MIN_DELAY, "0");
2564        env::set_var(crate::env_vars::ENV_FAKE_WALLET_MAX_DELAY, "5");
2565
2566        // Load settings and apply environment variables (same as production code)
2567        let mut settings = Settings::try_new(Some(&config_path)).expect("Failed to load config");
2568        settings.from_env().expect("Failed to apply env vars");
2569
2570        // Verify that settings were populated from env vars
2571        assert!(settings.fake_wallet.is_some());
2572        let fakewallet_config = settings.fake_wallet.as_ref().unwrap();
2573        assert_eq!(fakewallet_config.fee_percent, 0.0);
2574        let reserve_fee_u64: u64 = fakewallet_config.reserve_fee_min.into();
2575        assert_eq!(reserve_fee_u64, 0);
2576        assert_eq!(
2577            fakewallet_config.custom_payment_methods,
2578            vec![
2579                FakeWalletCustomPaymentMethod::MethodForUnit {
2580                    method: "venmo".to_string(),
2581                    unit: CurrencyUnit::Msat,
2582                },
2583                FakeWalletCustomPaymentMethod::MethodForUnit {
2584                    method: "cashapp".to_string(),
2585                    unit: CurrencyUnit::Sat,
2586                },
2587                FakeWalletCustomPaymentMethod::Method("paypal".to_string()),
2588            ]
2589        );
2590        assert_eq!(fakewallet_config.min_delay_time, 0);
2591        assert_eq!(fakewallet_config.max_delay_time, 5);
2592        assert_eq!(
2593            settings
2594                .payment_backend
2595                .iter()
2596                .map(|backend| backend.unit.clone())
2597                .collect::<Vec<_>>(),
2598            vec![CurrencyUnit::Sat, CurrencyUnit::Msat]
2599        );
2600
2601        // Cleanup env vars
2602        env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2603        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_SUPPORTED_UNITS);
2604        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_FEE_PERCENT);
2605        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_RESERVE_FEE_MIN);
2606        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_CUSTOM_PAYMENT_METHODS);
2607        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_MIN_DELAY);
2608        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_MAX_DELAY);
2609
2610        // Cleanup test file
2611        let _ = fs::remove_dir_all(&temp_dir);
2612    }
2613
2614    #[cfg(feature = "grpc-processor")]
2615    fn test_grpc_processor_env_config() {
2616        use std::{env, fs};
2617
2618        // Create a temporary directory for config file
2619        let temp_dir = env::temp_dir().join("cdk_test_env_vars_grpc");
2620        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2621        let config_path = temp_dir.join("config.toml");
2622
2623        // Create a minimal config.toml with backend set but NO [grpc_processor] section
2624        let config_content = r#"
2625[payment_backend]
2626backend = "grpcprocessor"
2627min_mint = 1
2628max_mint = 500000
2629min_melt = 1
2630max_melt = 500000
2631"#;
2632        fs::write(&config_path, config_content).expect("Failed to write config file");
2633
2634        // Set environment variables for GRPC Processor configuration
2635        env::set_var(crate::env_vars::ENV_PAYMENT_BACKEND, "grpcprocessor");
2636        env::set_var(
2637            crate::env_vars::ENV_GRPC_PROCESSOR_SUPPORTED_UNITS,
2638            "sat,msat",
2639        );
2640        env::set_var(crate::env_vars::ENV_GRPC_PROCESSOR_ADDRESS, "localhost");
2641        env::set_var(crate::env_vars::ENV_GRPC_PROCESSOR_PORT, "50051");
2642
2643        // Load settings and apply environment variables (same as production code)
2644        let mut settings = Settings::try_new(Some(&config_path)).expect("Failed to load config");
2645        settings.from_env().expect("Failed to apply env vars");
2646
2647        // Verify that settings were populated from env vars
2648        assert!(settings.grpc_processor.is_some());
2649        let grpc_config = settings.grpc_processor.as_ref().unwrap();
2650        assert_eq!(grpc_config.address, "localhost");
2651        assert_eq!(grpc_config.port, 50051);
2652
2653        // Cleanup env vars
2654        env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2655        env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_SUPPORTED_UNITS);
2656        env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_ADDRESS);
2657        env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_PORT);
2658
2659        // Cleanup test file
2660        let _ = fs::remove_dir_all(&temp_dir);
2661    }
2662
2663    #[cfg(feature = "ldk-node")]
2664    fn test_ldk_node_env_config() {
2665        use std::{env, fs};
2666
2667        // Create a temporary directory for config file
2668        let temp_dir = env::temp_dir().join("cdk_test_env_vars_ldk");
2669        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2670        let config_path = temp_dir.join("config.toml");
2671
2672        // Create a minimal config.toml with backend set but NO [ldk_node] section
2673        let config_content = r#"
2674[payment_backend]
2675backend = "ldknode"
2676min_mint = 1
2677max_mint = 500000
2678min_melt = 1
2679max_melt = 500000
2680"#;
2681        fs::write(&config_path, config_content).expect("Failed to write config file");
2682
2683        // Set environment variables for LDK Node configuration
2684        env::set_var(crate::env_vars::ENV_PAYMENT_BACKEND, "ldknode");
2685        env::set_var(crate::env_vars::LDK_NODE_FEE_PERCENT_ENV_VAR, "0.01");
2686        env::set_var(crate::env_vars::LDK_NODE_RESERVE_FEE_MIN_ENV_VAR, "4");
2687        env::set_var(crate::env_vars::LDK_NODE_BITCOIN_NETWORK_ENV_VAR, "regtest");
2688        env::set_var(
2689            crate::env_vars::LDK_NODE_CHAIN_SOURCE_TYPE_ENV_VAR,
2690            "esplora",
2691        );
2692        env::set_var(
2693            crate::env_vars::LDK_NODE_ESPLORA_URL_ENV_VAR,
2694            "http://localhost:3000",
2695        );
2696        env::set_var(
2697            crate::env_vars::LDK_NODE_ELECTRUM_URL_ENV_VAR,
2698            "tcp://localhost:50001",
2699        );
2700        env::set_var(
2701            crate::env_vars::LDK_NODE_STORAGE_DIR_PATH_ENV_VAR,
2702            "/tmp/ldk",
2703        );
2704
2705        // Load settings and apply environment variables (same as production code)
2706        let mut settings = Settings::try_new(Some(&config_path)).expect("Failed to load config");
2707        settings.from_env().expect("Failed to apply env vars");
2708
2709        // Verify that settings were populated from env vars
2710        assert!(settings.ldk_node.is_some());
2711        let ldk_config = settings.ldk_node.as_ref().unwrap();
2712        assert_eq!(ldk_config.fee_percent, 0.01);
2713        let reserve_fee_u64: u64 = ldk_config.reserve_fee_min.into();
2714        assert_eq!(reserve_fee_u64, 4);
2715        assert_eq!(ldk_config.bitcoin_network, Some("regtest".to_string()));
2716        assert_eq!(ldk_config.chain_source_type, Some("esplora".to_string()));
2717        assert_eq!(
2718            ldk_config.esplora_url,
2719            Some("http://localhost:3000".to_string())
2720        );
2721        assert_eq!(
2722            ldk_config.electrum_url,
2723            Some("tcp://localhost:50001".to_string())
2724        );
2725        assert_eq!(ldk_config.storage_dir_path, Some("/tmp/ldk".to_string()));
2726
2727        // Cleanup env vars
2728        env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2729        env::remove_var(crate::env_vars::LDK_NODE_FEE_PERCENT_ENV_VAR);
2730        env::remove_var(crate::env_vars::LDK_NODE_RESERVE_FEE_MIN_ENV_VAR);
2731        env::remove_var(crate::env_vars::LDK_NODE_BITCOIN_NETWORK_ENV_VAR);
2732        env::remove_var(crate::env_vars::LDK_NODE_CHAIN_SOURCE_TYPE_ENV_VAR);
2733        env::remove_var(crate::env_vars::LDK_NODE_ESPLORA_URL_ENV_VAR);
2734        env::remove_var(crate::env_vars::LDK_NODE_ELECTRUM_URL_ENV_VAR);
2735        env::remove_var(crate::env_vars::LDK_NODE_STORAGE_DIR_PATH_ENV_VAR);
2736
2737        // Cleanup test file
2738        let _ = fs::remove_dir_all(&temp_dir);
2739    }
2740}