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        let mnemonic_hash = sha256::Hash::from_slice(&self.mnemonic.clone().into_bytes())
30            .map_err(|_| std::fmt::Error)?;
31
32        f.debug_struct("Info")
33            .field("url", &self.url)
34            .field("listen_host", &self.listen_host)
35            .field("listen_port", &self.listen_port)
36            .field("mnemonic", &format!("<hashed: {}>", mnemonic_hash))
37            .field("input_fee_ppk", &self.input_fee_ppk)
38            .field("http_cache", &self.http_cache)
39            .field("enable_swagger_ui", &self.enable_swagger_ui)
40            .finish()
41    }
42}
43
44#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
45#[serde(rename_all = "lowercase")]
46pub enum LnBackend {
47    #[default]
48    None,
49    #[cfg(feature = "cln")]
50    Cln,
51    #[cfg(feature = "lnbits")]
52    LNbits,
53    #[cfg(feature = "fakewallet")]
54    FakeWallet,
55    #[cfg(feature = "lnd")]
56    Lnd,
57    #[cfg(feature = "grpc-processor")]
58    GrpcProcessor,
59}
60
61impl std::str::FromStr for LnBackend {
62    type Err = String;
63
64    fn from_str(s: &str) -> Result<Self, Self::Err> {
65        match s.to_lowercase().as_str() {
66            #[cfg(feature = "cln")]
67            "cln" => Ok(LnBackend::Cln),
68            #[cfg(feature = "lnbits")]
69            "lnbits" => Ok(LnBackend::LNbits),
70            #[cfg(feature = "fakewallet")]
71            "fakewallet" => Ok(LnBackend::FakeWallet),
72            #[cfg(feature = "lnd")]
73            "lnd" => Ok(LnBackend::Lnd),
74            #[cfg(feature = "grpc-processor")]
75            "grpcprocessor" => Ok(LnBackend::GrpcProcessor),
76            _ => Err(format!("Unknown Lightning backend: {}", s)),
77        }
78    }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct Ln {
83    pub ln_backend: LnBackend,
84    pub invoice_description: Option<String>,
85    pub min_mint: Amount,
86    pub max_mint: Amount,
87    pub min_melt: Amount,
88    pub max_melt: Amount,
89}
90
91impl Default for Ln {
92    fn default() -> Self {
93        Ln {
94            ln_backend: LnBackend::default(),
95            invoice_description: None,
96            min_mint: 1.into(),
97            max_mint: 500_000.into(),
98            min_melt: 1.into(),
99            max_melt: 500_000.into(),
100        }
101    }
102}
103
104#[cfg(feature = "lnbits")]
105#[derive(Debug, Clone, Serialize, Deserialize, Default)]
106pub struct LNbits {
107    pub admin_api_key: String,
108    pub invoice_api_key: String,
109    pub lnbits_api: String,
110    pub fee_percent: f32,
111    pub reserve_fee_min: Amount,
112}
113
114#[cfg(feature = "cln")]
115#[derive(Debug, Clone, Serialize, Deserialize, Default)]
116pub struct Cln {
117    pub rpc_path: PathBuf,
118    #[serde(default)]
119    pub bolt12: bool,
120    pub fee_percent: f32,
121    pub reserve_fee_min: Amount,
122}
123
124#[cfg(feature = "lnd")]
125#[derive(Debug, Clone, Serialize, Deserialize, Default)]
126pub struct Lnd {
127    pub address: String,
128    pub cert_file: PathBuf,
129    pub macaroon_file: PathBuf,
130    pub fee_percent: f32,
131    pub reserve_fee_min: Amount,
132}
133
134#[cfg(feature = "fakewallet")]
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct FakeWallet {
137    pub supported_units: Vec<CurrencyUnit>,
138    pub fee_percent: f32,
139    pub reserve_fee_min: Amount,
140    #[serde(default = "default_min_delay_time")]
141    pub min_delay_time: u64,
142    #[serde(default = "default_max_delay_time")]
143    pub max_delay_time: u64,
144}
145
146#[cfg(feature = "fakewallet")]
147impl Default for FakeWallet {
148    fn default() -> Self {
149        Self {
150            supported_units: vec![CurrencyUnit::Sat],
151            fee_percent: 0.02,
152            reserve_fee_min: 2.into(),
153            min_delay_time: 1,
154            max_delay_time: 3,
155        }
156    }
157}
158
159// Helper functions to provide default values
160#[cfg(feature = "fakewallet")]
161fn default_min_delay_time() -> u64 {
162    1
163}
164
165#[cfg(feature = "fakewallet")]
166fn default_max_delay_time() -> u64 {
167    3
168}
169
170#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
171pub struct GrpcProcessor {
172    pub supported_units: Vec<CurrencyUnit>,
173    pub addr: String,
174    pub port: u16,
175    pub tls_dir: Option<PathBuf>,
176}
177
178#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
179#[serde(rename_all = "lowercase")]
180pub enum DatabaseEngine {
181    #[default]
182    Sqlite,
183    #[cfg(feature = "redb")]
184    Redb,
185}
186
187impl std::str::FromStr for DatabaseEngine {
188    type Err = String;
189
190    fn from_str(s: &str) -> Result<Self, Self::Err> {
191        match s.to_lowercase().as_str() {
192            "sqlite" => Ok(DatabaseEngine::Sqlite),
193            #[cfg(feature = "redb")]
194            "redb" => Ok(DatabaseEngine::Redb),
195            _ => Err(format!("Unknown database engine: {}", s)),
196        }
197    }
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize, Default)]
201pub struct Database {
202    pub engine: DatabaseEngine,
203}
204
205#[derive(Debug, Clone, Default, Serialize, Deserialize)]
206pub struct Auth {
207    pub openid_discovery: String,
208    pub openid_client_id: String,
209    pub mint_max_bat: u64,
210    #[serde(default = "default_true")]
211    pub enabled_mint: bool,
212    #[serde(default = "default_true")]
213    pub enabled_melt: bool,
214    #[serde(default = "default_true")]
215    pub enabled_swap: bool,
216    #[serde(default = "default_true")]
217    pub enabled_check_mint_quote: bool,
218    #[serde(default = "default_true")]
219    pub enabled_check_melt_quote: bool,
220    #[serde(default = "default_true")]
221    pub enabled_restore: bool,
222    #[serde(default = "default_true")]
223    pub enabled_check_proof_state: bool,
224}
225
226fn default_true() -> bool {
227    true
228}
229/// CDK settings, derived from `config.toml`
230#[derive(Debug, Clone, Serialize, Deserialize, Default)]
231pub struct Settings {
232    pub info: Info,
233    pub mint_info: MintInfo,
234    pub ln: Ln,
235    #[cfg(feature = "cln")]
236    pub cln: Option<Cln>,
237    #[cfg(feature = "lnbits")]
238    pub lnbits: Option<LNbits>,
239    #[cfg(feature = "lnd")]
240    pub lnd: Option<Lnd>,
241    #[cfg(feature = "fakewallet")]
242    pub fake_wallet: Option<FakeWallet>,
243    pub grpc_processor: Option<GrpcProcessor>,
244    pub database: Database,
245    #[cfg(feature = "management-rpc")]
246    pub mint_management_rpc: Option<MintManagementRpc>,
247    pub auth: Option<Auth>,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize, Default)]
251pub struct MintInfo {
252    /// name of the mint and should be recognizable
253    pub name: String,
254    /// hex pubkey of the mint
255    pub pubkey: Option<PublicKey>,
256    /// short description of the mint
257    pub description: String,
258    /// long description
259    pub description_long: Option<String>,
260    /// url to the mint icon
261    pub icon_url: Option<String>,
262    /// message of the day that the wallet must display to the user
263    pub motd: Option<String>,
264    /// Nostr publickey
265    pub contact_nostr_public_key: Option<String>,
266    /// Contact email
267    pub contact_email: Option<String>,
268    /// URL to the terms of service
269    pub tos_url: Option<String>,
270}
271
272#[cfg(feature = "management-rpc")]
273#[derive(Debug, Clone, Serialize, Deserialize, Default)]
274pub struct MintManagementRpc {
275    /// When this is set to `true` the mint use the config file for the initial set up on first start.
276    /// Changes to the `[mint_info]` after this **MUST** be made via the RPC changes to the config file or env vars will be ignored.
277    pub enabled: bool,
278    pub address: Option<String>,
279    pub port: Option<u16>,
280    pub tls_dir_path: Option<PathBuf>,
281}
282
283impl Settings {
284    #[must_use]
285    pub fn new<P>(config_file_name: Option<P>) -> Self
286    where
287        P: Into<PathBuf>,
288    {
289        let default_settings = Self::default();
290        // attempt to construct settings with file
291        let from_file = Self::new_from_default(&default_settings, config_file_name);
292        match from_file {
293            Ok(f) => f,
294            Err(e) => {
295                tracing::error!(
296                    "Error reading config file, falling back to defaults. Error: {e:?}"
297                );
298                default_settings
299            }
300        }
301    }
302
303    fn new_from_default<P>(
304        default: &Settings,
305        config_file_name: Option<P>,
306    ) -> Result<Self, ConfigError>
307    where
308        P: Into<PathBuf>,
309    {
310        let mut default_config_file_name = home::home_dir()
311            .ok_or(ConfigError::NotFound("Config Path".to_string()))?
312            .join("cashu-rs-mint");
313
314        default_config_file_name.push("config.toml");
315        let config: String = match config_file_name {
316            Some(value) => value.into().to_string_lossy().to_string(),
317            None => default_config_file_name.to_string_lossy().to_string(),
318        };
319        let builder = Config::builder();
320        let config: Config = builder
321            // use defaults
322            .add_source(Config::try_from(default)?)
323            // override with file contents
324            .add_source(File::with_name(&config))
325            .build()?;
326        let settings: Settings = config.try_deserialize()?;
327
328        match settings.ln.ln_backend {
329            LnBackend::None => panic!("Ln backend must be set"),
330            #[cfg(feature = "cln")]
331            LnBackend::Cln => assert!(
332                settings.cln.is_some(),
333                "CLN backend requires a valid config."
334            ),
335            #[cfg(feature = "lnbits")]
336            LnBackend::LNbits => assert!(
337                settings.lnbits.is_some(),
338                "LNbits backend requires a valid config"
339            ),
340            #[cfg(feature = "lnd")]
341            LnBackend::Lnd => {
342                assert!(
343                    settings.lnd.is_some(),
344                    "LND backend requires a valid config."
345                )
346            }
347            #[cfg(feature = "fakewallet")]
348            LnBackend::FakeWallet => assert!(
349                settings.fake_wallet.is_some(),
350                "FakeWallet backend requires a valid config."
351            ),
352            #[cfg(feature = "grpc-processor")]
353            LnBackend::GrpcProcessor => {
354                assert!(
355                    settings.grpc_processor.is_some(),
356                    "GRPC backend requires a valid config."
357                )
358            }
359        }
360
361        Ok(settings)
362    }
363}