cdk_mintd/
config.rs

1use std::path::PathBuf;
2
3use bitcoin::hashes::{sha256, Hash};
4use cdk::nuts::{CurrencyUnit, PublicKey};
5use cdk::Amount;
6use cdk_axum::cache;
7use cdk_common::common::QuoteTTL;
8use config::{Config, ConfigError, File};
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
12#[serde(rename_all = "lowercase")]
13pub enum LoggingOutput {
14    /// Log to stderr only
15    Stderr,
16    /// Log to file only
17    File,
18    /// Log to both stderr and file (default)
19    #[default]
20    Both,
21}
22
23impl std::str::FromStr for LoggingOutput {
24    type Err = String;
25
26    fn from_str(s: &str) -> Result<Self, Self::Err> {
27        match s.to_lowercase().as_str() {
28            "stderr" => Ok(LoggingOutput::Stderr),
29            "file" => Ok(LoggingOutput::File),
30            "both" => Ok(LoggingOutput::Both),
31            _ => Err(format!(
32                "Unknown logging output: {s}. Valid options: stdout, file, both"
33            )),
34        }
35    }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, Default)]
39pub struct LoggingConfig {
40    /// Where to output logs: stdout, file, or both
41    #[serde(default)]
42    pub output: LoggingOutput,
43    /// Log level for console output (when stdout or both)
44    pub console_level: Option<String>,
45    /// Log level for file output (when file or both)
46    pub file_level: Option<String>,
47}
48
49#[derive(Clone, Serialize, Deserialize)]
50pub struct Info {
51    pub url: String,
52    pub listen_host: String,
53    pub listen_port: u16,
54    /// Overrides mnemonic
55    pub seed: Option<String>,
56    pub mnemonic: Option<String>,
57    pub signatory_url: Option<String>,
58    pub signatory_certs: Option<String>,
59    pub input_fee_ppk: Option<u64>,
60
61    pub http_cache: cache::Config,
62
63    /// Logging configuration
64    #[serde(default)]
65    pub logging: LoggingConfig,
66
67    /// When this is set to true, the mint exposes a Swagger UI for it's API at
68    /// `[listen_host]:[listen_port]/swagger-ui`
69    ///
70    /// This requires `mintd` was built with the `swagger` feature flag.
71    pub enable_swagger_ui: Option<bool>,
72
73    /// Optional persisted quote TTL values (seconds) to initialize the database with
74    /// when RPC is disabled or on first-run when RPC is enabled.
75    /// If not provided, defaults are used.
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub quote_ttl: Option<QuoteTTL>,
78}
79
80impl Default for Info {
81    fn default() -> Self {
82        Info {
83            url: String::new(),
84            listen_host: "127.0.0.1".to_string(),
85            listen_port: 8091, // Default to port 8091 instead of 0
86            seed: None,
87            mnemonic: None,
88            signatory_url: None,
89            signatory_certs: None,
90            input_fee_ppk: None,
91            http_cache: cache::Config::default(),
92            enable_swagger_ui: None,
93            logging: LoggingConfig::default(),
94            quote_ttl: None,
95        }
96    }
97}
98
99impl std::fmt::Debug for Info {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        // Use a fallback approach that won't panic
102        let mnemonic_display: String = {
103            if let Some(mnemonic) = self.mnemonic.as_ref() {
104                let hash = sha256::Hash::hash(mnemonic.as_bytes());
105                format!("<hashed: {hash}>")
106            } else {
107                format!("<url: {}>", self.signatory_url.clone().unwrap_or_default())
108            }
109        };
110
111        f.debug_struct("Info")
112            .field("url", &self.url)
113            .field("listen_host", &self.listen_host)
114            .field("listen_port", &self.listen_port)
115            .field("mnemonic", &mnemonic_display)
116            .field("input_fee_ppk", &self.input_fee_ppk)
117            .field("http_cache", &self.http_cache)
118            .field("logging", &self.logging)
119            .field("enable_swagger_ui", &self.enable_swagger_ui)
120            .finish()
121    }
122}
123
124#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
125#[serde(rename_all = "lowercase")]
126pub enum LnBackend {
127    #[default]
128    None,
129    #[cfg(feature = "cln")]
130    Cln,
131    #[cfg(feature = "lnbits")]
132    LNbits,
133    #[cfg(feature = "fakewallet")]
134    FakeWallet,
135    #[cfg(feature = "lnd")]
136    Lnd,
137    #[cfg(feature = "ldk-node")]
138    LdkNode,
139    #[cfg(feature = "grpc-processor")]
140    GrpcProcessor,
141}
142
143impl std::str::FromStr for LnBackend {
144    type Err = String;
145
146    fn from_str(s: &str) -> Result<Self, Self::Err> {
147        match s.to_lowercase().as_str() {
148            #[cfg(feature = "cln")]
149            "cln" => Ok(LnBackend::Cln),
150            #[cfg(feature = "lnbits")]
151            "lnbits" => Ok(LnBackend::LNbits),
152            #[cfg(feature = "fakewallet")]
153            "fakewallet" => Ok(LnBackend::FakeWallet),
154            #[cfg(feature = "lnd")]
155            "lnd" => Ok(LnBackend::Lnd),
156            #[cfg(feature = "ldk-node")]
157            "ldk-node" | "ldknode" => Ok(LnBackend::LdkNode),
158            #[cfg(feature = "grpc-processor")]
159            "grpcprocessor" => Ok(LnBackend::GrpcProcessor),
160            _ => Err(format!("Unknown Lightning backend: {s}")),
161        }
162    }
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct Ln {
167    pub ln_backend: LnBackend,
168    pub invoice_description: Option<String>,
169    pub min_mint: Amount,
170    pub max_mint: Amount,
171    pub min_melt: Amount,
172    pub max_melt: Amount,
173}
174
175impl Default for Ln {
176    fn default() -> Self {
177        Ln {
178            ln_backend: LnBackend::default(),
179            invoice_description: None,
180            min_mint: 1.into(),
181            max_mint: 500_000.into(),
182            min_melt: 1.into(),
183            max_melt: 500_000.into(),
184        }
185    }
186}
187
188#[cfg(feature = "lnbits")]
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct LNbits {
191    pub admin_api_key: String,
192    pub invoice_api_key: String,
193    pub lnbits_api: String,
194    #[serde(default = "default_fee_percent")]
195    pub fee_percent: f32,
196    #[serde(default = "default_reserve_fee_min")]
197    pub reserve_fee_min: Amount,
198}
199
200#[cfg(feature = "lnbits")]
201impl Default for LNbits {
202    fn default() -> Self {
203        Self {
204            admin_api_key: String::new(),
205            invoice_api_key: String::new(),
206            lnbits_api: String::new(),
207            fee_percent: 0.02,
208            reserve_fee_min: 2.into(),
209        }
210    }
211}
212
213#[cfg(feature = "cln")]
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct Cln {
216    pub rpc_path: PathBuf,
217    #[serde(default = "default_cln_bolt12")]
218    pub bolt12: bool,
219    #[serde(default = "default_fee_percent")]
220    pub fee_percent: f32,
221    #[serde(default = "default_reserve_fee_min")]
222    pub reserve_fee_min: Amount,
223}
224
225#[cfg(feature = "cln")]
226impl Default for Cln {
227    fn default() -> Self {
228        Self {
229            rpc_path: PathBuf::new(),
230            bolt12: true,
231            fee_percent: 0.02,
232            reserve_fee_min: 2.into(),
233        }
234    }
235}
236
237#[cfg(feature = "cln")]
238fn default_cln_bolt12() -> bool {
239    true
240}
241
242#[cfg(feature = "lnd")]
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct Lnd {
245    pub address: String,
246    pub cert_file: PathBuf,
247    pub macaroon_file: PathBuf,
248    #[serde(default = "default_fee_percent")]
249    pub fee_percent: f32,
250    #[serde(default = "default_reserve_fee_min")]
251    pub reserve_fee_min: Amount,
252}
253
254#[cfg(feature = "lnd")]
255impl Default for Lnd {
256    fn default() -> Self {
257        Self {
258            address: String::new(),
259            cert_file: PathBuf::new(),
260            macaroon_file: PathBuf::new(),
261            fee_percent: 0.02,
262            reserve_fee_min: 2.into(),
263        }
264    }
265}
266
267#[cfg(feature = "ldk-node")]
268#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct LdkNode {
270    /// Fee percentage (e.g., 0.02 for 2%)
271    #[serde(default = "default_ldk_fee_percent")]
272    pub fee_percent: f32,
273    /// Minimum reserve fee
274    #[serde(default = "default_ldk_reserve_fee_min")]
275    pub reserve_fee_min: Amount,
276    /// Bitcoin network (mainnet, testnet, signet, regtest)
277    pub bitcoin_network: Option<String>,
278    /// Chain source type (esplora or bitcoinrpc)
279    pub chain_source_type: Option<String>,
280    /// Esplora URL (when chain_source_type = "esplora")
281    pub esplora_url: Option<String>,
282    /// Bitcoin RPC configuration (when chain_source_type = "bitcoinrpc")
283    pub bitcoind_rpc_host: Option<String>,
284    pub bitcoind_rpc_port: Option<u16>,
285    pub bitcoind_rpc_user: Option<String>,
286    pub bitcoind_rpc_password: Option<String>,
287    /// Storage directory path
288    pub storage_dir_path: Option<String>,
289    /// LDK node listening host
290    pub ldk_node_host: Option<String>,
291    /// LDK node listening port
292    pub ldk_node_port: Option<u16>,
293    /// Gossip source type (p2p or rgs)
294    pub gossip_source_type: Option<String>,
295    /// Rapid Gossip Sync URL (when gossip_source_type = "rgs")
296    pub rgs_url: Option<String>,
297    /// Webserver host (defaults to 127.0.0.1)
298    #[serde(default = "default_webserver_host")]
299    pub webserver_host: Option<String>,
300    /// Webserver port
301    #[serde(default = "default_webserver_port")]
302    pub webserver_port: Option<u16>,
303}
304
305#[cfg(feature = "ldk-node")]
306impl Default for LdkNode {
307    fn default() -> Self {
308        Self {
309            fee_percent: default_ldk_fee_percent(),
310            reserve_fee_min: default_ldk_reserve_fee_min(),
311            bitcoin_network: None,
312            chain_source_type: None,
313            esplora_url: None,
314            bitcoind_rpc_host: None,
315            bitcoind_rpc_port: None,
316            bitcoind_rpc_user: None,
317            bitcoind_rpc_password: None,
318            storage_dir_path: None,
319            ldk_node_host: None,
320            ldk_node_port: None,
321            gossip_source_type: None,
322            rgs_url: None,
323            webserver_host: default_webserver_host(),
324            webserver_port: default_webserver_port(),
325        }
326    }
327}
328
329#[cfg(feature = "ldk-node")]
330fn default_ldk_fee_percent() -> f32 {
331    0.04
332}
333
334#[cfg(feature = "ldk-node")]
335fn default_ldk_reserve_fee_min() -> Amount {
336    4.into()
337}
338
339#[cfg(feature = "ldk-node")]
340fn default_webserver_host() -> Option<String> {
341    Some("127.0.0.1".to_string())
342}
343
344#[cfg(feature = "ldk-node")]
345fn default_webserver_port() -> Option<u16> {
346    Some(8091)
347}
348
349#[cfg(feature = "fakewallet")]
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct FakeWallet {
352    pub supported_units: Vec<CurrencyUnit>,
353    pub fee_percent: f32,
354    pub reserve_fee_min: Amount,
355    #[serde(default = "default_min_delay_time")]
356    pub min_delay_time: u64,
357    #[serde(default = "default_max_delay_time")]
358    pub max_delay_time: u64,
359}
360
361#[cfg(feature = "fakewallet")]
362impl Default for FakeWallet {
363    fn default() -> Self {
364        Self {
365            supported_units: vec![CurrencyUnit::Sat],
366            fee_percent: 0.02,
367            reserve_fee_min: 2.into(),
368            min_delay_time: 1,
369            max_delay_time: 3,
370        }
371    }
372}
373
374// Helper functions to provide default values
375// Common fee defaults for all backends
376fn default_fee_percent() -> f32 {
377    0.02
378}
379
380fn default_reserve_fee_min() -> Amount {
381    2.into()
382}
383
384#[cfg(feature = "fakewallet")]
385fn default_min_delay_time() -> u64 {
386    1
387}
388
389#[cfg(feature = "fakewallet")]
390fn default_max_delay_time() -> u64 {
391    3
392}
393
394#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
395pub struct GrpcProcessor {
396    #[serde(default)]
397    pub supported_units: Vec<CurrencyUnit>,
398    #[serde(default = "default_grpc_addr")]
399    pub addr: String,
400    #[serde(default = "default_grpc_port")]
401    pub port: u16,
402    #[serde(default)]
403    pub tls_dir: Option<PathBuf>,
404}
405
406impl Default for GrpcProcessor {
407    fn default() -> Self {
408        Self {
409            supported_units: Vec::new(),
410            addr: default_grpc_addr(),
411            port: default_grpc_port(),
412            tls_dir: None,
413        }
414    }
415}
416
417fn default_grpc_addr() -> String {
418    "127.0.0.1".to_string()
419}
420
421fn default_grpc_port() -> u16 {
422    50051
423}
424
425#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
426#[serde(rename_all = "lowercase")]
427pub enum DatabaseEngine {
428    #[default]
429    Sqlite,
430    Postgres,
431}
432
433impl std::str::FromStr for DatabaseEngine {
434    type Err = String;
435
436    fn from_str(s: &str) -> Result<Self, Self::Err> {
437        match s.to_lowercase().as_str() {
438            "sqlite" => Ok(DatabaseEngine::Sqlite),
439            "postgres" => Ok(DatabaseEngine::Postgres),
440            _ => Err(format!("Unknown database engine: {s}")),
441        }
442    }
443}
444
445#[derive(Debug, Clone, Serialize, Deserialize, Default)]
446pub struct Database {
447    pub engine: DatabaseEngine,
448    pub postgres: Option<PostgresConfig>,
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize, Default)]
452pub struct AuthDatabase {
453    pub postgres: Option<PostgresAuthConfig>,
454}
455
456#[derive(Debug, Clone, Serialize, Deserialize)]
457pub struct PostgresAuthConfig {
458    pub url: String,
459    pub tls_mode: Option<String>,
460    pub max_connections: Option<usize>,
461    pub connection_timeout_seconds: Option<u64>,
462}
463
464impl Default for PostgresAuthConfig {
465    fn default() -> Self {
466        Self {
467            url: String::new(),
468            tls_mode: Some("disable".to_string()),
469            max_connections: Some(20),
470            connection_timeout_seconds: Some(10),
471        }
472    }
473}
474
475#[derive(Debug, Clone, Serialize, Deserialize)]
476pub struct PostgresConfig {
477    pub url: String,
478    pub tls_mode: Option<String>,
479    pub max_connections: Option<usize>,
480    pub connection_timeout_seconds: Option<u64>,
481}
482
483impl Default for PostgresConfig {
484    fn default() -> Self {
485        Self {
486            url: String::new(),
487            tls_mode: Some("disable".to_string()),
488            max_connections: Some(20),
489            connection_timeout_seconds: Some(10),
490        }
491    }
492}
493
494#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
495#[serde(rename_all = "lowercase")]
496pub enum AuthType {
497    Clear,
498    Blind,
499    #[default]
500    None,
501}
502
503impl std::str::FromStr for AuthType {
504    type Err = String;
505
506    fn from_str(s: &str) -> Result<Self, Self::Err> {
507        match s.to_lowercase().as_str() {
508            "clear" => Ok(AuthType::Clear),
509            "blind" => Ok(AuthType::Blind),
510            "none" => Ok(AuthType::None),
511            _ => Err(format!("Unknown auth type: {s}")),
512        }
513    }
514}
515
516#[derive(Debug, Clone, Default, Serialize, Deserialize)]
517pub struct Auth {
518    #[serde(default)]
519    pub auth_enabled: bool,
520    pub openid_discovery: String,
521    pub openid_client_id: String,
522    pub mint_max_bat: u64,
523    #[serde(default = "default_blind")]
524    pub mint: AuthType,
525    #[serde(default)]
526    pub get_mint_quote: AuthType,
527    #[serde(default)]
528    pub check_mint_quote: AuthType,
529    #[serde(default)]
530    pub melt: AuthType,
531    #[serde(default)]
532    pub get_melt_quote: AuthType,
533    #[serde(default)]
534    pub check_melt_quote: AuthType,
535    #[serde(default = "default_blind")]
536    pub swap: AuthType,
537    #[serde(default = "default_blind")]
538    pub restore: AuthType,
539    #[serde(default)]
540    pub check_proof_state: AuthType,
541    /// Enable WebSocket authentication support
542    #[serde(default = "default_blind")]
543    pub websocket_auth: AuthType,
544}
545
546fn default_blind() -> AuthType {
547    AuthType::Blind
548}
549
550/// CDK settings, derived from `config.toml`
551#[derive(Debug, Clone, Serialize, Deserialize, Default)]
552pub struct Settings {
553    pub info: Info,
554    pub mint_info: MintInfo,
555    pub ln: Ln,
556    #[cfg(feature = "cln")]
557    pub cln: Option<Cln>,
558    #[cfg(feature = "lnbits")]
559    pub lnbits: Option<LNbits>,
560    #[cfg(feature = "lnd")]
561    pub lnd: Option<Lnd>,
562    #[cfg(feature = "ldk-node")]
563    pub ldk_node: Option<LdkNode>,
564    #[cfg(feature = "fakewallet")]
565    pub fake_wallet: Option<FakeWallet>,
566    pub grpc_processor: Option<GrpcProcessor>,
567    pub database: Database,
568    #[cfg(feature = "auth")]
569    pub auth_database: Option<AuthDatabase>,
570    #[cfg(feature = "management-rpc")]
571    pub mint_management_rpc: Option<MintManagementRpc>,
572    pub auth: Option<Auth>,
573    #[cfg(feature = "prometheus")]
574    pub prometheus: Option<Prometheus>,
575}
576
577#[derive(Debug, Clone, Serialize, Deserialize, Default)]
578#[cfg(feature = "prometheus")]
579pub struct Prometheus {
580    pub enabled: bool,
581    pub address: Option<String>,
582    pub port: Option<u16>,
583}
584
585#[derive(Debug, Clone, Serialize, Deserialize, Default)]
586pub struct MintInfo {
587    /// name of the mint and should be recognizable
588    pub name: String,
589    /// hex pubkey of the mint
590    pub pubkey: Option<PublicKey>,
591    /// short description of the mint
592    pub description: String,
593    /// long description
594    pub description_long: Option<String>,
595    /// url to the mint icon
596    pub icon_url: Option<String>,
597    /// message of the day that the wallet must display to the user
598    pub motd: Option<String>,
599    /// Nostr publickey
600    pub contact_nostr_public_key: Option<String>,
601    /// Contact email
602    pub contact_email: Option<String>,
603    /// URL to the terms of service
604    pub tos_url: Option<String>,
605}
606
607#[cfg(feature = "management-rpc")]
608#[derive(Debug, Clone, Serialize, Deserialize, Default)]
609pub struct MintManagementRpc {
610    /// When this is set to `true` the mint use the config file for the initial set up on first start.
611    /// Changes to the `[mint_info]` after this **MUST** be made via the RPC changes to the config file or env vars will be ignored.
612    pub enabled: bool,
613    pub address: Option<String>,
614    pub port: Option<u16>,
615    pub tls_dir_path: Option<PathBuf>,
616}
617
618impl Settings {
619    #[must_use]
620    pub fn new<P>(config_file_name: Option<P>) -> Self
621    where
622        P: Into<PathBuf>,
623    {
624        let default_settings = Self::default();
625        // attempt to construct settings with file
626        let from_file = Self::new_from_default(&default_settings, config_file_name);
627        match from_file {
628            Ok(f) => f,
629            Err(e) => {
630                tracing::error!(
631                    "Error reading config file, falling back to defaults. Error: {e:?}"
632                );
633                default_settings
634            }
635        }
636    }
637
638    fn new_from_default<P>(
639        default: &Settings,
640        config_file_name: Option<P>,
641    ) -> Result<Self, ConfigError>
642    where
643        P: Into<PathBuf>,
644    {
645        let mut default_config_file_name = home::home_dir()
646            .ok_or(ConfigError::NotFound("Config Path".to_string()))?
647            .join("cashu-rs-mint");
648
649        default_config_file_name.push("config.toml");
650        let config: String = match config_file_name {
651            Some(value) => value.into().to_string_lossy().to_string(),
652            None => default_config_file_name.to_string_lossy().to_string(),
653        };
654        let builder = Config::builder();
655        let config: Config = builder
656            // use defaults
657            .add_source(Config::try_from(default)?)
658            // override with file contents
659            .add_source(File::with_name(&config))
660            .build()?;
661        let settings: Settings = config.try_deserialize()?;
662
663        Ok(settings)
664    }
665}
666
667#[cfg(test)]
668mod tests {
669
670    use super::*;
671
672    #[test]
673    fn test_info_debug_impl() {
674        // Create a sample Info struct with test data
675        let info = Info {
676            url: "http://example.com".to_string(),
677            listen_host: "127.0.0.1".to_string(),
678            listen_port: 8080,
679            mnemonic: Some("test secret mnemonic phrase".to_string()),
680            input_fee_ppk: Some(100),
681            ..Default::default()
682        };
683
684        // Convert the Info struct to a debug string
685        let debug_output = format!("{info:?}");
686
687        // Verify the debug output contains expected fields
688        assert!(debug_output.contains("url: \"http://example.com\""));
689        assert!(debug_output.contains("listen_host: \"127.0.0.1\""));
690        assert!(debug_output.contains("listen_port: 8080"));
691
692        // The mnemonic should be hashed, not displayed in plaintext
693        assert!(!debug_output.contains("test secret mnemonic phrase"));
694        assert!(debug_output.contains("<hashed: "));
695
696        assert!(debug_output.contains("input_fee_ppk: Some(100)"));
697    }
698
699    #[test]
700    fn test_info_debug_with_empty_mnemonic() {
701        // Test with an empty mnemonic to ensure it doesn't panic
702        let info = Info {
703            url: "http://example.com".to_string(),
704            listen_host: "127.0.0.1".to_string(),
705            listen_port: 8080,
706            mnemonic: Some("".to_string()), // Empty mnemonic
707            enable_swagger_ui: Some(false),
708            ..Default::default()
709        };
710
711        // This should not panic
712        let debug_output = format!("{:?}", info);
713
714        // The empty mnemonic should still be hashed
715        assert!(debug_output.contains("<hashed: "));
716    }
717
718    #[test]
719    fn test_info_debug_with_special_chars() {
720        // Test with a mnemonic containing special characters
721        let info = Info {
722            url: "http://example.com".to_string(),
723            listen_host: "127.0.0.1".to_string(),
724            listen_port: 8080,
725            mnemonic: Some("特殊字符 !@#$%^&*()".to_string()), // Special characters
726            ..Default::default()
727        };
728
729        // This should not panic
730        let debug_output = format!("{:?}", info);
731
732        // The mnemonic with special chars should be hashed
733        assert!(!debug_output.contains("特殊字符 !@#$%^&*()"));
734        assert!(debug_output.contains("<hashed: "));
735    }
736
737    /// Test that configuration can be loaded purely from environment variables
738    /// without requiring a config.toml file with backend sections.
739    ///
740    /// This test runs sequentially for all enabled backends to avoid env var interference.
741    #[test]
742    fn test_env_var_only_config_all_backends() {
743        // Run each backend test sequentially
744        #[cfg(feature = "lnd")]
745        test_lnd_env_config();
746
747        #[cfg(feature = "cln")]
748        test_cln_env_config();
749
750        #[cfg(feature = "lnbits")]
751        test_lnbits_env_config();
752
753        #[cfg(feature = "fakewallet")]
754        test_fakewallet_env_config();
755
756        #[cfg(feature = "grpc-processor")]
757        test_grpc_processor_env_config();
758
759        #[cfg(feature = "ldk-node")]
760        test_ldk_node_env_config();
761    }
762
763    #[cfg(feature = "lnd")]
764    fn test_lnd_env_config() {
765        use std::path::PathBuf;
766        use std::{env, fs};
767
768        // Create a temporary directory for config file
769        let temp_dir = env::temp_dir().join("cdk_test_env_vars");
770        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
771        let config_path = temp_dir.join("config.toml");
772
773        // Create a minimal config.toml with backend set but NO [lnd] section
774        let config_content = r#"
775[ln]
776backend = "lnd"
777min_mint = 1
778max_mint = 500000
779min_melt = 1
780max_melt = 500000
781"#;
782        fs::write(&config_path, config_content).expect("Failed to write config file");
783
784        // Set environment variables for LND configuration
785        env::set_var(crate::env_vars::ENV_LN_BACKEND, "lnd");
786        env::set_var(crate::env_vars::ENV_LND_ADDRESS, "https://localhost:10009");
787        env::set_var(crate::env_vars::ENV_LND_CERT_FILE, "/tmp/test_tls.cert");
788        env::set_var(
789            crate::env_vars::ENV_LND_MACAROON_FILE,
790            "/tmp/test_admin.macaroon",
791        );
792        env::set_var(crate::env_vars::ENV_LND_FEE_PERCENT, "0.01");
793        env::set_var(crate::env_vars::ENV_LND_RESERVE_FEE_MIN, "4");
794
795        // Load settings and apply environment variables (same as production code)
796        let mut settings = Settings::new(Some(&config_path));
797        settings.from_env().expect("Failed to apply env vars");
798
799        // Verify that settings were populated from env vars
800        assert!(settings.lnd.is_some());
801        let lnd_config = settings.lnd.as_ref().unwrap();
802        assert_eq!(lnd_config.address, "https://localhost:10009");
803        assert_eq!(lnd_config.cert_file, PathBuf::from("/tmp/test_tls.cert"));
804        assert_eq!(
805            lnd_config.macaroon_file,
806            PathBuf::from("/tmp/test_admin.macaroon")
807        );
808        assert_eq!(lnd_config.fee_percent, 0.01);
809        let reserve_fee_u64: u64 = lnd_config.reserve_fee_min.into();
810        assert_eq!(reserve_fee_u64, 4);
811
812        // Cleanup env vars
813        env::remove_var(crate::env_vars::ENV_LN_BACKEND);
814        env::remove_var(crate::env_vars::ENV_LND_ADDRESS);
815        env::remove_var(crate::env_vars::ENV_LND_CERT_FILE);
816        env::remove_var(crate::env_vars::ENV_LND_MACAROON_FILE);
817        env::remove_var(crate::env_vars::ENV_LND_FEE_PERCENT);
818        env::remove_var(crate::env_vars::ENV_LND_RESERVE_FEE_MIN);
819
820        // Cleanup test file
821        let _ = fs::remove_dir_all(&temp_dir);
822    }
823
824    #[cfg(feature = "cln")]
825    fn test_cln_env_config() {
826        use std::path::PathBuf;
827        use std::{env, fs};
828
829        // Create a temporary directory for config file
830        let temp_dir = env::temp_dir().join("cdk_test_env_vars_cln");
831        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
832        let config_path = temp_dir.join("config.toml");
833
834        // Create a minimal config.toml with backend set but NO [cln] section
835        let config_content = r#"
836[ln]
837backend = "cln"
838min_mint = 1
839max_mint = 500000
840min_melt = 1
841max_melt = 500000
842"#;
843        fs::write(&config_path, config_content).expect("Failed to write config file");
844
845        // Set environment variables for CLN configuration
846        env::set_var(crate::env_vars::ENV_LN_BACKEND, "cln");
847        env::set_var(crate::env_vars::ENV_CLN_RPC_PATH, "/tmp/lightning-rpc");
848        env::set_var(crate::env_vars::ENV_CLN_BOLT12, "false");
849        env::set_var(crate::env_vars::ENV_CLN_FEE_PERCENT, "0.01");
850        env::set_var(crate::env_vars::ENV_CLN_RESERVE_FEE_MIN, "4");
851
852        // Load settings and apply environment variables (same as production code)
853        let mut settings = Settings::new(Some(&config_path));
854        settings.from_env().expect("Failed to apply env vars");
855
856        // Verify that settings were populated from env vars
857        assert!(settings.cln.is_some());
858        let cln_config = settings.cln.as_ref().unwrap();
859        assert_eq!(cln_config.rpc_path, PathBuf::from("/tmp/lightning-rpc"));
860        assert_eq!(cln_config.bolt12, false);
861        assert_eq!(cln_config.fee_percent, 0.01);
862        let reserve_fee_u64: u64 = cln_config.reserve_fee_min.into();
863        assert_eq!(reserve_fee_u64, 4);
864
865        // Cleanup env vars
866        env::remove_var(crate::env_vars::ENV_LN_BACKEND);
867        env::remove_var(crate::env_vars::ENV_CLN_RPC_PATH);
868        env::remove_var(crate::env_vars::ENV_CLN_BOLT12);
869        env::remove_var(crate::env_vars::ENV_CLN_FEE_PERCENT);
870        env::remove_var(crate::env_vars::ENV_CLN_RESERVE_FEE_MIN);
871
872        // Cleanup test file
873        let _ = fs::remove_dir_all(&temp_dir);
874    }
875
876    #[cfg(feature = "lnbits")]
877    fn test_lnbits_env_config() {
878        use std::{env, fs};
879
880        // Create a temporary directory for config file
881        let temp_dir = env::temp_dir().join("cdk_test_env_vars_lnbits");
882        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
883        let config_path = temp_dir.join("config.toml");
884
885        // Create a minimal config.toml with backend set but NO [lnbits] section
886        let config_content = r#"
887[ln]
888backend = "lnbits"
889min_mint = 1
890max_mint = 500000
891min_melt = 1
892max_melt = 500000
893"#;
894        fs::write(&config_path, config_content).expect("Failed to write config file");
895
896        // Set environment variables for LNbits configuration
897        env::set_var(crate::env_vars::ENV_LN_BACKEND, "lnbits");
898        env::set_var(crate::env_vars::ENV_LNBITS_ADMIN_API_KEY, "test_admin_key");
899        env::set_var(
900            crate::env_vars::ENV_LNBITS_INVOICE_API_KEY,
901            "test_invoice_key",
902        );
903        env::set_var(
904            crate::env_vars::ENV_LNBITS_API,
905            "https://lnbits.example.com",
906        );
907        env::set_var(crate::env_vars::ENV_LNBITS_FEE_PERCENT, "0.02");
908        env::set_var(crate::env_vars::ENV_LNBITS_RESERVE_FEE_MIN, "5");
909
910        // Load settings and apply environment variables (same as production code)
911        let mut settings = Settings::new(Some(&config_path));
912        settings.from_env().expect("Failed to apply env vars");
913
914        // Verify that settings were populated from env vars
915        assert!(settings.lnbits.is_some());
916        let lnbits_config = settings.lnbits.as_ref().unwrap();
917        assert_eq!(lnbits_config.admin_api_key, "test_admin_key");
918        assert_eq!(lnbits_config.invoice_api_key, "test_invoice_key");
919        assert_eq!(lnbits_config.lnbits_api, "https://lnbits.example.com");
920        assert_eq!(lnbits_config.fee_percent, 0.02);
921        let reserve_fee_u64: u64 = lnbits_config.reserve_fee_min.into();
922        assert_eq!(reserve_fee_u64, 5);
923
924        // Cleanup env vars
925        env::remove_var(crate::env_vars::ENV_LN_BACKEND);
926        env::remove_var(crate::env_vars::ENV_LNBITS_ADMIN_API_KEY);
927        env::remove_var(crate::env_vars::ENV_LNBITS_INVOICE_API_KEY);
928        env::remove_var(crate::env_vars::ENV_LNBITS_API);
929        env::remove_var(crate::env_vars::ENV_LNBITS_FEE_PERCENT);
930        env::remove_var(crate::env_vars::ENV_LNBITS_RESERVE_FEE_MIN);
931
932        // Cleanup test file
933        let _ = fs::remove_dir_all(&temp_dir);
934    }
935
936    #[cfg(feature = "fakewallet")]
937    fn test_fakewallet_env_config() {
938        use std::{env, fs};
939
940        // Create a temporary directory for config file
941        let temp_dir = env::temp_dir().join("cdk_test_env_vars_fakewallet");
942        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
943        let config_path = temp_dir.join("config.toml");
944
945        // Create a minimal config.toml with backend set but NO [fake_wallet] section
946        let config_content = r#"
947[ln]
948backend = "fakewallet"
949min_mint = 1
950max_mint = 500000
951min_melt = 1
952max_melt = 500000
953"#;
954        fs::write(&config_path, config_content).expect("Failed to write config file");
955
956        // Set environment variables for FakeWallet configuration
957        env::set_var(crate::env_vars::ENV_LN_BACKEND, "fakewallet");
958        env::set_var(crate::env_vars::ENV_FAKE_WALLET_SUPPORTED_UNITS, "sat,msat");
959        env::set_var(crate::env_vars::ENV_FAKE_WALLET_FEE_PERCENT, "0.0");
960        env::set_var(crate::env_vars::ENV_FAKE_WALLET_RESERVE_FEE_MIN, "0");
961        env::set_var(crate::env_vars::ENV_FAKE_WALLET_MIN_DELAY, "0");
962        env::set_var(crate::env_vars::ENV_FAKE_WALLET_MAX_DELAY, "5");
963
964        // Load settings and apply environment variables (same as production code)
965        let mut settings = Settings::new(Some(&config_path));
966        settings.from_env().expect("Failed to apply env vars");
967
968        // Verify that settings were populated from env vars
969        assert!(settings.fake_wallet.is_some());
970        let fakewallet_config = settings.fake_wallet.as_ref().unwrap();
971        assert_eq!(fakewallet_config.fee_percent, 0.0);
972        let reserve_fee_u64: u64 = fakewallet_config.reserve_fee_min.into();
973        assert_eq!(reserve_fee_u64, 0);
974        assert_eq!(fakewallet_config.min_delay_time, 0);
975        assert_eq!(fakewallet_config.max_delay_time, 5);
976
977        // Cleanup env vars
978        env::remove_var(crate::env_vars::ENV_LN_BACKEND);
979        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_SUPPORTED_UNITS);
980        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_FEE_PERCENT);
981        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_RESERVE_FEE_MIN);
982        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_MIN_DELAY);
983        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_MAX_DELAY);
984
985        // Cleanup test file
986        let _ = fs::remove_dir_all(&temp_dir);
987    }
988
989    #[cfg(feature = "grpc-processor")]
990    fn test_grpc_processor_env_config() {
991        use std::{env, fs};
992
993        // Create a temporary directory for config file
994        let temp_dir = env::temp_dir().join("cdk_test_env_vars_grpc");
995        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
996        let config_path = temp_dir.join("config.toml");
997
998        // Create a minimal config.toml with backend set but NO [grpc_processor] section
999        let config_content = r#"
1000[ln]
1001backend = "grpcprocessor"
1002min_mint = 1
1003max_mint = 500000
1004min_melt = 1
1005max_melt = 500000
1006"#;
1007        fs::write(&config_path, config_content).expect("Failed to write config file");
1008
1009        // Set environment variables for GRPC Processor configuration
1010        env::set_var(crate::env_vars::ENV_LN_BACKEND, "grpcprocessor");
1011        env::set_var(
1012            crate::env_vars::ENV_GRPC_PROCESSOR_SUPPORTED_UNITS,
1013            "sat,msat",
1014        );
1015        env::set_var(crate::env_vars::ENV_GRPC_PROCESSOR_ADDRESS, "localhost");
1016        env::set_var(crate::env_vars::ENV_GRPC_PROCESSOR_PORT, "50051");
1017
1018        // Load settings and apply environment variables (same as production code)
1019        let mut settings = Settings::new(Some(&config_path));
1020        settings.from_env().expect("Failed to apply env vars");
1021
1022        // Verify that settings were populated from env vars
1023        assert!(settings.grpc_processor.is_some());
1024        let grpc_config = settings.grpc_processor.as_ref().unwrap();
1025        assert_eq!(grpc_config.addr, "localhost");
1026        assert_eq!(grpc_config.port, 50051);
1027
1028        // Cleanup env vars
1029        env::remove_var(crate::env_vars::ENV_LN_BACKEND);
1030        env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_SUPPORTED_UNITS);
1031        env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_ADDRESS);
1032        env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_PORT);
1033
1034        // Cleanup test file
1035        let _ = fs::remove_dir_all(&temp_dir);
1036    }
1037
1038    #[cfg(feature = "ldk-node")]
1039    fn test_ldk_node_env_config() {
1040        use std::{env, fs};
1041
1042        // Create a temporary directory for config file
1043        let temp_dir = env::temp_dir().join("cdk_test_env_vars_ldk");
1044        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1045        let config_path = temp_dir.join("config.toml");
1046
1047        // Create a minimal config.toml with backend set but NO [ldk_node] section
1048        let config_content = r#"
1049[ln]
1050backend = "ldknode"
1051min_mint = 1
1052max_mint = 500000
1053min_melt = 1
1054max_melt = 500000
1055"#;
1056        fs::write(&config_path, config_content).expect("Failed to write config file");
1057
1058        // Set environment variables for LDK Node configuration
1059        env::set_var(crate::env_vars::ENV_LN_BACKEND, "ldknode");
1060        env::set_var(crate::env_vars::LDK_NODE_FEE_PERCENT_ENV_VAR, "0.01");
1061        env::set_var(crate::env_vars::LDK_NODE_RESERVE_FEE_MIN_ENV_VAR, "4");
1062        env::set_var(crate::env_vars::LDK_NODE_BITCOIN_NETWORK_ENV_VAR, "regtest");
1063        env::set_var(
1064            crate::env_vars::LDK_NODE_CHAIN_SOURCE_TYPE_ENV_VAR,
1065            "esplora",
1066        );
1067        env::set_var(
1068            crate::env_vars::LDK_NODE_ESPLORA_URL_ENV_VAR,
1069            "http://localhost:3000",
1070        );
1071        env::set_var(
1072            crate::env_vars::LDK_NODE_STORAGE_DIR_PATH_ENV_VAR,
1073            "/tmp/ldk",
1074        );
1075
1076        // Load settings and apply environment variables (same as production code)
1077        let mut settings = Settings::new(Some(&config_path));
1078        settings.from_env().expect("Failed to apply env vars");
1079
1080        // Verify that settings were populated from env vars
1081        assert!(settings.ldk_node.is_some());
1082        let ldk_config = settings.ldk_node.as_ref().unwrap();
1083        assert_eq!(ldk_config.fee_percent, 0.01);
1084        let reserve_fee_u64: u64 = ldk_config.reserve_fee_min.into();
1085        assert_eq!(reserve_fee_u64, 4);
1086        assert_eq!(ldk_config.bitcoin_network, Some("regtest".to_string()));
1087        assert_eq!(ldk_config.chain_source_type, Some("esplora".to_string()));
1088        assert_eq!(
1089            ldk_config.esplora_url,
1090            Some("http://localhost:3000".to_string())
1091        );
1092        assert_eq!(ldk_config.storage_dir_path, Some("/tmp/ldk".to_string()));
1093
1094        // Cleanup env vars
1095        env::remove_var(crate::env_vars::ENV_LN_BACKEND);
1096        env::remove_var(crate::env_vars::LDK_NODE_FEE_PERCENT_ENV_VAR);
1097        env::remove_var(crate::env_vars::LDK_NODE_RESERVE_FEE_MIN_ENV_VAR);
1098        env::remove_var(crate::env_vars::LDK_NODE_BITCOIN_NETWORK_ENV_VAR);
1099        env::remove_var(crate::env_vars::LDK_NODE_CHAIN_SOURCE_TYPE_ENV_VAR);
1100        env::remove_var(crate::env_vars::LDK_NODE_ESPLORA_URL_ENV_VAR);
1101        env::remove_var(crate::env_vars::LDK_NODE_STORAGE_DIR_PATH_ENV_VAR);
1102
1103        // Cleanup test file
1104        let _ = fs::remove_dir_all(&temp_dir);
1105    }
1106}