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