1use std::fmt;
2use std::path::PathBuf;
3
4use bitcoin::hashes::{sha256, Hash};
5use cdk::nuts::{CurrencyUnit, PublicKey};
6use cdk::Amount;
7use cdk_axum::cache;
8use cdk_common::common::QuoteTTL;
9use cdk_common::redact::url_for_logs;
10use config::{Config, ConfigError, File, FileFormat};
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
14#[serde(rename_all = "lowercase")]
15pub enum LoggingOutput {
16 Stderr,
18 File,
20 #[default]
22 Both,
23}
24
25impl std::str::FromStr for LoggingOutput {
26 type Err = String;
27
28 fn from_str(s: &str) -> Result<Self, Self::Err> {
29 match s.to_lowercase().as_str() {
30 "stderr" => Ok(LoggingOutput::Stderr),
31 "file" => Ok(LoggingOutput::File),
32 "both" => Ok(LoggingOutput::Both),
33 _ => Err(format!(
34 "Unknown logging output: {s}. Valid options: stdout, file, both"
35 )),
36 }
37 }
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, Default)]
41pub struct LoggingConfig {
42 #[serde(default)]
44 pub output: LoggingOutput,
45 pub console_level: Option<String>,
47 pub file_level: Option<String>,
49}
50
51#[derive(Clone, Serialize, Deserialize)]
52#[serde(default)]
53pub struct Info {
54 pub url: String,
55 pub listen_host: String,
56 pub listen_port: u16,
57 pub seed: Option<String>,
59 pub mnemonic: Option<String>,
60 pub input_fee_ppk: Option<u64>,
61 pub use_keyset_v2: Option<bool>,
63
64 pub http_cache: cache::Config,
65
66 #[serde(default)]
68 pub logging: LoggingConfig,
69
70 pub enable_info_page: Option<bool>,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
80 pub quote_ttl: Option<QuoteTTL>,
81}
82
83impl Default for Info {
84 fn default() -> Self {
85 Info {
86 url: String::new(),
87 listen_host: "127.0.0.1".to_string(),
88 listen_port: 8091, seed: None,
90 mnemonic: None,
91 input_fee_ppk: None,
92 use_keyset_v2: None,
93 http_cache: cache::Config::default(),
94 enable_info_page: Some(true),
95 logging: LoggingConfig::default(),
96 quote_ttl: None,
97 }
98 }
99}
100
101impl std::fmt::Debug for Info {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 let mnemonic_display: String = {
105 if let Some(mnemonic) = self.mnemonic.as_ref() {
106 let hash = sha256::Hash::hash(mnemonic.as_bytes());
107 format!("<hashed: {hash}>")
108 } else {
109 "<not set>".to_string()
110 }
111 };
112
113 f.debug_struct("Info")
114 .field("url", &self.url)
115 .field("listen_host", &self.listen_host)
116 .field("listen_port", &self.listen_port)
117 .field("mnemonic", &mnemonic_display)
118 .field("input_fee_ppk", &self.input_fee_ppk)
119 .field("use_keyset_v2", &self.use_keyset_v2)
120 .field("http_cache", &self.http_cache)
121 .field("logging", &self.logging)
122 .field("enable_info_page", &self.enable_info_page)
123 .finish()
124 }
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
128#[serde(default)]
129pub struct Signatory {
130 #[serde(default)]
131 pub enabled: bool,
132 #[serde(default = "default_signatory_address")]
133 pub address: String,
134 #[serde(default = "default_signatory_port")]
135 pub port: u16,
136 #[serde(default)]
137 pub tls_dir: Option<PathBuf>,
138 #[serde(default)]
139 pub allow_insecure: bool,
140}
141
142impl Default for Signatory {
143 fn default() -> Self {
144 Self {
145 enabled: false,
146 address: default_signatory_address(),
147 port: default_signatory_port(),
148 tls_dir: None,
149 allow_insecure: false,
150 }
151 }
152}
153
154fn default_signatory_address() -> String {
155 "127.0.0.1".to_string()
156}
157
158fn default_signatory_port() -> u16 {
159 15060
160}
161
162#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
163#[serde(rename_all = "lowercase")]
164pub enum PaymentBackendType {
165 #[default]
166 None,
167 #[cfg(feature = "cln")]
168 Cln,
169 #[cfg(feature = "fakewallet")]
170 FakeWallet,
171 #[cfg(feature = "lnd")]
172 Lnd,
173 #[cfg(feature = "ldk-node")]
174 #[serde(alias = "ldk-node")]
175 LdkNode,
176 #[cfg(feature = "grpc-processor")]
177 GrpcProcessor,
178}
179
180impl std::str::FromStr for PaymentBackendType {
181 type Err = String;
182
183 fn from_str(s: &str) -> Result<Self, Self::Err> {
184 match s.to_lowercase().as_str() {
185 #[cfg(feature = "cln")]
186 "cln" => Ok(PaymentBackendType::Cln),
187 #[cfg(feature = "fakewallet")]
188 "fakewallet" => Ok(PaymentBackendType::FakeWallet),
189 #[cfg(feature = "lnd")]
190 "lnd" => Ok(PaymentBackendType::Lnd),
191 #[cfg(feature = "ldk-node")]
192 "ldk-node" | "ldknode" => Ok(PaymentBackendType::LdkNode),
193 #[cfg(feature = "grpc-processor")]
194 "grpcprocessor" => Ok(PaymentBackendType::GrpcProcessor),
195 _ => Err(format!("Unknown payment backend: {s}")),
196 }
197 }
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
201#[serde(default)]
202pub struct PaymentBackend {
203 pub backend: PaymentBackendType,
204 #[serde(default)]
205 pub unit: CurrencyUnit,
206 pub invoice_description: Option<String>,
207 pub min_mint: Amount,
208 pub max_mint: Amount,
209 pub min_melt: Amount,
210 pub max_melt: Amount,
211}
212
213impl Default for PaymentBackend {
214 fn default() -> Self {
215 PaymentBackend {
216 backend: PaymentBackendType::default(),
217 unit: CurrencyUnit::default(),
218 invoice_description: None,
219 min_mint: 1.into(),
220 max_mint: 500_000.into(),
221 min_melt: 1.into(),
222 max_melt: 500_000.into(),
223 }
224 }
225}
226
227fn deserialize_payment_backend<'de, D>(deserializer: D) -> Result<Vec<PaymentBackend>, D::Error>
228where
229 D: serde::Deserializer<'de>,
230{
231 #[derive(Deserialize)]
232 #[serde(untagged)]
233 enum PaymentBackendOneOrMany {
234 Many(Vec<PaymentBackend>),
235 One(PaymentBackend),
236 }
237
238 match PaymentBackendOneOrMany::deserialize(deserializer)? {
239 PaymentBackendOneOrMany::Many(backends) => Ok(backends),
240 PaymentBackendOneOrMany::One(backend) => Ok(vec![backend]),
241 }
242}
243
244#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
245#[serde(rename_all = "lowercase")]
246pub enum OnchainBackend {
247 #[default]
248 None,
249 #[cfg(feature = "bdk")]
250 Bdk,
251 #[cfg(feature = "fakewallet")]
252 FakeWallet,
253}
254
255impl std::str::FromStr for OnchainBackend {
256 type Err = String;
257
258 fn from_str(s: &str) -> Result<Self, Self::Err> {
259 match s.to_lowercase().as_str() {
260 "none" => Ok(OnchainBackend::None),
261 #[cfg(feature = "bdk")]
262 "bdk" => Ok(OnchainBackend::Bdk),
263 #[cfg(feature = "fakewallet")]
264 "fakewallet" => Ok(OnchainBackend::FakeWallet),
265 _ => Err(format!("Unknown Onchain backend: {s}")),
266 }
267 }
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
271#[serde(default)]
272pub struct Onchain {
273 pub onchain_backend: OnchainBackend,
274 pub min_mint: Amount,
275 pub max_mint: Amount,
276 pub min_melt: Amount,
277 pub max_melt: Amount,
278}
279
280impl Default for Onchain {
281 fn default() -> Self {
282 Onchain {
283 onchain_backend: OnchainBackend::default(),
284 min_mint: 1.into(),
285 max_mint: 500_000.into(),
286 min_melt: 1.into(),
287 max_melt: 500_000.into(),
288 }
289 }
290}
291
292#[cfg(feature = "bdk")]
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct BatchConfig {
295 #[serde(default = "default_bdk_poll_interval_secs")]
297 pub poll_interval_secs: u64,
298 #[serde(default = "default_bdk_max_batch_size")]
300 pub max_batch_size: usize,
301 #[serde(default = "default_bdk_target_block_time_secs")]
303 pub target_block_time_secs: u64,
304 #[serde(default)]
306 pub standard_deadline_secs: Option<u64>,
307 #[serde(default)]
309 pub economy_deadline_secs: Option<u64>,
310 #[serde(default = "default_bdk_fee_options")]
312 pub fee_options: Vec<String>,
313 #[serde(default = "default_bdk_fee_fallback_sat_per_vb")]
315 pub fee_fallback_sat_per_vb: f64,
316 #[serde(default = "default_bdk_fee_cache_ttl_secs")]
318 pub fee_cache_ttl_secs: u64,
319 #[serde(default = "default_bdk_quote_max_input_count")]
321 pub quote_max_input_count: usize,
322 #[serde(default = "default_bdk_quote_fixed_safety_sat")]
324 pub quote_fixed_safety_sat: u64,
325 #[serde(default = "default_bdk_quote_safety_multiplier")]
327 pub quote_safety_multiplier: f64,
328}
329
330#[cfg(feature = "bdk")]
331impl Default for BatchConfig {
332 fn default() -> Self {
333 Self {
334 poll_interval_secs: default_bdk_poll_interval_secs(),
335 max_batch_size: default_bdk_max_batch_size(),
336 target_block_time_secs: default_bdk_target_block_time_secs(),
337 standard_deadline_secs: None,
338 economy_deadline_secs: None,
339 fee_options: default_bdk_fee_options(),
340 fee_fallback_sat_per_vb: default_bdk_fee_fallback_sat_per_vb(),
341 fee_cache_ttl_secs: default_bdk_fee_cache_ttl_secs(),
342 quote_max_input_count: default_bdk_quote_max_input_count(),
343 quote_fixed_safety_sat: default_bdk_quote_fixed_safety_sat(),
344 quote_safety_multiplier: default_bdk_quote_safety_multiplier(),
345 }
346 }
347}
348
349#[cfg(feature = "bdk")]
350#[derive(Clone, Serialize, Deserialize)]
351pub struct Bdk {
352 #[serde(default = "default_fee_percent")]
354 pub fee_percent: f32,
355 #[serde(default = "default_reserve_fee_min")]
357 pub reserve_fee_min: Amount,
358 pub network: Option<String>,
360 pub chain_source_type: Option<String>,
362 pub esplora_url: Option<String>,
364 #[serde(default = "default_bdk_esplora_parallel_requests")]
370 pub esplora_parallel_requests: usize,
371 pub electrum_url: Option<String>,
373 #[serde(default = "default_bdk_electrum_batch_size")]
375 pub electrum_batch_size: usize,
376 pub bitcoind_rpc_host: Option<String>,
378 pub bitcoind_rpc_port: Option<u16>,
380 pub bitcoind_rpc_user: Option<String>,
382 pub bitcoind_rpc_password: Option<String>,
384 pub wallet_rescan_from_height: Option<u32>,
390 pub mnemonic: Option<String>,
392 #[serde(default)]
394 pub batch_config: BatchConfig,
395 #[serde(default = "default_bdk_num_confs")]
402 pub num_confs: u32,
403 #[serde(default = "default_bdk_min_receive_amount_sat")]
405 pub min_receive_amount_sat: u64,
406 #[serde(default = "default_bdk_min_send_amount_sat")]
408 pub min_send_amount_sat: u64,
409 #[serde(default = "default_bdk_sync_interval_secs")]
411 pub sync_interval_secs: u64,
412}
413
414#[cfg(feature = "bdk")]
415impl fmt::Debug for Bdk {
416 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
417 f.debug_struct("Bdk")
418 .field("fee_percent", &self.fee_percent)
419 .field("reserve_fee_min", &self.reserve_fee_min)
420 .field("network", &self.network)
421 .field("chain_source_type", &self.chain_source_type)
422 .field(
423 "esplora_url",
424 &self.esplora_url.as_deref().map(url_for_logs),
425 )
426 .field("esplora_parallel_requests", &self.esplora_parallel_requests)
427 .field(
428 "electrum_url",
429 &self.electrum_url.as_deref().map(url_for_logs),
430 )
431 .field("electrum_batch_size", &self.electrum_batch_size)
432 .field("bitcoind_rpc_host", &self.bitcoind_rpc_host)
433 .field("bitcoind_rpc_port", &self.bitcoind_rpc_port)
434 .field("bitcoind_rpc_user", &self.bitcoind_rpc_user)
435 .field("bitcoind_rpc_password", &"[REDACTED]")
436 .field("wallet_rescan_from_height", &self.wallet_rescan_from_height)
437 .field("mnemonic", &"[REDACTED]")
438 .field("batch_config", &self.batch_config)
439 .field("num_confs", &self.num_confs)
440 .field("min_receive_amount_sat", &self.min_receive_amount_sat)
441 .field("min_send_amount_sat", &self.min_send_amount_sat)
442 .field("sync_interval_secs", &self.sync_interval_secs)
443 .finish()
444 }
445}
446
447#[cfg(feature = "bdk")]
448impl Default for Bdk {
449 fn default() -> Self {
450 Self {
451 fee_percent: default_fee_percent(),
452 reserve_fee_min: default_reserve_fee_min(),
453 network: None,
454 chain_source_type: None,
455 esplora_url: None,
456 esplora_parallel_requests: default_bdk_esplora_parallel_requests(),
457 electrum_url: None,
458 electrum_batch_size: default_bdk_electrum_batch_size(),
459 bitcoind_rpc_host: None,
460 bitcoind_rpc_port: None,
461 bitcoind_rpc_user: None,
462 bitcoind_rpc_password: None,
463 wallet_rescan_from_height: None,
464 mnemonic: None,
465 batch_config: BatchConfig::default(),
466 num_confs: default_bdk_num_confs(),
467 min_receive_amount_sat: default_bdk_min_receive_amount_sat(),
468 min_send_amount_sat: default_bdk_min_send_amount_sat(),
469 sync_interval_secs: default_bdk_sync_interval_secs(),
470 }
471 }
472}
473
474#[cfg(feature = "bdk")]
475impl Bdk {
476 pub fn validate(&self) -> Result<(), String> {
478 if self.num_confs == 0 {
479 return Err(
480 "BDK num_confs must be >= 1 (0 is rejected because it still \
481 requires an on-chain anchor and is almost never intended; \
482 use 1 for 'any confirmation')"
483 .to_string(),
484 );
485 }
486
487 if self.min_send_amount_sat == 0 {
488 return Err("BDK min_send_amount_sat must be >= 1".to_string());
489 }
490
491 if self.batch_config.target_block_time_secs == 0 {
492 return Err("BDK batch_config.target_block_time_secs must be >= 1".to_string());
493 }
494
495 validate_bdk_fee_options(&self.batch_config.fee_options)?;
496
497 Ok(())
498 }
499}
500
501#[cfg(feature = "bdk")]
502fn default_bdk_num_confs() -> u32 {
503 6
504}
505
506#[cfg(feature = "bdk")]
507fn default_bdk_min_receive_amount_sat() -> u64 {
508 1000
509}
510
511#[cfg(feature = "bdk")]
512fn default_bdk_min_send_amount_sat() -> u64 {
513 546
514}
515
516#[cfg(feature = "bdk")]
517fn default_bdk_sync_interval_secs() -> u64 {
518 30
519}
520
521#[cfg(feature = "bdk")]
522fn default_bdk_esplora_parallel_requests() -> usize {
523 1
524}
525
526#[cfg(feature = "bdk")]
527fn default_bdk_electrum_batch_size() -> usize {
528 5
529}
530
531#[cfg(feature = "bdk")]
532fn default_bdk_poll_interval_secs() -> u64 {
533 30
534}
535
536#[cfg(feature = "bdk")]
537fn default_bdk_max_batch_size() -> usize {
538 50
539}
540
541#[cfg(feature = "bdk")]
542fn default_bdk_target_block_time_secs() -> u64 {
543 cdk_bdk::DEFAULT_TARGET_BLOCK_TIME_SECS
544}
545
546#[cfg(feature = "bdk")]
547fn default_bdk_fee_options() -> Vec<String> {
548 vec!["immediate".to_string()]
549}
550
551#[cfg(feature = "bdk")]
552fn validate_bdk_fee_options(fee_options: &[String]) -> Result<(), String> {
553 let tiers = fee_options
554 .iter()
555 .map(|tier| {
556 cdk_bdk::PaymentTier::from_config_name(tier).ok_or_else(|| {
557 format!(
558 "Unknown BDK batch_config.fee_options tier '{tier}'; expected immediate, standard, or economy"
559 )
560 })
561 })
562 .collect::<Result<Vec<_>, _>>()?;
563
564 cdk_bdk::types::validate_fee_options(&tiers)
565}
566
567#[cfg(feature = "bdk")]
568fn default_bdk_fee_fallback_sat_per_vb() -> f64 {
569 2.0
570}
571
572#[cfg(feature = "bdk")]
573fn default_bdk_fee_cache_ttl_secs() -> u64 {
574 60
575}
576
577#[cfg(feature = "bdk")]
578fn default_bdk_quote_max_input_count() -> usize {
579 24
580}
581
582#[cfg(feature = "bdk")]
583fn default_bdk_quote_fixed_safety_sat() -> u64 {
584 500
585}
586
587#[cfg(feature = "bdk")]
588fn default_bdk_quote_safety_multiplier() -> f64 {
589 1.25
590}
591
592#[cfg(feature = "cln")]
593#[derive(Debug, Clone, Serialize, Deserialize)]
594#[serde(default)]
595pub struct Cln {
596 pub rpc_path: PathBuf,
597 #[serde(default = "default_cln_bolt12")]
598 pub bolt12: bool,
599 #[serde(default)]
600 pub expose_private_channels: bool,
601 #[serde(default = "default_fee_percent")]
602 pub fee_percent: f32,
603 #[serde(default = "default_reserve_fee_min")]
604 pub reserve_fee_min: Amount,
605}
606
607#[cfg(feature = "cln")]
608impl Default for Cln {
609 fn default() -> Self {
610 Self {
611 rpc_path: PathBuf::new(),
612 bolt12: true,
613 expose_private_channels: false,
614 fee_percent: 0.02,
615 reserve_fee_min: 2.into(),
616 }
617 }
618}
619
620#[cfg(feature = "cln")]
621fn default_cln_bolt12() -> bool {
622 true
623}
624
625#[cfg(feature = "lnd")]
626#[derive(Debug, Clone, Serialize, Deserialize)]
627#[serde(default)]
628pub struct Lnd {
629 pub address: String,
630 pub cert_file: PathBuf,
631 pub macaroon_file: PathBuf,
632 #[serde(default = "default_fee_percent")]
633 pub fee_percent: f32,
634 #[serde(default = "default_reserve_fee_min")]
635 pub reserve_fee_min: Amount,
636}
637
638#[cfg(feature = "lnd")]
639impl Default for Lnd {
640 fn default() -> Self {
641 Self {
642 address: String::new(),
643 cert_file: PathBuf::new(),
644 macaroon_file: PathBuf::new(),
645 fee_percent: 0.02,
646 reserve_fee_min: 2.into(),
647 }
648 }
649}
650
651#[cfg(feature = "ldk-node")]
652#[derive(Clone, Serialize, Deserialize)]
653#[serde(default)]
654pub struct LdkNode {
655 #[serde(default = "default_ldk_fee_percent")]
657 pub fee_percent: f32,
658 #[serde(default = "default_ldk_reserve_fee_min")]
660 pub reserve_fee_min: Amount,
661 pub bitcoin_network: Option<String>,
663 pub chain_source_type: Option<String>,
665 pub esplora_url: Option<String>,
667 pub electrum_url: Option<String>,
669 pub bitcoind_rpc_host: Option<String>,
671 pub bitcoind_rpc_port: Option<u16>,
672 pub bitcoind_rpc_user: Option<String>,
673 pub bitcoind_rpc_password: Option<String>,
674 pub storage_dir_path: Option<String>,
676 pub log_dir_path: Option<String>,
678 pub ldk_node_host: Option<String>,
680 pub ldk_node_port: Option<u16>,
682 pub ldk_node_announce_addresses: Option<Vec<String>>,
684 pub gossip_source_type: Option<String>,
686 pub rgs_url: Option<String>,
688 #[serde(default = "default_webserver_host")]
690 pub webserver_host: Option<String>,
691 #[serde(default = "default_webserver_port")]
693 pub webserver_port: Option<u16>,
694 pub ldk_node_mnemonic: Option<String>,
697}
698
699#[cfg(feature = "ldk-node")]
700impl Default for LdkNode {
701 fn default() -> Self {
702 Self {
703 fee_percent: default_ldk_fee_percent(),
704 reserve_fee_min: default_ldk_reserve_fee_min(),
705 bitcoin_network: None,
706 chain_source_type: None,
707 esplora_url: None,
708 electrum_url: None,
709 bitcoind_rpc_host: None,
710 bitcoind_rpc_port: None,
711 bitcoind_rpc_user: None,
712 ldk_node_announce_addresses: None,
713 bitcoind_rpc_password: None,
714 storage_dir_path: None,
715 ldk_node_host: None,
716 log_dir_path: None,
717 ldk_node_port: None,
718 gossip_source_type: None,
719 rgs_url: None,
720 webserver_host: default_webserver_host(),
721 webserver_port: default_webserver_port(),
722 ldk_node_mnemonic: None,
723 }
724 }
725}
726
727#[cfg(feature = "ldk-node")]
728impl fmt::Debug for LdkNode {
729 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
730 f.debug_struct("LdkNode")
731 .field("fee_percent", &self.fee_percent)
732 .field("reserve_fee_min", &self.reserve_fee_min)
733 .field("bitcoin_network", &self.bitcoin_network)
734 .field("chain_source_type", &self.chain_source_type)
735 .field(
736 "esplora_url",
737 &self.esplora_url.as_deref().map(url_for_logs),
738 )
739 .field(
740 "electrum_url",
741 &self.electrum_url.as_deref().map(url_for_logs),
742 )
743 .field("bitcoind_rpc_host", &self.bitcoind_rpc_host)
744 .field("bitcoind_rpc_port", &self.bitcoind_rpc_port)
745 .field("bitcoind_rpc_user", &self.bitcoind_rpc_user)
746 .field("bitcoind_rpc_password", &"[REDACTED]")
747 .field("storage_dir_path", &self.storage_dir_path)
748 .field("log_dir_path", &self.log_dir_path)
749 .field("ldk_node_host", &self.ldk_node_host)
750 .field("ldk_node_port", &self.ldk_node_port)
751 .field(
752 "ldk_node_announce_addresses",
753 &self.ldk_node_announce_addresses,
754 )
755 .field("gossip_source_type", &self.gossip_source_type)
756 .field("rgs_url", &self.rgs_url.as_deref().map(url_for_logs))
757 .field("webserver_host", &self.webserver_host)
758 .field("webserver_port", &self.webserver_port)
759 .field("ldk_node_mnemonic", &"[REDACTED]")
760 .finish()
761 }
762}
763
764#[cfg(feature = "ldk-node")]
765fn default_ldk_fee_percent() -> f32 {
766 0.04
767}
768
769#[cfg(feature = "ldk-node")]
770fn default_ldk_reserve_fee_min() -> Amount {
771 4.into()
772}
773
774#[cfg(feature = "ldk-node")]
775fn default_webserver_host() -> Option<String> {
776 Some("127.0.0.1".to_string())
777}
778
779#[cfg(feature = "ldk-node")]
780fn default_webserver_port() -> Option<u16> {
781 Some(8091)
782}
783
784#[cfg(feature = "fakewallet")]
785#[derive(Debug, Clone, Serialize, Deserialize)]
786pub struct FakeWalletKeysetRotation {
787 pub unit: CurrencyUnit,
789 #[serde(default)]
791 pub input_fee_ppk: u64,
792 #[serde(default = "default_keyset_version")]
794 pub version: String,
795 #[serde(default)]
797 pub expired: bool,
798}
799
800#[cfg(feature = "fakewallet")]
801fn default_keyset_version() -> String {
802 "v1".to_string()
803}
804
805#[cfg(feature = "fakewallet")]
806#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
807#[serde(untagged)]
808pub enum FakeWalletCustomPaymentMethod {
809 Method(String),
811 MethodForUnit {
813 method: String,
815 unit: CurrencyUnit,
817 },
818}
819
820#[cfg(feature = "fakewallet")]
821impl FakeWalletCustomPaymentMethod {
822 pub fn method(&self) -> &str {
823 match self {
824 Self::Method(method) => method,
825 Self::MethodForUnit { method, .. } => method,
826 }
827 }
828
829 pub fn applies_to_unit(&self, unit: &CurrencyUnit) -> bool {
830 match self {
831 Self::Method(_) => true,
832 Self::MethodForUnit {
833 unit: method_unit, ..
834 } => method_unit == unit,
835 }
836 }
837}
838
839#[cfg(feature = "fakewallet")]
840#[derive(Debug, Clone, Serialize, Deserialize)]
841#[serde(default)]
842pub struct FakeWallet {
843 #[serde(default = "default_fake_wallet_supported_units")]
844 pub supported_units: Vec<CurrencyUnit>,
845 pub fee_percent: f32,
846 pub reserve_fee_min: Amount,
847 #[serde(default = "default_fake_wallet_custom_payment_methods")]
848 pub custom_payment_methods: Vec<FakeWalletCustomPaymentMethod>,
849 #[serde(default = "default_min_delay_time")]
850 pub min_delay_time: u64,
851 #[serde(default = "default_max_delay_time")]
852 pub max_delay_time: u64,
853 #[serde(default)]
855 pub keyset_rotations: Vec<FakeWalletKeysetRotation>,
856}
857
858#[cfg(feature = "fakewallet")]
859impl Default for FakeWallet {
860 fn default() -> Self {
861 Self {
862 supported_units: vec![CurrencyUnit::Sat],
863 fee_percent: 0.02,
864 reserve_fee_min: 2.into(),
865 custom_payment_methods: default_fake_wallet_custom_payment_methods(),
866 min_delay_time: 1,
867 max_delay_time: 3,
868 keyset_rotations: Vec::new(),
869 }
870 }
871}
872
873#[cfg(any(feature = "cln", feature = "lnd"))]
876fn default_fee_percent() -> f32 {
877 0.02
878}
879
880#[cfg(any(feature = "cln", feature = "lnd"))]
881fn default_reserve_fee_min() -> Amount {
882 2.into()
883}
884
885#[cfg(feature = "fakewallet")]
886fn default_min_delay_time() -> u64 {
887 1
888}
889
890#[cfg(feature = "fakewallet")]
891fn default_max_delay_time() -> u64 {
892 3
893}
894
895#[cfg(feature = "fakewallet")]
896fn default_fake_wallet_custom_payment_methods() -> Vec<FakeWalletCustomPaymentMethod> {
897 vec![FakeWalletCustomPaymentMethod::Method("paypal".to_string())]
898}
899
900#[cfg(feature = "fakewallet")]
901fn default_fake_wallet_supported_units() -> Vec<CurrencyUnit> {
902 vec![CurrencyUnit::Sat]
903}
904
905#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
906#[serde(default)]
907pub struct GrpcProcessor {
908 #[serde(default)]
909 pub supported_units: Vec<CurrencyUnit>,
910 #[serde(default = "default_grpc_address", alias = "addr")]
911 pub address: String,
912 #[serde(default = "default_grpc_port")]
913 pub port: u16,
914 #[serde(default)]
915 pub tls_dir: Option<PathBuf>,
916 #[serde(default)]
917 pub allow_insecure: bool,
918}
919
920impl Default for GrpcProcessor {
921 fn default() -> Self {
922 Self {
923 supported_units: Vec::new(),
924 address: default_grpc_address(),
925 port: default_grpc_port(),
926 tls_dir: None,
927 allow_insecure: false,
928 }
929 }
930}
931
932fn default_grpc_address() -> String {
933 "127.0.0.1".to_string()
934}
935
936fn default_grpc_port() -> u16 {
937 50051
938}
939
940#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
941#[serde(rename_all = "lowercase")]
942pub enum DatabaseEngine {
943 #[default]
944 Sqlite,
945 Postgres,
946}
947
948impl std::str::FromStr for DatabaseEngine {
949 type Err = String;
950
951 fn from_str(s: &str) -> Result<Self, Self::Err> {
952 match s.to_lowercase().as_str() {
953 "sqlite" => Ok(DatabaseEngine::Sqlite),
954 "postgres" => Ok(DatabaseEngine::Postgres),
955 _ => Err(format!("Unknown database engine: {s}")),
956 }
957 }
958}
959
960#[derive(Debug, Clone, Serialize, Deserialize, Default)]
961#[serde(default)]
962pub struct Database {
963 pub engine: DatabaseEngine,
964 pub postgres: Option<PostgresConfig>,
965}
966
967#[derive(Debug, Clone, Serialize, Deserialize, Default)]
968#[serde(default)]
969pub struct AuthDatabase {
970 pub postgres: Option<PostgresAuthConfig>,
971}
972
973#[derive(Clone, Serialize, Deserialize)]
974#[serde(default)]
975pub struct PostgresAuthConfig {
976 pub url: String,
977 pub tls_mode: Option<String>,
978 pub max_connections: Option<usize>,
979 pub connection_timeout_seconds: Option<u64>,
980}
981
982impl fmt::Debug for PostgresAuthConfig {
983 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
984 f.debug_struct("PostgresAuthConfig")
985 .field("url", &url_for_logs(&self.url))
986 .field("tls_mode", &self.tls_mode)
987 .field("max_connections", &self.max_connections)
988 .field(
989 "connection_timeout_seconds",
990 &self.connection_timeout_seconds,
991 )
992 .finish()
993 }
994}
995
996impl Default for PostgresAuthConfig {
997 fn default() -> Self {
998 Self {
999 url: String::new(),
1000 tls_mode: Some("disable".to_string()),
1001 max_connections: Some(20),
1002 connection_timeout_seconds: Some(10),
1003 }
1004 }
1005}
1006
1007#[derive(Clone, Serialize, Deserialize)]
1008#[serde(default)]
1009pub struct PostgresConfig {
1010 pub url: String,
1011 pub tls_mode: Option<String>,
1012 pub max_connections: Option<usize>,
1013 pub connection_timeout_seconds: Option<u64>,
1014}
1015
1016impl fmt::Debug for PostgresConfig {
1017 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1018 f.debug_struct("PostgresConfig")
1019 .field("url", &url_for_logs(&self.url))
1020 .field("tls_mode", &self.tls_mode)
1021 .field("max_connections", &self.max_connections)
1022 .field(
1023 "connection_timeout_seconds",
1024 &self.connection_timeout_seconds,
1025 )
1026 .finish()
1027 }
1028}
1029
1030impl Default for PostgresConfig {
1031 fn default() -> Self {
1032 Self {
1033 url: String::new(),
1034 tls_mode: Some("disable".to_string()),
1035 max_connections: Some(20),
1036 connection_timeout_seconds: Some(10),
1037 }
1038 }
1039}
1040
1041#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
1042#[serde(rename_all = "lowercase")]
1043pub enum AuthType {
1044 Clear,
1045 Blind,
1046 #[default]
1047 None,
1048}
1049
1050impl std::str::FromStr for AuthType {
1051 type Err = String;
1052
1053 fn from_str(s: &str) -> Result<Self, Self::Err> {
1054 match s.to_lowercase().as_str() {
1055 "clear" => Ok(AuthType::Clear),
1056 "blind" => Ok(AuthType::Blind),
1057 "none" => Ok(AuthType::None),
1058 _ => Err(format!("Unknown auth type: {s}")),
1059 }
1060 }
1061}
1062
1063#[derive(Debug, Clone, Serialize, Deserialize)]
1064#[serde(default)]
1065pub struct Auth {
1066 #[serde(default)]
1067 pub auth_enabled: bool,
1068 pub openid_discovery: String,
1069 pub openid_client_id: String,
1070 pub mint_max_bat: u64,
1071 #[serde(default = "default_blind")]
1072 pub mint: AuthType,
1073 #[serde(default)]
1074 pub get_mint_quote: AuthType,
1075 #[serde(default)]
1076 pub check_mint_quote: AuthType,
1077 #[serde(default)]
1078 pub melt: AuthType,
1079 #[serde(default)]
1080 pub get_melt_quote: AuthType,
1081 #[serde(default)]
1082 pub check_melt_quote: AuthType,
1083 #[serde(default = "default_blind")]
1084 pub swap: AuthType,
1085 #[serde(default = "default_blind")]
1086 pub restore: AuthType,
1087 #[serde(default)]
1088 pub check_proof_state: AuthType,
1089 #[serde(default = "default_blind")]
1091 pub websocket_auth: AuthType,
1092}
1093
1094fn default_blind() -> AuthType {
1095 AuthType::Blind
1096}
1097
1098impl Default for Auth {
1099 fn default() -> Self {
1100 Self {
1101 auth_enabled: false,
1102 openid_discovery: String::new(),
1103 openid_client_id: String::new(),
1104 mint_max_bat: 0,
1105 mint: default_blind(),
1106 get_mint_quote: AuthType::default(),
1107 check_mint_quote: AuthType::default(),
1108 melt: AuthType::default(),
1109 get_melt_quote: AuthType::default(),
1110 check_melt_quote: AuthType::default(),
1111 swap: default_blind(),
1112 restore: default_blind(),
1113 check_proof_state: AuthType::default(),
1114 websocket_auth: default_blind(),
1115 }
1116 }
1117}
1118
1119#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1121#[serde(default)]
1122pub struct Settings {
1123 pub info: Info,
1124 pub signatory: Option<Signatory>,
1125 pub mint_info: MintInfo,
1126 #[serde(default, deserialize_with = "deserialize_payment_backend")]
1127 pub payment_backend: Vec<PaymentBackend>,
1128 pub onchain: Option<Onchain>,
1129 #[serde(default)]
1131 pub limits: Limits,
1132 #[cfg(feature = "cln")]
1133 pub cln: Option<Cln>,
1134 #[cfg(feature = "lnd")]
1135 pub lnd: Option<Lnd>,
1136 #[cfg(feature = "ldk-node")]
1137 pub ldk_node: Option<LdkNode>,
1138 #[cfg(feature = "fakewallet")]
1139 pub fake_wallet: Option<FakeWallet>,
1140 pub grpc_processor: Option<GrpcProcessor>,
1141 #[cfg(feature = "bdk")]
1142 pub bdk: Option<Bdk>,
1143 pub database: Database,
1144 pub auth_database: Option<AuthDatabase>,
1145 #[cfg(feature = "management-rpc")]
1146 pub mint_management_rpc: Option<MintManagementRpc>,
1147 pub auth: Option<Auth>,
1148 #[cfg(feature = "prometheus")]
1149 #[serde(default, skip_serializing_if = "Option::is_none")]
1150 pub prometheus: Option<Prometheus>,
1151}
1152
1153#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1154#[cfg(feature = "prometheus")]
1155#[serde(default)]
1156pub struct Prometheus {
1157 pub enabled: bool,
1158 pub address: Option<String>,
1159 pub port: Option<u16>,
1160}
1161
1162#[derive(Debug, Clone, Serialize, Deserialize)]
1164pub struct Limits {
1165 #[serde(default = "default_max_inputs")]
1167 pub max_inputs: usize,
1168 #[serde(default = "default_max_outputs")]
1170 pub max_outputs: usize,
1171}
1172
1173impl Default for Limits {
1174 fn default() -> Self {
1175 Self {
1176 max_inputs: 1000,
1177 max_outputs: 1000,
1178 }
1179 }
1180}
1181
1182fn default_max_inputs() -> usize {
1183 1000
1184}
1185
1186fn default_max_outputs() -> usize {
1187 1000
1188}
1189
1190#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1191#[serde(default)]
1192pub struct MintInfo {
1193 pub name: String,
1195 pub pubkey: Option<PublicKey>,
1197 pub description: String,
1199 pub description_long: Option<String>,
1201 pub icon_url: Option<String>,
1203 pub motd: Option<String>,
1205 pub contact_nostr_public_key: Option<String>,
1207 pub contact_email: Option<String>,
1209 pub tos_url: Option<String>,
1211}
1212
1213#[cfg(feature = "management-rpc")]
1214#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1215#[serde(default)]
1216pub struct MintManagementRpc {
1217 pub enabled: bool,
1220 pub address: Option<String>,
1221 pub port: Option<u16>,
1222 pub tls_dir: Option<PathBuf>,
1223 #[serde(default)]
1224 pub allow_insecure: bool,
1225 #[serde(default)]
1230 pub allow_mint_quote_payment_override: bool,
1231}
1232
1233impl Settings {
1234 pub fn try_from_toml(document: &str) -> Result<Self, ConfigError> {
1236 Self::try_from_toml_allowing(document, &[])
1237 }
1238
1239 pub(crate) fn try_from_toml_allowing(
1242 document: &str,
1243 allowed_unknown_fields: &[&str],
1244 ) -> Result<Self, ConfigError> {
1245 let defaults = Self::default();
1246 let configuration = Config::builder()
1247 .add_source(Config::try_from(&defaults)?)
1248 .add_source(File::from_str(document, FileFormat::Toml))
1249 .build()?;
1250 Self::deserialize_configuration(configuration, allowed_unknown_fields)
1251 }
1252
1253 fn deserialize_configuration(
1254 configuration: Config,
1255 allowed_unknown_fields: &[&str],
1256 ) -> Result<Self, ConfigError> {
1257 let mut unknown_fields = Vec::new();
1258 let settings = serde_ignored::deserialize(configuration, |path| {
1259 let path = path.to_string().replace(".?.", ".");
1260 if !allowed_unknown_fields.contains(&path.as_str()) {
1261 unknown_fields.push(path);
1262 }
1263 })?;
1264 unknown_fields.sort();
1265 unknown_fields.dedup();
1266 if unknown_fields.is_empty() {
1267 Ok(settings)
1268 } else {
1269 let mut message = format!(
1270 "unknown configuration field(s): {}",
1271 unknown_fields.join(", ")
1272 );
1273 if unknown_fields.iter().any(|field| field == "ln") {
1274 message.push_str(
1275 "; legacy `[ln]`/`[[ln]]` configuration must be migrated to \
1276 `[payment_backend]`/`[[payment_backend]]`; run `cdk-mintd config migrate \
1277 --file <old> --output <new>`",
1278 );
1279 }
1280 Err(ConfigError::Message(message))
1281 }
1282 }
1283
1284 pub fn validate_backend_pairing(&self) -> Result<(), String> {
1285 #[cfg(feature = "fakewallet")]
1286 self.validate_fake_wallet_backend_pairing()?;
1287
1288 Ok(())
1289 }
1290
1291 #[cfg(feature = "fakewallet")]
1292 fn validate_fake_wallet_backend_pairing(&self) -> Result<(), String> {
1293 let onchain_backend = self
1294 .onchain
1295 .as_ref()
1296 .map(|onchain| &onchain.onchain_backend)
1297 .unwrap_or(&OnchainBackend::None);
1298
1299 let has_fake_wallet_backend = self
1300 .payment_backend
1301 .iter()
1302 .any(|backend| backend.backend == PaymentBackendType::FakeWallet);
1303 let has_real_backend = self.payment_backend.iter().any(|backend| {
1304 !matches!(
1305 backend.backend,
1306 PaymentBackendType::None | PaymentBackendType::FakeWallet
1307 )
1308 });
1309
1310 if has_fake_wallet_backend && has_real_backend {
1311 return Err("backend = \"fakewallet\" cannot be combined with a real \
1312 payment backend; use only fakewallet backends or only real backends"
1313 .to_string());
1314 }
1315
1316 match onchain_backend {
1317 #[cfg(feature = "bdk")]
1318 OnchainBackend::Bdk if has_fake_wallet_backend => {
1319 return Err("backend = \"fakewallet\" cannot be combined with \
1320 onchain_backend = \"bdk\"; use onchain_backend = \
1321 \"fakewallet\" or \"none\""
1322 .to_string());
1323 }
1324 OnchainBackend::FakeWallet if has_real_backend => {
1325 return Err("onchain_backend = \"fakewallet\" cannot be combined with \
1326 a real payment backend; use backend = \"fakewallet\" \
1327 or \"none\""
1328 .to_string());
1329 }
1330 _ => {}
1331 }
1332
1333 Ok(())
1334 }
1335
1336 pub fn try_new<P>(config_file_name: Option<P>) -> Result<Self, ConfigError>
1337 where
1338 P: Into<PathBuf>,
1339 {
1340 let default_settings = Self::default();
1341 Self::new_from_default(&default_settings, config_file_name)
1342 }
1343
1344 #[deprecated(note = "use Settings::try_new to handle configuration errors")]
1350 #[must_use]
1351 pub fn new<P>(config_file_name: Option<P>) -> Self
1352 where
1353 P: Into<PathBuf>,
1354 {
1355 match Self::try_new(config_file_name) {
1356 Ok(settings) => settings,
1357 Err(err) => {
1358 tracing::error!("Error reading config file, falling back to defaults: {err}");
1359 Self::default()
1360 }
1361 }
1362 }
1363
1364 fn new_from_default<P>(
1365 default: &Settings,
1366 config_file_name: Option<P>,
1367 ) -> Result<Self, ConfigError>
1368 where
1369 P: Into<PathBuf>,
1370 {
1371 let config: String = match config_file_name {
1372 Some(value) => value.into().to_string_lossy().to_string(),
1373 None => {
1374 let mut default_config_file_name = home::home_dir()
1375 .ok_or(ConfigError::NotFound("Config Path".to_string()))?
1376 .join("cashu-rs-mint");
1377 default_config_file_name.push("config.toml");
1378 default_config_file_name.to_string_lossy().to_string()
1379 }
1380 };
1381 let builder = Config::builder();
1382 let config: Config = builder
1383 .add_source(Config::try_from(default)?)
1385 .add_source(File::with_name(&config))
1387 .build()?;
1388 Self::deserialize_configuration(config, &[])
1389 }
1390
1391 pub(crate) fn enabled_signatory(&self) -> Option<&Signatory> {
1392 self.signatory
1393 .as_ref()
1394 .filter(|signatory| signatory.enabled)
1395 }
1396}
1397
1398#[cfg(test)]
1399mod tests {
1400
1401 use super::*;
1402
1403 #[test]
1404 fn postgres_config_debug_redacts_connection_credentials() {
1405 let secret = "postgres-password-secret";
1406 let url = format!("postgres://mint:{secret}@db.example.com/cdk?token={secret}");
1407 let database = Database {
1408 engine: DatabaseEngine::Postgres,
1409 postgres: Some(PostgresConfig {
1410 url: url.clone(),
1411 ..Default::default()
1412 }),
1413 };
1414 let auth_database = AuthDatabase {
1415 postgres: Some(PostgresAuthConfig {
1416 url,
1417 ..Default::default()
1418 }),
1419 };
1420
1421 let database_debug = format!("{database:?}");
1422 let auth_debug = format!("{auth_database:?}");
1423
1424 assert!(database_debug.contains("postgres://db.example.com/cdk"));
1425 assert!(auth_debug.contains("postgres://db.example.com/cdk"));
1426 assert!(!database_debug.contains(secret));
1427 assert!(!auth_debug.contains(secret));
1428 }
1429
1430 #[cfg(feature = "management-rpc")]
1431 #[test]
1432 fn mint_quote_payment_override_is_explicitly_opt_in() {
1433 let default_settings =
1434 Settings::try_from_toml("").expect("default settings should deserialize");
1435 assert!(
1436 !default_settings
1437 .mint_management_rpc
1438 .unwrap_or_default()
1439 .allow_mint_quote_payment_override
1440 );
1441
1442 let enabled_settings = Settings::try_from_toml(
1443 r#"
1444[mint_management_rpc]
1445allow_mint_quote_payment_override = true
1446"#,
1447 )
1448 .expect("management RPC payment override should deserialize");
1449 assert!(
1450 enabled_settings
1451 .mint_management_rpc
1452 .expect("management RPC settings should be present")
1453 .allow_mint_quote_payment_override
1454 );
1455 }
1456
1457 #[cfg(feature = "ldk-node")]
1458 #[test]
1459 fn ldk_node_backend_accepts_supported_spellings() {
1460 for backend in ["ldk-node", "ldknode"] {
1461 let document = format!(
1462 r#"
1463[[payment_backend]]
1464backend = "{backend}"
1465unit = "sat"
1466"#
1467 );
1468
1469 let settings =
1470 Settings::try_from_toml(&document).expect("LDK Node backend should deserialize");
1471
1472 assert_eq!(
1473 settings
1474 .payment_backend
1475 .first()
1476 .expect("config should contain one payment backend")
1477 .backend,
1478 PaymentBackendType::LdkNode
1479 );
1480 }
1481 }
1482
1483 #[test]
1484 fn toml_parser_rejects_unknown_fields_with_full_paths() {
1485 let error = Settings::try_from_toml(
1486 r#"
1487[info]
1488listen_por = 8085
1489
1490[database]
1491engin = "sqlite"
1492"#,
1493 )
1494 .expect_err("misspelled fields must be rejected");
1495 let message = error.to_string();
1496 assert!(message.contains("database.engin"));
1497 assert!(message.contains("info.listen_por"));
1498 }
1499
1500 #[test]
1501 fn toml_parser_reports_legacy_ln_migration() {
1502 let error = Settings::try_from_toml(
1503 r#"
1504[ln]
1505ln_backend = "fakewallet"
1506"#,
1507 )
1508 .expect_err("legacy [ln] configuration must be rejected");
1509 let message = error.to_string();
1510 assert!(message.contains("legacy `[ln]`/`[[ln]]` configuration must be migrated"));
1511 assert!(message.contains("`[payment_backend]`/`[[payment_backend]]`"));
1512 assert!(message.contains("`cdk-mintd config migrate --file <old> --output <new>`"));
1513 }
1514
1515 #[test]
1516 fn legacy_parser_allowlist_does_not_hide_unrelated_typos() {
1517 let error = Settings::try_from_toml_allowing(
1518 r#"
1519[info]
1520signatory_url = "http://127.0.0.1:10009"
1521listen_por = 8085
1522"#,
1523 &["info.signatory_url"],
1524 )
1525 .expect_err("only exact legacy fields may be ignored");
1526 let message = error.to_string();
1527 assert!(message.contains("info.listen_por"));
1528 assert!(!message.contains("info.signatory_url"));
1529 }
1530
1531 fn config_env_lock() -> std::sync::MutexGuard<'static, ()> {
1532 crate::test_utils::env_lock()
1536 }
1537
1538 #[cfg(feature = "bdk")]
1539 fn clear_bdk_env_vars() {
1540 std::env::remove_var(crate::env_vars::BDK_MNEMONIC_ENV_VAR);
1541 std::env::remove_var(crate::env_vars::BDK_NETWORK_ENV_VAR);
1542 std::env::remove_var(crate::env_vars::BDK_CHAIN_SOURCE_TYPE_ENV_VAR);
1543 std::env::remove_var(crate::env_vars::BDK_ESPLORA_URL_ENV_VAR);
1544 std::env::remove_var(crate::env_vars::BDK_ELECTRUM_URL_ENV_VAR);
1545 std::env::remove_var(crate::env_vars::BDK_ELECTRUM_BATCH_SIZE_ENV_VAR);
1546 std::env::remove_var(crate::env_vars::BDK_WALLET_RESCAN_FROM_HEIGHT_ENV_VAR);
1547 std::env::remove_var(crate::env_vars::BDK_MIN_SEND_AMOUNT_SAT_ENV_VAR);
1548 std::env::remove_var(crate::env_vars::BDK_TARGET_BLOCK_TIME_SECS_ENV_VAR);
1549 std::env::remove_var(crate::env_vars::BDK_FEE_OPTIONS_ENV_VAR);
1550 std::env::remove_var(crate::env_vars::ENV_ONCHAIN_BACKEND);
1551 }
1552
1553 #[test]
1554 fn test_info_debug_impl() {
1555 let info = Info {
1557 url: "http://example.com".to_string(),
1558 listen_host: "127.0.0.1".to_string(),
1559 listen_port: 8080,
1560 mnemonic: Some("test secret mnemonic phrase".to_string()),
1561 input_fee_ppk: Some(100),
1562 ..Default::default()
1563 };
1564
1565 let debug_output = format!("{info:?}");
1567
1568 assert!(debug_output.contains("url: \"http://example.com\""));
1570 assert!(debug_output.contains("listen_host: \"127.0.0.1\""));
1571 assert!(debug_output.contains("listen_port: 8080"));
1572
1573 assert!(!debug_output.contains("test secret mnemonic phrase"));
1575 assert!(debug_output.contains("<hashed: "));
1576
1577 assert!(debug_output.contains("input_fee_ppk: Some(100)"));
1578 }
1579
1580 #[test]
1581 fn test_info_debug_with_empty_mnemonic() {
1582 let info = Info {
1584 url: "http://example.com".to_string(),
1585 listen_host: "127.0.0.1".to_string(),
1586 listen_port: 8080,
1587 mnemonic: Some("".to_string()), ..Default::default()
1589 };
1590
1591 let debug_output = format!("{:?}", info);
1593
1594 assert!(debug_output.contains("<hashed: "));
1596 }
1597
1598 #[cfg(feature = "bdk")]
1599 #[test]
1600 fn test_bdk_default_min_send_amount_sat() {
1601 assert_eq!(Bdk::default().min_send_amount_sat, 546);
1602 }
1603
1604 #[cfg(feature = "bdk")]
1605 #[test]
1606 fn test_bdk_default_electrum_batch_size() {
1607 assert_eq!(Bdk::default().electrum_batch_size, 5);
1608 }
1609
1610 #[cfg(feature = "bdk")]
1611 #[test]
1612 fn test_bdk_debug_redacts_secrets_and_url_credentials() {
1613 let config = Bdk {
1614 network: Some("regtest".to_string()),
1615 esplora_url: Some(
1616 "https://esplora-user:esplora-secret@example.com/esplora".to_string(),
1617 ),
1618 electrum_url: Some("ssl://electrum-user:electrum-secret@example.com:50002".to_string()),
1619 bitcoind_rpc_password: Some("rpc-password-secret".to_string()),
1620 mnemonic: Some("mnemonic-secret-words".to_string()),
1621 ..Default::default()
1622 };
1623
1624 let debug = format!("{config:?}");
1625
1626 assert!(debug.contains("regtest"));
1627 assert!(debug.contains("https://example.com/esplora"));
1628 assert!(debug.contains("ssl://example.com:50002"));
1629 assert!(debug.contains("[REDACTED]"));
1630 assert!(!debug.contains("esplora-user"));
1631 assert!(!debug.contains("esplora-secret"));
1632 assert!(!debug.contains("electrum-user"));
1633 assert!(!debug.contains("electrum-secret"));
1634 assert!(!debug.contains("rpc-password-secret"));
1635 assert!(!debug.contains("mnemonic-secret-words"));
1636 }
1637
1638 #[cfg(feature = "ldk-node")]
1639 #[test]
1640 fn test_ldk_node_debug_redacts_secrets_and_url_credentials() {
1641 let config = LdkNode {
1642 bitcoin_network: Some("regtest".to_string()),
1643 esplora_url: Some(
1644 "https://esplora-user:esplora-secret@example.com/esplora".to_string(),
1645 ),
1646 electrum_url: Some("ssl://electrum-user:electrum-secret@example.com:50002".to_string()),
1647 bitcoind_rpc_password: Some("rpc-password-secret".to_string()),
1648 rgs_url: Some("https://rgs-user:rgs-secret@example.com/snapshot".to_string()),
1649 ldk_node_mnemonic: Some("mnemonic-secret-words".to_string()),
1650 ..Default::default()
1651 };
1652
1653 let debug = format!("{config:?}");
1654
1655 assert!(debug.contains("regtest"));
1656 assert!(debug.contains("https://example.com/esplora"));
1657 assert!(debug.contains("ssl://example.com:50002"));
1658 assert!(debug.contains("https://example.com/snapshot"));
1659 assert!(debug.contains("[REDACTED]"));
1660 assert!(!debug.contains("esplora-user"));
1661 assert!(!debug.contains("esplora-secret"));
1662 assert!(!debug.contains("electrum-user"));
1663 assert!(!debug.contains("electrum-secret"));
1664 assert!(!debug.contains("rgs-user"));
1665 assert!(!debug.contains("rgs-secret"));
1666 assert!(!debug.contains("rpc-password-secret"));
1667 assert!(!debug.contains("mnemonic-secret-words"));
1668 }
1669
1670 #[cfg(feature = "bdk")]
1671 #[test]
1672 fn test_bdk_electrum_toml_config() {
1673 use std::{env, fs};
1674
1675 let temp_dir = env::temp_dir().join("cdk_test_bdk_electrum_config");
1676 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1677 let config_path = temp_dir.join("config.toml");
1678
1679 let config_content = r#"
1680[bdk]
1681network = "regtest"
1682chain_source_type = "electrum"
1683electrum_url = "tcp://127.0.0.1:50001"
1684electrum_batch_size = 11
1685"#;
1686 fs::write(&config_path, config_content).expect("Failed to write config file");
1687
1688 let settings = Settings::try_new(Some(&config_path))
1689 .expect("electrum config should parse successfully");
1690 let bdk = settings.bdk.expect("bdk config should be present");
1691
1692 assert_eq!(bdk.chain_source_type, Some("electrum".to_string()));
1693 assert_eq!(bdk.electrum_url, Some("tcp://127.0.0.1:50001".to_string()));
1694 assert_eq!(bdk.electrum_batch_size, 11);
1695
1696 let _ = fs::remove_dir_all(&temp_dir);
1697 }
1698
1699 #[cfg(feature = "bdk")]
1700 #[test]
1701 fn test_bdk_config_min_send_amount_sat_override() {
1702 use std::{env, fs};
1703
1704 let temp_dir = env::temp_dir().join("cdk_test_bdk_min_send_config");
1705 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1706 let config_path = temp_dir.join("config.toml");
1707
1708 let config_content = r#"
1709[bdk]
1710min_send_amount_sat = 1200
1711"#;
1712 fs::write(&config_path, config_content).expect("Failed to write config file");
1713
1714 let settings = Settings::try_new(Some(&config_path)).expect("config should load");
1715
1716 assert_eq!(
1717 settings
1718 .bdk
1719 .as_ref()
1720 .expect("bdk config should be present")
1721 .min_send_amount_sat,
1722 1200
1723 );
1724
1725 let _ = fs::remove_dir_all(&temp_dir);
1726 }
1727
1728 #[cfg(feature = "bdk")]
1729 #[test]
1730 fn test_bdk_env_min_send_amount_sat_override() {
1731 let _guard = config_env_lock();
1732 clear_bdk_env_vars();
1733 std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1734 std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1735 std::env::set_var(crate::env_vars::BDK_MIN_SEND_AMOUNT_SAT_ENV_VAR, "777");
1736
1737 let mut settings = Settings::default();
1738 settings.from_env().expect("Failed to apply env vars");
1739
1740 assert_eq!(
1741 settings
1742 .bdk
1743 .as_ref()
1744 .expect("bdk config should be present")
1745 .min_send_amount_sat,
1746 777
1747 );
1748
1749 clear_bdk_env_vars();
1750 }
1751
1752 #[cfg(feature = "bdk")]
1753 #[test]
1754 fn test_bdk_env_wallet_rescan_height_override() {
1755 let _guard = config_env_lock();
1756 clear_bdk_env_vars();
1757 std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1758 std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1759 std::env::set_var(
1760 crate::env_vars::BDK_WALLET_RESCAN_FROM_HEIGHT_ENV_VAR,
1761 "850000",
1762 );
1763
1764 let mut settings = Settings::default();
1765 settings.from_env().expect("Failed to apply env vars");
1766
1767 assert_eq!(
1768 settings
1769 .bdk
1770 .as_ref()
1771 .expect("bdk config should be present")
1772 .wallet_rescan_from_height,
1773 Some(850000)
1774 );
1775
1776 clear_bdk_env_vars();
1777 }
1778
1779 #[cfg(feature = "bdk")]
1780 #[test]
1781 fn test_bdk_env_electrum_config() {
1782 let _guard = config_env_lock();
1783 clear_bdk_env_vars();
1784 std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1785 std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1786 std::env::set_var(crate::env_vars::BDK_CHAIN_SOURCE_TYPE_ENV_VAR, "electrum");
1787 std::env::set_var(
1788 crate::env_vars::BDK_ELECTRUM_URL_ENV_VAR,
1789 "tcp://127.0.0.1:50001",
1790 );
1791 std::env::set_var(crate::env_vars::BDK_ELECTRUM_BATCH_SIZE_ENV_VAR, "9");
1792
1793 let mut settings = Settings::default();
1794 settings.from_env().expect("Failed to apply env vars");
1795
1796 let bdk = settings.bdk.expect("bdk config should be present");
1797 assert_eq!(bdk.chain_source_type, Some("electrum".to_string()));
1798 assert_eq!(bdk.electrum_url, Some("tcp://127.0.0.1:50001".to_string()));
1799 assert_eq!(bdk.electrum_batch_size, 9);
1800
1801 clear_bdk_env_vars();
1802 }
1803
1804 #[cfg(feature = "bdk")]
1805 #[test]
1806 fn test_bdk_default_fee_options_immediate_only() {
1807 assert_eq!(
1808 Bdk::default().batch_config.fee_options,
1809 vec!["immediate".to_string()]
1810 );
1811 }
1812
1813 #[cfg(feature = "bdk")]
1814 #[test]
1815 fn test_bdk_default_batch_deadlines_derive_from_target_block_time() {
1816 let batch_config: cdk_bdk::BatchConfig = Bdk::default().batch_config.into();
1817
1818 assert_eq!(
1819 batch_config.target_block_time,
1820 std::time::Duration::from_secs(600)
1821 );
1822 assert_eq!(
1823 batch_config.standard_deadline,
1824 std::time::Duration::from_secs(3600)
1825 );
1826 assert_eq!(
1827 batch_config.economy_deadline,
1828 std::time::Duration::from_secs(86_400)
1829 );
1830 assert_eq!(
1831 batch_config.max_intent_age,
1832 Some(std::time::Duration::from_secs(86_430))
1833 );
1834 }
1835
1836 #[cfg(feature = "bdk")]
1837 #[test]
1838 fn test_bdk_config_fee_options_override() {
1839 use std::{env, fs};
1840
1841 let temp_dir = env::temp_dir().join("cdk_test_bdk_fee_options_config");
1842 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1843 let config_path = temp_dir.join("config.toml");
1844
1845 let config_content = r#"
1846[bdk.batch_config]
1847fee_options = ["immediate", "economy"]
1848"#;
1849 fs::write(&config_path, config_content).expect("Failed to write config file");
1850
1851 let settings = Settings::try_new(Some(&config_path)).expect("config should load");
1852
1853 assert_eq!(
1854 settings
1855 .bdk
1856 .as_ref()
1857 .expect("bdk config should be present")
1858 .batch_config
1859 .fee_options,
1860 vec!["immediate".to_string(), "economy".to_string()]
1861 );
1862
1863 let _ = fs::remove_dir_all(&temp_dir);
1864 }
1865
1866 #[cfg(feature = "bdk")]
1867 #[test]
1868 fn test_bdk_config_target_block_time_derives_deadlines() {
1869 use std::{env, fs};
1870
1871 let temp_dir = env::temp_dir().join("cdk_test_bdk_target_block_time_config");
1872 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1873 let config_path = temp_dir.join("config.toml");
1874
1875 let config_content = r#"
1876[bdk.batch_config]
1877target_block_time_secs = 300
1878"#;
1879 fs::write(&config_path, config_content).expect("Failed to write config file");
1880
1881 let settings = Settings::try_new(Some(&config_path)).expect("config should load");
1882 let batch_config: cdk_bdk::BatchConfig = settings
1883 .bdk
1884 .as_ref()
1885 .expect("bdk config should be present")
1886 .batch_config
1887 .clone()
1888 .into();
1889
1890 assert_eq!(
1891 batch_config.target_block_time,
1892 std::time::Duration::from_secs(300)
1893 );
1894 assert_eq!(
1895 batch_config.standard_deadline,
1896 std::time::Duration::from_secs(1800)
1897 );
1898 assert_eq!(
1899 batch_config.economy_deadline,
1900 std::time::Duration::from_secs(43_200)
1901 );
1902 assert_eq!(
1903 batch_config.max_intent_age,
1904 Some(std::time::Duration::from_secs(43_230))
1905 );
1906
1907 let _ = fs::remove_dir_all(&temp_dir);
1908 }
1909
1910 #[cfg(feature = "bdk")]
1911 #[test]
1912 fn test_bdk_env_fee_options_override() {
1913 let _guard = config_env_lock();
1914 clear_bdk_env_vars();
1915 std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1916 std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1917 std::env::set_var(
1918 crate::env_vars::BDK_FEE_OPTIONS_ENV_VAR,
1919 "immediate,standard,economy",
1920 );
1921
1922 let mut settings = Settings::default();
1923 settings.from_env().expect("Failed to apply env vars");
1924
1925 assert_eq!(
1926 settings
1927 .bdk
1928 .as_ref()
1929 .expect("bdk config should be present")
1930 .batch_config
1931 .fee_options,
1932 vec![
1933 "immediate".to_string(),
1934 "standard".to_string(),
1935 "economy".to_string()
1936 ]
1937 );
1938
1939 clear_bdk_env_vars();
1940 }
1941
1942 #[cfg(feature = "bdk")]
1943 #[test]
1944 fn test_bdk_env_target_block_time_override() {
1945 let _guard = config_env_lock();
1946 clear_bdk_env_vars();
1947 std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1948 std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1949 std::env::set_var(crate::env_vars::BDK_TARGET_BLOCK_TIME_SECS_ENV_VAR, "120");
1950
1951 let mut settings = Settings::default();
1952 settings.from_env().expect("Failed to apply env vars");
1953 let batch_config: cdk_bdk::BatchConfig = settings
1954 .bdk
1955 .as_ref()
1956 .expect("bdk config should be present")
1957 .batch_config
1958 .clone()
1959 .into();
1960
1961 assert_eq!(
1962 batch_config.target_block_time,
1963 std::time::Duration::from_secs(120)
1964 );
1965 assert_eq!(
1966 batch_config.standard_deadline,
1967 std::time::Duration::from_secs(720)
1968 );
1969 assert_eq!(
1970 batch_config.economy_deadline,
1971 std::time::Duration::from_secs(17_280)
1972 );
1973 assert_eq!(
1974 batch_config.max_intent_age,
1975 Some(std::time::Duration::from_secs(17_310))
1976 );
1977
1978 clear_bdk_env_vars();
1979 }
1980
1981 #[cfg(feature = "bdk")]
1982 #[test]
1983 fn test_bdk_invalid_fee_options_rejected() {
1984 for fee_options in [
1985 Vec::new(),
1986 vec!["immediate".to_string(), "immediate".to_string()],
1987 vec!["urgent".to_string()],
1988 vec![
1989 "immediate".to_string(),
1990 "standard".to_string(),
1991 "economy".to_string(),
1992 "immediate".to_string(),
1993 ],
1994 ] {
1995 let bdk = Bdk {
1996 batch_config: BatchConfig {
1997 fee_options,
1998 ..BatchConfig::default()
1999 },
2000 ..Default::default()
2001 };
2002
2003 let err = bdk.validate().expect_err("invalid fee options should fail");
2004
2005 assert!(err.contains("fee_options"));
2006 }
2007 }
2008
2009 #[cfg(feature = "bdk")]
2010 #[test]
2011 fn test_bdk_target_block_time_zero_rejected() {
2012 let bdk = Bdk {
2013 batch_config: BatchConfig {
2014 target_block_time_secs: 0,
2015 ..BatchConfig::default()
2016 },
2017 ..Default::default()
2018 };
2019
2020 let err = bdk
2021 .validate()
2022 .expect_err("zero target block time should fail");
2023
2024 assert!(err.contains("target_block_time_secs"));
2025 }
2026
2027 #[cfg(feature = "bdk")]
2028 #[test]
2029 fn test_bdk_min_send_amount_sat_zero_rejected() {
2030 let bdk = Bdk {
2031 min_send_amount_sat: 0,
2032 ..Default::default()
2033 };
2034
2035 let err = bdk.validate().expect_err("zero send minimum should fail");
2036
2037 assert!(err.contains("min_send_amount_sat"));
2038 }
2039
2040 #[cfg(all(feature = "fakewallet", feature = "bdk"))]
2041 #[test]
2042 fn test_fakewallet_backend_with_bdk_onchain_rejected() {
2043 let settings = Settings {
2044 payment_backend: vec![PaymentBackend {
2045 backend: PaymentBackendType::FakeWallet,
2046 ..Default::default()
2047 }],
2048 onchain: Some(Onchain {
2049 onchain_backend: OnchainBackend::Bdk,
2050 ..Default::default()
2051 }),
2052 ..Default::default()
2053 };
2054
2055 let err = settings
2056 .validate_backend_pairing()
2057 .expect_err("fake payment backend with BDK onchain should fail");
2058
2059 assert!(err.contains("fakewallet"));
2060 assert!(err.contains("bdk"));
2061 }
2062
2063 #[cfg(all(feature = "fakewallet", feature = "cln"))]
2064 #[test]
2065 fn test_real_backend_with_fakewallet_onchain_rejected() {
2066 let settings = Settings {
2067 payment_backend: vec![PaymentBackend {
2068 backend: PaymentBackendType::Cln,
2069 ..Default::default()
2070 }],
2071 onchain: Some(Onchain {
2072 onchain_backend: OnchainBackend::FakeWallet,
2073 ..Default::default()
2074 }),
2075 ..Default::default()
2076 };
2077
2078 let err = settings
2079 .validate_backend_pairing()
2080 .expect_err("real payment backend with fake onchain should fail");
2081
2082 assert!(err.contains("fakewallet"));
2083 assert!(err.contains("real payment backend"));
2084 }
2085
2086 #[cfg(feature = "fakewallet")]
2087 #[test]
2088 fn test_fakewallet_backend_with_fakewallet_onchain_accepted() {
2089 let settings = Settings {
2090 payment_backend: vec![PaymentBackend {
2091 backend: PaymentBackendType::FakeWallet,
2092 ..Default::default()
2093 }],
2094 onchain: Some(Onchain {
2095 onchain_backend: OnchainBackend::FakeWallet,
2096 ..Default::default()
2097 }),
2098 ..Default::default()
2099 };
2100
2101 settings
2102 .validate_backend_pairing()
2103 .expect("fake-only backend pairing should pass");
2104 }
2105
2106 #[cfg(feature = "fakewallet")]
2107 #[test]
2108 fn test_fakewallet_backend_with_no_onchain_accepted() {
2109 let settings = Settings {
2110 payment_backend: vec![PaymentBackend {
2111 backend: PaymentBackendType::FakeWallet,
2112 ..Default::default()
2113 }],
2114 onchain: Some(Onchain {
2115 onchain_backend: OnchainBackend::None,
2116 ..Default::default()
2117 }),
2118 ..Default::default()
2119 };
2120
2121 settings
2122 .validate_backend_pairing()
2123 .expect("fake payment backend without onchain should pass");
2124 }
2125
2126 #[cfg(feature = "fakewallet")]
2127 #[test]
2128 fn test_no_payment_backend_with_fakewallet_onchain_accepted() {
2129 let settings = Settings {
2130 payment_backend: vec![PaymentBackend {
2131 backend: PaymentBackendType::None,
2132 ..Default::default()
2133 }],
2134 onchain: Some(Onchain {
2135 onchain_backend: OnchainBackend::FakeWallet,
2136 ..Default::default()
2137 }),
2138 ..Default::default()
2139 };
2140
2141 settings
2142 .validate_backend_pairing()
2143 .expect("fake onchain-only backend pairing should pass");
2144 }
2145
2146 #[cfg(all(feature = "fakewallet", feature = "cln"))]
2147 #[test]
2148 fn test_fakewallet_backend_with_real_backend_rejected() {
2149 let settings = Settings {
2150 payment_backend: vec![
2151 PaymentBackend {
2152 backend: PaymentBackendType::FakeWallet,
2153 ..Default::default()
2154 },
2155 PaymentBackend {
2156 backend: PaymentBackendType::Cln,
2157 ..Default::default()
2158 },
2159 ],
2160 ..Default::default()
2161 };
2162
2163 let err = settings
2164 .validate_backend_pairing()
2165 .expect_err("fake payment backend combined with real backend should fail");
2166
2167 assert!(err.contains("fakewallet"));
2168 assert!(err.contains("real payment backend"));
2169 }
2170
2171 #[cfg(feature = "fakewallet")]
2172 #[test]
2173 fn test_fakewallet_custom_payment_method_unit_matching() {
2174 let global = FakeWalletCustomPaymentMethod::Method("paypal".to_string());
2175 let usd_only = FakeWalletCustomPaymentMethod::MethodForUnit {
2176 method: "venmo".to_string(),
2177 unit: CurrencyUnit::Usd,
2178 };
2179
2180 assert!(global.applies_to_unit(&CurrencyUnit::Sat));
2181 assert!(global.applies_to_unit(&CurrencyUnit::Usd));
2182 assert!(!usd_only.applies_to_unit(&CurrencyUnit::Sat));
2183 assert!(usd_only.applies_to_unit(&CurrencyUnit::Usd));
2184 }
2185
2186 #[test]
2187 fn test_info_debug_with_special_chars() {
2188 let info = Info {
2190 url: "http://example.com".to_string(),
2191 listen_host: "127.0.0.1".to_string(),
2192 listen_port: 8080,
2193 mnemonic: Some("特殊字符 !@#$%^&*()".to_string()), ..Default::default()
2195 };
2196
2197 let debug_output = format!("{:?}", info);
2199
2200 assert!(!debug_output.contains("特殊字符 !@#$%^&*()"));
2202 assert!(debug_output.contains("<hashed: "));
2203 }
2204
2205 #[cfg(feature = "fakewallet")]
2206 #[test]
2207 fn test_multi_backend_config_parses() {
2208 use std::{env, fs};
2209
2210 let temp_dir = env::temp_dir().join("cdk_test_multi_backend_config");
2211 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2212 let config_path = temp_dir.join("config.toml");
2213
2214 let config_content = r#"
2215[[payment_backend]]
2216backend = "fakewallet"
2217unit = "sat"
2218min_mint = 1
2219max_mint = 500000
2220min_melt = 1
2221max_melt = 500000
2222
2223[[payment_backend]]
2224backend = "fakewallet"
2225unit = "eur"
2226min_mint = 1
2227max_mint = 1000
2228min_melt = 1
2229max_melt = 1000
2230"#;
2231 fs::write(&config_path, config_content).expect("Failed to write config file");
2232
2233 let settings = Settings::try_new(Some(&config_path)).expect("config should load");
2234
2235 assert_eq!(settings.payment_backend.len(), 2);
2236
2237 assert_eq!(
2238 settings.payment_backend[0].backend,
2239 PaymentBackendType::FakeWallet
2240 );
2241 assert_eq!(settings.payment_backend[0].unit, CurrencyUnit::Sat);
2242 let max_mint_0: u64 = settings.payment_backend[0].max_mint.into();
2243 assert_eq!(max_mint_0, 500_000);
2244
2245 assert_eq!(
2246 settings.payment_backend[1].backend,
2247 PaymentBackendType::FakeWallet
2248 );
2249 assert_eq!(settings.payment_backend[1].unit, CurrencyUnit::Eur);
2250 let max_mint_1: u64 = settings.payment_backend[1].max_mint.into();
2251 assert_eq!(max_mint_1, 1_000);
2252
2253 let _ = fs::remove_dir_all(&temp_dir);
2254 }
2255
2256 #[cfg(feature = "fakewallet")]
2257 #[test]
2258 fn test_single_payment_backend_block_parses() {
2259 use std::{env, fs};
2260
2261 let temp_dir = env::temp_dir().join("cdk_test_single_payment_backend_block");
2262 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2263 let config_path = temp_dir.join("config.toml");
2264
2265 let config_content = r#"
2266[payment_backend]
2267backend = "fakewallet"
2268min_mint = 1
2269max_mint = 500000
2270min_melt = 1
2271max_melt = 500000
2272"#;
2273 fs::write(&config_path, config_content).expect("Failed to write config file");
2274
2275 let settings = Settings::try_new(Some(&config_path)).expect("config should load");
2276
2277 assert_eq!(settings.payment_backend.len(), 1);
2278 assert_eq!(
2279 settings.payment_backend[0].backend,
2280 PaymentBackendType::FakeWallet
2281 );
2282 assert_eq!(settings.payment_backend[0].unit, CurrencyUnit::Sat);
2283
2284 let _ = fs::remove_dir_all(&temp_dir);
2285 }
2286
2287 #[cfg(feature = "fakewallet")]
2288 #[test]
2289 fn test_fakewallet_config_without_supported_units_parses() {
2290 use std::{env, fs};
2291
2292 let temp_dir = env::temp_dir().join("cdk_test_fakewallet_without_supported_units");
2293 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2294 let config_path = temp_dir.join("config.toml");
2295
2296 let config_content = r#"
2297[[payment_backend]]
2298backend = "fakewallet"
2299unit = "sat"
2300min_mint = 100
2301max_mint = 1000000
2302min_melt = 100
2303max_melt = 1000000
2304
2305[fake_wallet]
2306fee_percent = 0.02
2307reserve_fee_min = 1
2308min_delay_time = 1
2309max_delay_time = 3
2310"#;
2311 fs::write(&config_path, config_content).expect("Failed to write config file");
2312
2313 let settings = Settings::try_new(Some(&config_path)).expect("config should parse");
2314
2315 assert_eq!(settings.payment_backend.len(), 1);
2316 assert_eq!(settings.payment_backend[0].unit, CurrencyUnit::Sat);
2317 assert_eq!(
2318 settings
2319 .fake_wallet
2320 .expect("fake wallet section should parse")
2321 .supported_units,
2322 vec![CurrencyUnit::Sat]
2323 );
2324
2325 let _ = fs::remove_dir_all(&temp_dir);
2326 }
2327
2328 #[test]
2333 fn test_env_var_only_config_all_backends() {
2334 let _guard = config_env_lock();
2335
2336 #[cfg(feature = "lnd")]
2338 test_lnd_env_config();
2339
2340 #[cfg(feature = "cln")]
2341 test_cln_env_config();
2342
2343 #[cfg(feature = "fakewallet")]
2344 test_fakewallet_env_config();
2345
2346 #[cfg(feature = "grpc-processor")]
2347 test_grpc_processor_env_config();
2348
2349 #[cfg(feature = "ldk-node")]
2350 test_ldk_node_env_config();
2351 }
2352
2353 #[cfg(all(feature = "prometheus", feature = "fakewallet"))]
2354 #[test]
2355 fn test_prometheus_toml_config_survives_env_overlay() {
2356 use std::{env, fs};
2357
2358 let _guard = config_env_lock();
2359
2360 env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2361 env::remove_var(crate::env_vars::ENV_PROMETHEUS_ENABLED);
2362 env::remove_var(crate::env_vars::ENV_PROMETHEUS_ADDRESS);
2363 env::remove_var(crate::env_vars::ENV_PROMETHEUS_PORT);
2364
2365 let temp_dir =
2366 env::temp_dir().join(format!("cdk_prometheus_config_{}", std::process::id()));
2367 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2368 let config_path = temp_dir.join("config.toml");
2369
2370 let config_content = r#"
2371[info]
2372url = "http://127.0.0.1:8085"
2373listen_host = "127.0.0.1"
2374listen_port = 8085
2375
2376[payment_backend]
2377backend = "fakewallet"
2378min_mint = 1
2379max_mint = 500000
2380min_melt = 1
2381max_melt = 500000
2382
2383[prometheus]
2384enabled = true
2385address = "0.0.0.0"
2386port = 9090
2387"#;
2388 fs::write(&config_path, config_content).expect("Failed to write config file");
2389
2390 let mut settings = Settings::try_new(Some(&config_path)).expect("config should load");
2391 settings.from_env().expect("Failed to apply env vars");
2392
2393 let prometheus = settings
2394 .prometheus
2395 .as_ref()
2396 .expect("Prometheus config should be loaded from TOML");
2397 assert!(prometheus.enabled);
2398 assert_eq!(prometheus.address.as_deref(), Some("0.0.0.0"));
2399 assert_eq!(prometheus.port, Some(9090));
2400
2401 let _ = fs::remove_dir_all(&temp_dir);
2402 }
2403
2404 #[cfg(feature = "lnd")]
2405 fn test_lnd_env_config() {
2406 use std::path::PathBuf;
2407 use std::{env, fs};
2408
2409 let temp_dir = env::temp_dir().join("cdk_test_env_vars");
2411 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2412 let config_path = temp_dir.join("config.toml");
2413
2414 let config_content = r#"
2416[payment_backend]
2417backend = "lnd"
2418min_mint = 1
2419max_mint = 500000
2420min_melt = 1
2421max_melt = 500000
2422"#;
2423 fs::write(&config_path, config_content).expect("Failed to write config file");
2424
2425 env::set_var(crate::env_vars::ENV_PAYMENT_BACKEND, "lnd");
2427 env::set_var(crate::env_vars::ENV_LND_ADDRESS, "https://localhost:10009");
2428 env::set_var(crate::env_vars::ENV_LND_CERT_FILE, "/tmp/test_tls.cert");
2429 env::set_var(
2430 crate::env_vars::ENV_LND_MACAROON_FILE,
2431 "/tmp/test_admin.macaroon",
2432 );
2433 env::set_var(crate::env_vars::ENV_LND_FEE_PERCENT, "0.01");
2434 env::set_var(crate::env_vars::ENV_LND_RESERVE_FEE_MIN, "4");
2435
2436 let mut settings = Settings::try_new(Some(&config_path)).expect("Failed to load config");
2438 settings.from_env().expect("Failed to apply env vars");
2439
2440 assert!(settings.lnd.is_some());
2442 let lnd_config = settings.lnd.as_ref().unwrap();
2443 assert_eq!(lnd_config.address, "https://localhost:10009");
2444 assert_eq!(lnd_config.cert_file, PathBuf::from("/tmp/test_tls.cert"));
2445 assert_eq!(
2446 lnd_config.macaroon_file,
2447 PathBuf::from("/tmp/test_admin.macaroon")
2448 );
2449 assert_eq!(lnd_config.fee_percent, 0.01);
2450 let reserve_fee_u64: u64 = lnd_config.reserve_fee_min.into();
2451 assert_eq!(reserve_fee_u64, 4);
2452
2453 env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2455 env::remove_var(crate::env_vars::ENV_LND_ADDRESS);
2456 env::remove_var(crate::env_vars::ENV_LND_CERT_FILE);
2457 env::remove_var(crate::env_vars::ENV_LND_MACAROON_FILE);
2458 env::remove_var(crate::env_vars::ENV_LND_FEE_PERCENT);
2459 env::remove_var(crate::env_vars::ENV_LND_RESERVE_FEE_MIN);
2460
2461 let _ = fs::remove_dir_all(&temp_dir);
2463 }
2464
2465 #[cfg(feature = "cln")]
2466 fn test_cln_env_config() {
2467 use std::path::PathBuf;
2468 use std::{env, fs};
2469
2470 let temp_dir = env::temp_dir().join("cdk_test_env_vars_cln");
2472 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2473 let config_path = temp_dir.join("config.toml");
2474
2475 let config_content = r#"
2477[payment_backend]
2478backend = "cln"
2479min_mint = 1
2480max_mint = 500000
2481min_melt = 1
2482max_melt = 500000
2483"#;
2484 fs::write(&config_path, config_content).expect("Failed to write config file");
2485
2486 env::set_var(crate::env_vars::ENV_PAYMENT_BACKEND, "cln");
2488 env::set_var(crate::env_vars::ENV_CLN_RPC_PATH, "/tmp/lightning-rpc");
2489 env::set_var(crate::env_vars::ENV_CLN_BOLT12, "false");
2490 env::set_var(crate::env_vars::ENV_CLN_FEE_PERCENT, "0.01");
2491 env::set_var(crate::env_vars::ENV_CLN_RESERVE_FEE_MIN, "4");
2492
2493 let mut settings = Settings::try_new(Some(&config_path)).expect("Failed to load config");
2495 settings.from_env().expect("Failed to apply env vars");
2496
2497 assert!(settings.cln.is_some());
2499 let cln_config = settings.cln.as_ref().unwrap();
2500 assert_eq!(cln_config.rpc_path, PathBuf::from("/tmp/lightning-rpc"));
2501 assert!(!cln_config.bolt12);
2502 assert_eq!(cln_config.fee_percent, 0.01);
2503 let reserve_fee_u64: u64 = cln_config.reserve_fee_min.into();
2504 assert_eq!(reserve_fee_u64, 4);
2505
2506 env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2508 env::remove_var(crate::env_vars::ENV_CLN_RPC_PATH);
2509 env::remove_var(crate::env_vars::ENV_CLN_BOLT12);
2510 env::remove_var(crate::env_vars::ENV_CLN_FEE_PERCENT);
2511 env::remove_var(crate::env_vars::ENV_CLN_RESERVE_FEE_MIN);
2512
2513 let _ = fs::remove_dir_all(&temp_dir);
2515 }
2516
2517 #[cfg(feature = "fakewallet")]
2518 fn test_fakewallet_env_config() {
2519 use std::{env, fs};
2520
2521 let temp_dir = env::temp_dir().join("cdk_test_env_vars_fakewallet");
2523 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2524 let config_path = temp_dir.join("config.toml");
2525
2526 let config_content = r#"
2528[payment_backend]
2529backend = "fakewallet"
2530min_mint = 1
2531max_mint = 500000
2532min_melt = 1
2533max_melt = 500000
2534"#;
2535 fs::write(&config_path, config_content).expect("Failed to write config file");
2536
2537 env::set_var(crate::env_vars::ENV_PAYMENT_BACKEND, "fakewallet");
2539 env::set_var(crate::env_vars::ENV_FAKE_WALLET_SUPPORTED_UNITS, "sat,msat");
2540 env::set_var(crate::env_vars::ENV_FAKE_WALLET_FEE_PERCENT, "0.0");
2541 env::set_var(crate::env_vars::ENV_FAKE_WALLET_RESERVE_FEE_MIN, "0");
2542 env::set_var(
2543 crate::env_vars::ENV_FAKE_WALLET_CUSTOM_PAYMENT_METHODS,
2544 "venmo:msat,cashapp:sat,paypal",
2545 );
2546 env::set_var(crate::env_vars::ENV_FAKE_WALLET_MIN_DELAY, "0");
2547 env::set_var(crate::env_vars::ENV_FAKE_WALLET_MAX_DELAY, "5");
2548
2549 let mut settings = Settings::try_new(Some(&config_path)).expect("Failed to load config");
2551 settings.from_env().expect("Failed to apply env vars");
2552
2553 assert!(settings.fake_wallet.is_some());
2555 let fakewallet_config = settings.fake_wallet.as_ref().unwrap();
2556 assert_eq!(fakewallet_config.fee_percent, 0.0);
2557 let reserve_fee_u64: u64 = fakewallet_config.reserve_fee_min.into();
2558 assert_eq!(reserve_fee_u64, 0);
2559 assert_eq!(
2560 fakewallet_config.custom_payment_methods,
2561 vec![
2562 FakeWalletCustomPaymentMethod::MethodForUnit {
2563 method: "venmo".to_string(),
2564 unit: CurrencyUnit::Msat,
2565 },
2566 FakeWalletCustomPaymentMethod::MethodForUnit {
2567 method: "cashapp".to_string(),
2568 unit: CurrencyUnit::Sat,
2569 },
2570 FakeWalletCustomPaymentMethod::Method("paypal".to_string()),
2571 ]
2572 );
2573 assert_eq!(fakewallet_config.min_delay_time, 0);
2574 assert_eq!(fakewallet_config.max_delay_time, 5);
2575 assert_eq!(
2576 settings
2577 .payment_backend
2578 .iter()
2579 .map(|backend| backend.unit.clone())
2580 .collect::<Vec<_>>(),
2581 vec![CurrencyUnit::Sat, CurrencyUnit::Msat]
2582 );
2583
2584 env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2586 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_SUPPORTED_UNITS);
2587 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_FEE_PERCENT);
2588 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_RESERVE_FEE_MIN);
2589 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_CUSTOM_PAYMENT_METHODS);
2590 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_MIN_DELAY);
2591 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_MAX_DELAY);
2592
2593 let _ = fs::remove_dir_all(&temp_dir);
2595 }
2596
2597 #[cfg(feature = "grpc-processor")]
2598 fn test_grpc_processor_env_config() {
2599 use std::{env, fs};
2600
2601 let temp_dir = env::temp_dir().join("cdk_test_env_vars_grpc");
2603 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2604 let config_path = temp_dir.join("config.toml");
2605
2606 let config_content = r#"
2608[payment_backend]
2609backend = "grpcprocessor"
2610min_mint = 1
2611max_mint = 500000
2612min_melt = 1
2613max_melt = 500000
2614"#;
2615 fs::write(&config_path, config_content).expect("Failed to write config file");
2616
2617 env::set_var(crate::env_vars::ENV_PAYMENT_BACKEND, "grpcprocessor");
2619 env::set_var(
2620 crate::env_vars::ENV_GRPC_PROCESSOR_SUPPORTED_UNITS,
2621 "sat,msat",
2622 );
2623 env::set_var(crate::env_vars::ENV_GRPC_PROCESSOR_ADDRESS, "localhost");
2624 env::set_var(crate::env_vars::ENV_GRPC_PROCESSOR_PORT, "50051");
2625
2626 let mut settings = Settings::try_new(Some(&config_path)).expect("Failed to load config");
2628 settings.from_env().expect("Failed to apply env vars");
2629
2630 assert!(settings.grpc_processor.is_some());
2632 let grpc_config = settings.grpc_processor.as_ref().unwrap();
2633 assert_eq!(grpc_config.address, "localhost");
2634 assert_eq!(grpc_config.port, 50051);
2635
2636 env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2638 env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_SUPPORTED_UNITS);
2639 env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_ADDRESS);
2640 env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_PORT);
2641
2642 let _ = fs::remove_dir_all(&temp_dir);
2644 }
2645
2646 #[cfg(feature = "ldk-node")]
2647 fn test_ldk_node_env_config() {
2648 use std::{env, fs};
2649
2650 let temp_dir = env::temp_dir().join("cdk_test_env_vars_ldk");
2652 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2653 let config_path = temp_dir.join("config.toml");
2654
2655 let config_content = r#"
2657[payment_backend]
2658backend = "ldknode"
2659min_mint = 1
2660max_mint = 500000
2661min_melt = 1
2662max_melt = 500000
2663"#;
2664 fs::write(&config_path, config_content).expect("Failed to write config file");
2665
2666 env::set_var(crate::env_vars::ENV_PAYMENT_BACKEND, "ldknode");
2668 env::set_var(crate::env_vars::LDK_NODE_FEE_PERCENT_ENV_VAR, "0.01");
2669 env::set_var(crate::env_vars::LDK_NODE_RESERVE_FEE_MIN_ENV_VAR, "4");
2670 env::set_var(crate::env_vars::LDK_NODE_BITCOIN_NETWORK_ENV_VAR, "regtest");
2671 env::set_var(
2672 crate::env_vars::LDK_NODE_CHAIN_SOURCE_TYPE_ENV_VAR,
2673 "esplora",
2674 );
2675 env::set_var(
2676 crate::env_vars::LDK_NODE_ESPLORA_URL_ENV_VAR,
2677 "http://localhost:3000",
2678 );
2679 env::set_var(
2680 crate::env_vars::LDK_NODE_ELECTRUM_URL_ENV_VAR,
2681 "tcp://localhost:50001",
2682 );
2683 env::set_var(
2684 crate::env_vars::LDK_NODE_STORAGE_DIR_PATH_ENV_VAR,
2685 "/tmp/ldk",
2686 );
2687
2688 let mut settings = Settings::try_new(Some(&config_path)).expect("Failed to load config");
2690 settings.from_env().expect("Failed to apply env vars");
2691
2692 assert!(settings.ldk_node.is_some());
2694 let ldk_config = settings.ldk_node.as_ref().unwrap();
2695 assert_eq!(ldk_config.fee_percent, 0.01);
2696 let reserve_fee_u64: u64 = ldk_config.reserve_fee_min.into();
2697 assert_eq!(reserve_fee_u64, 4);
2698 assert_eq!(ldk_config.bitcoin_network, Some("regtest".to_string()));
2699 assert_eq!(ldk_config.chain_source_type, Some("esplora".to_string()));
2700 assert_eq!(
2701 ldk_config.esplora_url,
2702 Some("http://localhost:3000".to_string())
2703 );
2704 assert_eq!(
2705 ldk_config.electrum_url,
2706 Some("tcp://localhost:50001".to_string())
2707 );
2708 assert_eq!(ldk_config.storage_dir_path, Some("/tmp/ldk".to_string()));
2709
2710 env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2712 env::remove_var(crate::env_vars::LDK_NODE_FEE_PERCENT_ENV_VAR);
2713 env::remove_var(crate::env_vars::LDK_NODE_RESERVE_FEE_MIN_ENV_VAR);
2714 env::remove_var(crate::env_vars::LDK_NODE_BITCOIN_NETWORK_ENV_VAR);
2715 env::remove_var(crate::env_vars::LDK_NODE_CHAIN_SOURCE_TYPE_ENV_VAR);
2716 env::remove_var(crate::env_vars::LDK_NODE_ESPLORA_URL_ENV_VAR);
2717 env::remove_var(crate::env_vars::LDK_NODE_ELECTRUM_URL_ENV_VAR);
2718 env::remove_var(crate::env_vars::LDK_NODE_STORAGE_DIR_PATH_ENV_VAR);
2719
2720 let _ = fs::remove_dir_all(&temp_dir);
2722 }
2723}