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