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 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 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#[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#[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 pub name: String,
263 pub pubkey: Option<PublicKey>,
265 pub description: String,
267 pub description_long: Option<String>,
269 pub icon_url: Option<String>,
271 pub motd: Option<String>,
273 pub contact_nostr_public_key: Option<String>,
275 pub contact_email: Option<String>,
277 pub tos_url: Option<String>,
279}
280
281#[cfg(feature = "management-rpc")]
282#[derive(Debug, Clone, Serialize, Deserialize, Default)]
283pub struct MintManagementRpc {
284 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 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 .add_source(Config::try_from(default)?)
332 .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 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 let debug_output = format!("{info:?}");
393
394 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 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 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()), enable_swagger_ui: Some(false),
415 ..Default::default()
416 };
417
418 let debug_output = format!("{:?}", info);
420
421 assert!(debug_output.contains("<hashed: "));
423 }
424
425 #[test]
426 fn test_info_debug_with_special_chars() {
427 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()), ..Default::default()
434 };
435
436 let debug_output = format!("{:?}", info);
438
439 assert!(!debug_output.contains("特殊字符 !@#$%^&*()"));
441 assert!(debug_output.contains("<hashed: "));
442 }
443}