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 config::{Config, ConfigError, File};
8use serde::{Deserialize, Serialize};
9
10#[derive(Clone, Serialize, Deserialize, Default)]
11pub struct Info {
12    pub url: String,
13    pub listen_host: String,
14    pub listen_port: u16,
15    pub mnemonic: String,
16    pub input_fee_ppk: Option<u64>,
17
18    pub http_cache: cache::Config,
19
20    /// When this is set to true, the mint exposes a Swagger UI for it's API at
21    /// `[listen_host]:[listen_port]/swagger-ui`
22    ///
23    /// This requires `mintd` was built with the `swagger` feature flag.
24    pub enable_swagger_ui: Option<bool>,
25}
26
27impl std::fmt::Debug for Info {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        // Use a fallback approach that won't panic
30        let mnemonic_display = {
31            let hash = sha256::Hash::hash(self.mnemonic.clone().into_bytes().as_ref());
32            format!("<hashed: {hash}>")
33        };
34
35        f.debug_struct("Info")
36            .field("url", &self.url)
37            .field("listen_host", &self.listen_host)
38            .field("listen_port", &self.listen_port)
39            .field("mnemonic", &mnemonic_display)
40            .field("input_fee_ppk", &self.input_fee_ppk)
41            .field("http_cache", &self.http_cache)
42            .field("enable_swagger_ui", &self.enable_swagger_ui)
43            .finish()
44    }
45}
46
47#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
48#[serde(rename_all = "lowercase")]
49pub enum LnBackend {
50    #[default]
51    None,
52    #[cfg(feature = "cln")]
53    Cln,
54    #[cfg(feature = "lnbits")]
55    LNbits,
56    #[cfg(feature = "fakewallet")]
57    FakeWallet,
58    #[cfg(feature = "lnd")]
59    Lnd,
60    #[cfg(feature = "grpc-processor")]
61    GrpcProcessor,
62}
63
64impl std::str::FromStr for LnBackend {
65    type Err = String;
66
67    fn from_str(s: &str) -> Result<Self, Self::Err> {
68        match s.to_lowercase().as_str() {
69            #[cfg(feature = "cln")]
70            "cln" => Ok(LnBackend::Cln),
71            #[cfg(feature = "lnbits")]
72            "lnbits" => Ok(LnBackend::LNbits),
73            #[cfg(feature = "fakewallet")]
74            "fakewallet" => Ok(LnBackend::FakeWallet),
75            #[cfg(feature = "lnd")]
76            "lnd" => Ok(LnBackend::Lnd),
77            #[cfg(feature = "grpc-processor")]
78            "grpcprocessor" => Ok(LnBackend::GrpcProcessor),
79            _ => Err(format!("Unknown Lightning backend: {s}")),
80        }
81    }
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct Ln {
86    pub ln_backend: LnBackend,
87    pub invoice_description: Option<String>,
88    pub min_mint: Amount,
89    pub max_mint: Amount,
90    pub min_melt: Amount,
91    pub max_melt: Amount,
92}
93
94impl Default for Ln {
95    fn default() -> Self {
96        Ln {
97            ln_backend: LnBackend::default(),
98            invoice_description: None,
99            min_mint: 1.into(),
100            max_mint: 500_000.into(),
101            min_melt: 1.into(),
102            max_melt: 500_000.into(),
103        }
104    }
105}
106
107#[cfg(feature = "lnbits")]
108#[derive(Debug, Clone, Serialize, Deserialize, Default)]
109pub struct LNbits {
110    pub admin_api_key: String,
111    pub invoice_api_key: String,
112    pub lnbits_api: String,
113    pub fee_percent: f32,
114    pub reserve_fee_min: Amount,
115}
116
117#[cfg(feature = "cln")]
118#[derive(Debug, Clone, Serialize, Deserialize, Default)]
119pub struct Cln {
120    pub rpc_path: PathBuf,
121    #[serde(default)]
122    pub bolt12: bool,
123    pub fee_percent: f32,
124    pub reserve_fee_min: Amount,
125}
126
127#[cfg(feature = "lnd")]
128#[derive(Debug, Clone, Serialize, Deserialize, Default)]
129pub struct Lnd {
130    pub address: String,
131    pub cert_file: PathBuf,
132    pub macaroon_file: PathBuf,
133    pub fee_percent: f32,
134    pub reserve_fee_min: Amount,
135}
136
137#[cfg(feature = "fakewallet")]
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct FakeWallet {
140    pub supported_units: Vec<CurrencyUnit>,
141    pub fee_percent: f32,
142    pub reserve_fee_min: Amount,
143    #[serde(default = "default_min_delay_time")]
144    pub min_delay_time: u64,
145    #[serde(default = "default_max_delay_time")]
146    pub max_delay_time: u64,
147}
148
149#[cfg(feature = "fakewallet")]
150impl Default for FakeWallet {
151    fn default() -> Self {
152        Self {
153            supported_units: vec![CurrencyUnit::Sat],
154            fee_percent: 0.02,
155            reserve_fee_min: 2.into(),
156            min_delay_time: 1,
157            max_delay_time: 3,
158        }
159    }
160}
161
162// Helper functions to provide default values
163#[cfg(feature = "fakewallet")]
164fn default_min_delay_time() -> u64 {
165    1
166}
167
168#[cfg(feature = "fakewallet")]
169fn default_max_delay_time() -> u64 {
170    3
171}
172
173#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
174pub struct GrpcProcessor {
175    pub supported_units: Vec<CurrencyUnit>,
176    pub addr: String,
177    pub port: u16,
178    pub tls_dir: Option<PathBuf>,
179}
180
181#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
182#[serde(rename_all = "lowercase")]
183pub enum DatabaseEngine {
184    #[default]
185    Sqlite,
186    #[cfg(feature = "redb")]
187    Redb,
188}
189
190impl std::str::FromStr for DatabaseEngine {
191    type Err = String;
192
193    fn from_str(s: &str) -> Result<Self, Self::Err> {
194        match s.to_lowercase().as_str() {
195            "sqlite" => Ok(DatabaseEngine::Sqlite),
196            #[cfg(feature = "redb")]
197            "redb" => Ok(DatabaseEngine::Redb),
198            _ => Err(format!("Unknown database engine: {s}")),
199        }
200    }
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize, Default)]
204pub struct Database {
205    pub engine: DatabaseEngine,
206}
207
208#[derive(Debug, Clone, Default, Serialize, Deserialize)]
209pub struct Auth {
210    pub openid_discovery: String,
211    pub openid_client_id: String,
212    pub mint_max_bat: u64,
213    #[serde(default = "default_true")]
214    pub enabled_mint: bool,
215    #[serde(default = "default_true")]
216    pub enabled_melt: bool,
217    #[serde(default = "default_true")]
218    pub enabled_swap: bool,
219    #[serde(default = "default_true")]
220    pub enabled_check_mint_quote: bool,
221    #[serde(default = "default_true")]
222    pub enabled_check_melt_quote: bool,
223    #[serde(default = "default_true")]
224    pub enabled_restore: bool,
225    #[serde(default = "default_true")]
226    pub enabled_check_proof_state: bool,
227}
228
229fn default_true() -> bool {
230    true
231}
232/// CDK settings, derived from `config.toml`
233#[derive(Debug, Clone, Serialize, Deserialize, Default)]
234pub struct Settings {
235    pub info: Info,
236    pub mint_info: MintInfo,
237    pub ln: Ln,
238    #[cfg(feature = "cln")]
239    pub cln: Option<Cln>,
240    #[cfg(feature = "lnbits")]
241    pub lnbits: Option<LNbits>,
242    #[cfg(feature = "lnd")]
243    pub lnd: Option<Lnd>,
244    #[cfg(feature = "fakewallet")]
245    pub fake_wallet: Option<FakeWallet>,
246    pub grpc_processor: Option<GrpcProcessor>,
247    pub database: Database,
248    #[cfg(feature = "management-rpc")]
249    pub mint_management_rpc: Option<MintManagementRpc>,
250    pub auth: Option<Auth>,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize, Default)]
254pub struct MintInfo {
255    /// name of the mint and should be recognizable
256    pub name: String,
257    /// hex pubkey of the mint
258    pub pubkey: Option<PublicKey>,
259    /// short description of the mint
260    pub description: String,
261    /// long description
262    pub description_long: Option<String>,
263    /// url to the mint icon
264    pub icon_url: Option<String>,
265    /// message of the day that the wallet must display to the user
266    pub motd: Option<String>,
267    /// Nostr publickey
268    pub contact_nostr_public_key: Option<String>,
269    /// Contact email
270    pub contact_email: Option<String>,
271    /// URL to the terms of service
272    pub tos_url: Option<String>,
273}
274
275#[cfg(feature = "management-rpc")]
276#[derive(Debug, Clone, Serialize, Deserialize, Default)]
277pub struct MintManagementRpc {
278    /// When this is set to `true` the mint use the config file for the initial set up on first start.
279    /// Changes to the `[mint_info]` after this **MUST** be made via the RPC changes to the config file or env vars will be ignored.
280    pub enabled: bool,
281    pub address: Option<String>,
282    pub port: Option<u16>,
283    pub tls_dir_path: Option<PathBuf>,
284}
285
286impl Settings {
287    #[must_use]
288    pub fn new<P>(config_file_name: Option<P>) -> Self
289    where
290        P: Into<PathBuf>,
291    {
292        let default_settings = Self::default();
293        // attempt to construct settings with file
294        let from_file = Self::new_from_default(&default_settings, config_file_name);
295        match from_file {
296            Ok(f) => f,
297            Err(e) => {
298                tracing::error!(
299                    "Error reading config file, falling back to defaults. Error: {e:?}"
300                );
301                default_settings
302            }
303        }
304    }
305
306    fn new_from_default<P>(
307        default: &Settings,
308        config_file_name: Option<P>,
309    ) -> Result<Self, ConfigError>
310    where
311        P: Into<PathBuf>,
312    {
313        let mut default_config_file_name = home::home_dir()
314            .ok_or(ConfigError::NotFound("Config Path".to_string()))?
315            .join("cashu-rs-mint");
316
317        default_config_file_name.push("config.toml");
318        let config: String = match config_file_name {
319            Some(value) => value.into().to_string_lossy().to_string(),
320            None => default_config_file_name.to_string_lossy().to_string(),
321        };
322        let builder = Config::builder();
323        let config: Config = builder
324            // use defaults
325            .add_source(Config::try_from(default)?)
326            // override with file contents
327            .add_source(File::with_name(&config))
328            .build()?;
329        let settings: Settings = config.try_deserialize()?;
330
331        match settings.ln.ln_backend {
332            LnBackend::None => panic!("Ln backend must be set"),
333            #[cfg(feature = "cln")]
334            LnBackend::Cln => assert!(
335                settings.cln.is_some(),
336                "CLN backend requires a valid config."
337            ),
338            #[cfg(feature = "lnbits")]
339            LnBackend::LNbits => assert!(
340                settings.lnbits.is_some(),
341                "LNbits backend requires a valid config"
342            ),
343            #[cfg(feature = "lnd")]
344            LnBackend::Lnd => {
345                assert!(
346                    settings.lnd.is_some(),
347                    "LND backend requires a valid config."
348                )
349            }
350            #[cfg(feature = "fakewallet")]
351            LnBackend::FakeWallet => assert!(
352                settings.fake_wallet.is_some(),
353                "FakeWallet backend requires a valid config."
354            ),
355            #[cfg(feature = "grpc-processor")]
356            LnBackend::GrpcProcessor => {
357                assert!(
358                    settings.grpc_processor.is_some(),
359                    "GRPC backend requires a valid config."
360                )
361            }
362        }
363
364        Ok(settings)
365    }
366}
367
368#[cfg(test)]
369mod tests {
370
371    use super::*;
372
373    #[test]
374    fn test_info_debug_impl() {
375        // Create a sample Info struct with test data
376        let info = Info {
377            url: "http://example.com".to_string(),
378            listen_host: "127.0.0.1".to_string(),
379            listen_port: 8080,
380            mnemonic: "test secret mnemonic phrase".to_string(),
381            input_fee_ppk: Some(100),
382            ..Default::default()
383        };
384
385        // Convert the Info struct to a debug string
386        let debug_output = format!("{info:?}");
387
388        // Verify the debug output contains expected fields
389        assert!(debug_output.contains("url: \"http://example.com\""));
390        assert!(debug_output.contains("listen_host: \"127.0.0.1\""));
391        assert!(debug_output.contains("listen_port: 8080"));
392
393        // The mnemonic should be hashed, not displayed in plaintext
394        assert!(!debug_output.contains("test secret mnemonic phrase"));
395        assert!(debug_output.contains("<hashed: "));
396
397        assert!(debug_output.contains("input_fee_ppk: Some(100)"));
398    }
399
400    #[test]
401    fn test_info_debug_with_empty_mnemonic() {
402        // Test with an empty mnemonic to ensure it doesn't panic
403        let info = Info {
404            url: "http://example.com".to_string(),
405            listen_host: "127.0.0.1".to_string(),
406            listen_port: 8080,
407            mnemonic: "".to_string(), // Empty mnemonic
408            enable_swagger_ui: Some(false),
409            ..Default::default()
410        };
411
412        // This should not panic
413        let debug_output = format!("{:?}", info);
414
415        // The empty mnemonic should still be hashed
416        assert!(debug_output.contains("<hashed: "));
417    }
418
419    #[test]
420    fn test_info_debug_with_special_chars() {
421        // Test with a mnemonic containing special characters
422        let info = Info {
423            url: "http://example.com".to_string(),
424            listen_host: "127.0.0.1".to_string(),
425            listen_port: 8080,
426            mnemonic: "特殊字符 !@#$%^&*()".to_string(), // Special characters
427            ..Default::default()
428        };
429
430        // This should not panic
431        let debug_output = format!("{:?}", info);
432
433        // The mnemonic with special chars should be hashed
434        assert!(!debug_output.contains("特殊字符 !@#$%^&*()"));
435        assert!(debug_output.contains("<hashed: "));
436    }
437}