Skip to main content

cdk_mintd/
config.rs

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