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: None,
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: None,
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_tls_defaults_preserve_connection_url_policy() {
1405 let input = r#"url = "postgres://localhost/db?sslmode=verify-full""#;
1406 let postgres: PostgresConfig = toml::from_str(input).expect("PostgreSQL config");
1407 let auth: PostgresAuthConfig = toml::from_str(input).expect("auth PostgreSQL config");
1408 assert!(postgres.tls_mode.is_none());
1409 assert!(auth.tls_mode.is_none());
1410
1411 let explicit = format!("{input}\ntls_mode = \"require\"");
1412 let postgres: PostgresConfig =
1413 toml::from_str(&explicit).expect("explicit PostgreSQL config");
1414 let auth: PostgresAuthConfig =
1415 toml::from_str(&explicit).expect("explicit auth PostgreSQL config");
1416 assert_eq!(postgres.tls_mode.as_deref(), Some("require"));
1417 assert_eq!(auth.tls_mode.as_deref(), Some("require"));
1418 }
1419
1420 #[test]
1421 fn postgres_config_debug_redacts_connection_credentials() {
1422 let secret = "postgres-password-secret";
1423 let url = format!("postgres://mint:{secret}@db.example.com/cdk?token={secret}");
1424 let database = Database {
1425 engine: DatabaseEngine::Postgres,
1426 postgres: Some(PostgresConfig {
1427 url: url.clone(),
1428 ..Default::default()
1429 }),
1430 };
1431 let auth_database = AuthDatabase {
1432 postgres: Some(PostgresAuthConfig {
1433 url,
1434 ..Default::default()
1435 }),
1436 };
1437
1438 let database_debug = format!("{database:?}");
1439 let auth_debug = format!("{auth_database:?}");
1440
1441 assert!(database_debug.contains("postgres://db.example.com/cdk"));
1442 assert!(auth_debug.contains("postgres://db.example.com/cdk"));
1443 assert!(!database_debug.contains(secret));
1444 assert!(!auth_debug.contains(secret));
1445 }
1446
1447 #[cfg(feature = "management-rpc")]
1448 #[test]
1449 fn mint_quote_payment_override_is_explicitly_opt_in() {
1450 let default_settings =
1451 Settings::try_from_toml("").expect("default settings should deserialize");
1452 assert!(
1453 !default_settings
1454 .mint_management_rpc
1455 .unwrap_or_default()
1456 .allow_mint_quote_payment_override
1457 );
1458
1459 let enabled_settings = Settings::try_from_toml(
1460 r#"
1461[mint_management_rpc]
1462allow_mint_quote_payment_override = true
1463"#,
1464 )
1465 .expect("management RPC payment override should deserialize");
1466 assert!(
1467 enabled_settings
1468 .mint_management_rpc
1469 .expect("management RPC settings should be present")
1470 .allow_mint_quote_payment_override
1471 );
1472 }
1473
1474 #[cfg(feature = "ldk-node")]
1475 #[test]
1476 fn ldk_node_backend_accepts_supported_spellings() {
1477 for backend in ["ldk-node", "ldknode"] {
1478 let document = format!(
1479 r#"
1480[[payment_backend]]
1481backend = "{backend}"
1482unit = "sat"
1483"#
1484 );
1485
1486 let settings =
1487 Settings::try_from_toml(&document).expect("LDK Node backend should deserialize");
1488
1489 assert_eq!(
1490 settings
1491 .payment_backend
1492 .first()
1493 .expect("config should contain one payment backend")
1494 .backend,
1495 PaymentBackendType::LdkNode
1496 );
1497 }
1498 }
1499
1500 #[test]
1501 fn toml_parser_rejects_unknown_fields_with_full_paths() {
1502 let error = Settings::try_from_toml(
1503 r#"
1504[info]
1505listen_por = 8085
1506
1507[database]
1508engin = "sqlite"
1509"#,
1510 )
1511 .expect_err("misspelled fields must be rejected");
1512 let message = error.to_string();
1513 assert!(message.contains("database.engin"));
1514 assert!(message.contains("info.listen_por"));
1515 }
1516
1517 #[test]
1518 fn toml_parser_reports_legacy_ln_migration() {
1519 let error = Settings::try_from_toml(
1520 r#"
1521[ln]
1522ln_backend = "fakewallet"
1523"#,
1524 )
1525 .expect_err("legacy [ln] configuration must be rejected");
1526 let message = error.to_string();
1527 assert!(message.contains("legacy `[ln]`/`[[ln]]` configuration must be migrated"));
1528 assert!(message.contains("`[payment_backend]`/`[[payment_backend]]`"));
1529 assert!(message.contains("`cdk-mintd config migrate --file <old> --output <new>`"));
1530 }
1531
1532 #[test]
1533 fn legacy_parser_allowlist_does_not_hide_unrelated_typos() {
1534 let error = Settings::try_from_toml_allowing(
1535 r#"
1536[info]
1537signatory_url = "http://127.0.0.1:10009"
1538listen_por = 8085
1539"#,
1540 &["info.signatory_url"],
1541 )
1542 .expect_err("only exact legacy fields may be ignored");
1543 let message = error.to_string();
1544 assert!(message.contains("info.listen_por"));
1545 assert!(!message.contains("info.signatory_url"));
1546 }
1547
1548 fn config_env_lock() -> std::sync::MutexGuard<'static, ()> {
1549 crate::test_utils::env_lock()
1553 }
1554
1555 #[cfg(feature = "bdk")]
1556 fn clear_bdk_env_vars() {
1557 std::env::remove_var(crate::env_vars::BDK_MNEMONIC_ENV_VAR);
1558 std::env::remove_var(crate::env_vars::BDK_NETWORK_ENV_VAR);
1559 std::env::remove_var(crate::env_vars::BDK_CHAIN_SOURCE_TYPE_ENV_VAR);
1560 std::env::remove_var(crate::env_vars::BDK_ESPLORA_URL_ENV_VAR);
1561 std::env::remove_var(crate::env_vars::BDK_ELECTRUM_URL_ENV_VAR);
1562 std::env::remove_var(crate::env_vars::BDK_ELECTRUM_BATCH_SIZE_ENV_VAR);
1563 std::env::remove_var(crate::env_vars::BDK_WALLET_RESCAN_FROM_HEIGHT_ENV_VAR);
1564 std::env::remove_var(crate::env_vars::BDK_MIN_SEND_AMOUNT_SAT_ENV_VAR);
1565 std::env::remove_var(crate::env_vars::BDK_TARGET_BLOCK_TIME_SECS_ENV_VAR);
1566 std::env::remove_var(crate::env_vars::BDK_FEE_OPTIONS_ENV_VAR);
1567 std::env::remove_var(crate::env_vars::ENV_ONCHAIN_BACKEND);
1568 }
1569
1570 #[test]
1571 fn test_info_debug_impl() {
1572 let info = Info {
1574 url: "http://example.com".to_string(),
1575 listen_host: "127.0.0.1".to_string(),
1576 listen_port: 8080,
1577 mnemonic: Some("test secret mnemonic phrase".to_string()),
1578 input_fee_ppk: Some(100),
1579 ..Default::default()
1580 };
1581
1582 let debug_output = format!("{info:?}");
1584
1585 assert!(debug_output.contains("url: \"http://example.com\""));
1587 assert!(debug_output.contains("listen_host: \"127.0.0.1\""));
1588 assert!(debug_output.contains("listen_port: 8080"));
1589
1590 assert!(!debug_output.contains("test secret mnemonic phrase"));
1592 assert!(debug_output.contains("<hashed: "));
1593
1594 assert!(debug_output.contains("input_fee_ppk: Some(100)"));
1595 }
1596
1597 #[test]
1598 fn test_info_debug_with_empty_mnemonic() {
1599 let info = Info {
1601 url: "http://example.com".to_string(),
1602 listen_host: "127.0.0.1".to_string(),
1603 listen_port: 8080,
1604 mnemonic: Some("".to_string()), ..Default::default()
1606 };
1607
1608 let debug_output = format!("{:?}", info);
1610
1611 assert!(debug_output.contains("<hashed: "));
1613 }
1614
1615 #[cfg(feature = "bdk")]
1616 #[test]
1617 fn test_bdk_default_min_send_amount_sat() {
1618 assert_eq!(Bdk::default().min_send_amount_sat, 546);
1619 }
1620
1621 #[cfg(feature = "bdk")]
1622 #[test]
1623 fn test_bdk_default_electrum_batch_size() {
1624 assert_eq!(Bdk::default().electrum_batch_size, 5);
1625 }
1626
1627 #[cfg(feature = "bdk")]
1628 #[test]
1629 fn test_bdk_debug_redacts_secrets_and_url_credentials() {
1630 let config = Bdk {
1631 network: Some("regtest".to_string()),
1632 esplora_url: Some(
1633 "https://esplora-user:esplora-secret@example.com/esplora".to_string(),
1634 ),
1635 electrum_url: Some("ssl://electrum-user:electrum-secret@example.com:50002".to_string()),
1636 bitcoind_rpc_password: Some("rpc-password-secret".to_string()),
1637 mnemonic: Some("mnemonic-secret-words".to_string()),
1638 ..Default::default()
1639 };
1640
1641 let debug = format!("{config:?}");
1642
1643 assert!(debug.contains("regtest"));
1644 assert!(debug.contains("https://example.com/esplora"));
1645 assert!(debug.contains("ssl://example.com:50002"));
1646 assert!(debug.contains("[REDACTED]"));
1647 assert!(!debug.contains("esplora-user"));
1648 assert!(!debug.contains("esplora-secret"));
1649 assert!(!debug.contains("electrum-user"));
1650 assert!(!debug.contains("electrum-secret"));
1651 assert!(!debug.contains("rpc-password-secret"));
1652 assert!(!debug.contains("mnemonic-secret-words"));
1653 }
1654
1655 #[cfg(feature = "ldk-node")]
1656 #[test]
1657 fn test_ldk_node_debug_redacts_secrets_and_url_credentials() {
1658 let config = LdkNode {
1659 bitcoin_network: Some("regtest".to_string()),
1660 esplora_url: Some(
1661 "https://esplora-user:esplora-secret@example.com/esplora".to_string(),
1662 ),
1663 electrum_url: Some("ssl://electrum-user:electrum-secret@example.com:50002".to_string()),
1664 bitcoind_rpc_password: Some("rpc-password-secret".to_string()),
1665 rgs_url: Some("https://rgs-user:rgs-secret@example.com/snapshot".to_string()),
1666 ldk_node_mnemonic: Some("mnemonic-secret-words".to_string()),
1667 ..Default::default()
1668 };
1669
1670 let debug = format!("{config:?}");
1671
1672 assert!(debug.contains("regtest"));
1673 assert!(debug.contains("https://example.com/esplora"));
1674 assert!(debug.contains("ssl://example.com:50002"));
1675 assert!(debug.contains("https://example.com/snapshot"));
1676 assert!(debug.contains("[REDACTED]"));
1677 assert!(!debug.contains("esplora-user"));
1678 assert!(!debug.contains("esplora-secret"));
1679 assert!(!debug.contains("electrum-user"));
1680 assert!(!debug.contains("electrum-secret"));
1681 assert!(!debug.contains("rgs-user"));
1682 assert!(!debug.contains("rgs-secret"));
1683 assert!(!debug.contains("rpc-password-secret"));
1684 assert!(!debug.contains("mnemonic-secret-words"));
1685 }
1686
1687 #[cfg(feature = "bdk")]
1688 #[test]
1689 fn test_bdk_electrum_toml_config() {
1690 use std::{env, fs};
1691
1692 let temp_dir = env::temp_dir().join("cdk_test_bdk_electrum_config");
1693 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1694 let config_path = temp_dir.join("config.toml");
1695
1696 let config_content = r#"
1697[bdk]
1698network = "regtest"
1699chain_source_type = "electrum"
1700electrum_url = "tcp://127.0.0.1:50001"
1701electrum_batch_size = 11
1702"#;
1703 fs::write(&config_path, config_content).expect("Failed to write config file");
1704
1705 let settings = Settings::try_new(Some(&config_path))
1706 .expect("electrum config should parse successfully");
1707 let bdk = settings.bdk.expect("bdk config should be present");
1708
1709 assert_eq!(bdk.chain_source_type, Some("electrum".to_string()));
1710 assert_eq!(bdk.electrum_url, Some("tcp://127.0.0.1:50001".to_string()));
1711 assert_eq!(bdk.electrum_batch_size, 11);
1712
1713 let _ = fs::remove_dir_all(&temp_dir);
1714 }
1715
1716 #[cfg(feature = "bdk")]
1717 #[test]
1718 fn test_bdk_config_min_send_amount_sat_override() {
1719 use std::{env, fs};
1720
1721 let temp_dir = env::temp_dir().join("cdk_test_bdk_min_send_config");
1722 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1723 let config_path = temp_dir.join("config.toml");
1724
1725 let config_content = r#"
1726[bdk]
1727min_send_amount_sat = 1200
1728"#;
1729 fs::write(&config_path, config_content).expect("Failed to write config file");
1730
1731 let settings = Settings::try_new(Some(&config_path)).expect("config should load");
1732
1733 assert_eq!(
1734 settings
1735 .bdk
1736 .as_ref()
1737 .expect("bdk config should be present")
1738 .min_send_amount_sat,
1739 1200
1740 );
1741
1742 let _ = fs::remove_dir_all(&temp_dir);
1743 }
1744
1745 #[cfg(feature = "bdk")]
1746 #[test]
1747 fn test_bdk_env_min_send_amount_sat_override() {
1748 let _guard = config_env_lock();
1749 clear_bdk_env_vars();
1750 std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1751 std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1752 std::env::set_var(crate::env_vars::BDK_MIN_SEND_AMOUNT_SAT_ENV_VAR, "777");
1753
1754 let mut settings = Settings::default();
1755 settings.from_env().expect("Failed to apply env vars");
1756
1757 assert_eq!(
1758 settings
1759 .bdk
1760 .as_ref()
1761 .expect("bdk config should be present")
1762 .min_send_amount_sat,
1763 777
1764 );
1765
1766 clear_bdk_env_vars();
1767 }
1768
1769 #[cfg(feature = "bdk")]
1770 #[test]
1771 fn test_bdk_env_wallet_rescan_height_override() {
1772 let _guard = config_env_lock();
1773 clear_bdk_env_vars();
1774 std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1775 std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1776 std::env::set_var(
1777 crate::env_vars::BDK_WALLET_RESCAN_FROM_HEIGHT_ENV_VAR,
1778 "850000",
1779 );
1780
1781 let mut settings = Settings::default();
1782 settings.from_env().expect("Failed to apply env vars");
1783
1784 assert_eq!(
1785 settings
1786 .bdk
1787 .as_ref()
1788 .expect("bdk config should be present")
1789 .wallet_rescan_from_height,
1790 Some(850000)
1791 );
1792
1793 clear_bdk_env_vars();
1794 }
1795
1796 #[cfg(feature = "bdk")]
1797 #[test]
1798 fn test_bdk_env_electrum_config() {
1799 let _guard = config_env_lock();
1800 clear_bdk_env_vars();
1801 std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1802 std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1803 std::env::set_var(crate::env_vars::BDK_CHAIN_SOURCE_TYPE_ENV_VAR, "electrum");
1804 std::env::set_var(
1805 crate::env_vars::BDK_ELECTRUM_URL_ENV_VAR,
1806 "tcp://127.0.0.1:50001",
1807 );
1808 std::env::set_var(crate::env_vars::BDK_ELECTRUM_BATCH_SIZE_ENV_VAR, "9");
1809
1810 let mut settings = Settings::default();
1811 settings.from_env().expect("Failed to apply env vars");
1812
1813 let bdk = settings.bdk.expect("bdk config should be present");
1814 assert_eq!(bdk.chain_source_type, Some("electrum".to_string()));
1815 assert_eq!(bdk.electrum_url, Some("tcp://127.0.0.1:50001".to_string()));
1816 assert_eq!(bdk.electrum_batch_size, 9);
1817
1818 clear_bdk_env_vars();
1819 }
1820
1821 #[cfg(feature = "bdk")]
1822 #[test]
1823 fn test_bdk_default_fee_options_immediate_only() {
1824 assert_eq!(
1825 Bdk::default().batch_config.fee_options,
1826 vec!["immediate".to_string()]
1827 );
1828 }
1829
1830 #[cfg(feature = "bdk")]
1831 #[test]
1832 fn test_bdk_default_batch_deadlines_derive_from_target_block_time() {
1833 let batch_config: cdk_bdk::BatchConfig = Bdk::default().batch_config.into();
1834
1835 assert_eq!(
1836 batch_config.target_block_time,
1837 std::time::Duration::from_secs(600)
1838 );
1839 assert_eq!(
1840 batch_config.standard_deadline,
1841 std::time::Duration::from_secs(3600)
1842 );
1843 assert_eq!(
1844 batch_config.economy_deadline,
1845 std::time::Duration::from_secs(86_400)
1846 );
1847 assert_eq!(
1848 batch_config.max_intent_age,
1849 Some(std::time::Duration::from_secs(86_430))
1850 );
1851 }
1852
1853 #[cfg(feature = "bdk")]
1854 #[test]
1855 fn test_bdk_config_fee_options_override() {
1856 use std::{env, fs};
1857
1858 let temp_dir = env::temp_dir().join("cdk_test_bdk_fee_options_config");
1859 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1860 let config_path = temp_dir.join("config.toml");
1861
1862 let config_content = r#"
1863[bdk.batch_config]
1864fee_options = ["immediate", "economy"]
1865"#;
1866 fs::write(&config_path, config_content).expect("Failed to write config file");
1867
1868 let settings = Settings::try_new(Some(&config_path)).expect("config should load");
1869
1870 assert_eq!(
1871 settings
1872 .bdk
1873 .as_ref()
1874 .expect("bdk config should be present")
1875 .batch_config
1876 .fee_options,
1877 vec!["immediate".to_string(), "economy".to_string()]
1878 );
1879
1880 let _ = fs::remove_dir_all(&temp_dir);
1881 }
1882
1883 #[cfg(feature = "bdk")]
1884 #[test]
1885 fn test_bdk_config_target_block_time_derives_deadlines() {
1886 use std::{env, fs};
1887
1888 let temp_dir = env::temp_dir().join("cdk_test_bdk_target_block_time_config");
1889 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
1890 let config_path = temp_dir.join("config.toml");
1891
1892 let config_content = r#"
1893[bdk.batch_config]
1894target_block_time_secs = 300
1895"#;
1896 fs::write(&config_path, config_content).expect("Failed to write config file");
1897
1898 let settings = Settings::try_new(Some(&config_path)).expect("config should load");
1899 let batch_config: cdk_bdk::BatchConfig = settings
1900 .bdk
1901 .as_ref()
1902 .expect("bdk config should be present")
1903 .batch_config
1904 .clone()
1905 .into();
1906
1907 assert_eq!(
1908 batch_config.target_block_time,
1909 std::time::Duration::from_secs(300)
1910 );
1911 assert_eq!(
1912 batch_config.standard_deadline,
1913 std::time::Duration::from_secs(1800)
1914 );
1915 assert_eq!(
1916 batch_config.economy_deadline,
1917 std::time::Duration::from_secs(43_200)
1918 );
1919 assert_eq!(
1920 batch_config.max_intent_age,
1921 Some(std::time::Duration::from_secs(43_230))
1922 );
1923
1924 let _ = fs::remove_dir_all(&temp_dir);
1925 }
1926
1927 #[cfg(feature = "bdk")]
1928 #[test]
1929 fn test_bdk_env_fee_options_override() {
1930 let _guard = config_env_lock();
1931 clear_bdk_env_vars();
1932 std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1933 std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1934 std::env::set_var(
1935 crate::env_vars::BDK_FEE_OPTIONS_ENV_VAR,
1936 "immediate,standard,economy",
1937 );
1938
1939 let mut settings = Settings::default();
1940 settings.from_env().expect("Failed to apply env vars");
1941
1942 assert_eq!(
1943 settings
1944 .bdk
1945 .as_ref()
1946 .expect("bdk config should be present")
1947 .batch_config
1948 .fee_options,
1949 vec![
1950 "immediate".to_string(),
1951 "standard".to_string(),
1952 "economy".to_string()
1953 ]
1954 );
1955
1956 clear_bdk_env_vars();
1957 }
1958
1959 #[cfg(feature = "bdk")]
1960 #[test]
1961 fn test_bdk_env_target_block_time_override() {
1962 let _guard = config_env_lock();
1963 clear_bdk_env_vars();
1964 std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
1965 std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
1966 std::env::set_var(crate::env_vars::BDK_TARGET_BLOCK_TIME_SECS_ENV_VAR, "120");
1967
1968 let mut settings = Settings::default();
1969 settings.from_env().expect("Failed to apply env vars");
1970 let batch_config: cdk_bdk::BatchConfig = settings
1971 .bdk
1972 .as_ref()
1973 .expect("bdk config should be present")
1974 .batch_config
1975 .clone()
1976 .into();
1977
1978 assert_eq!(
1979 batch_config.target_block_time,
1980 std::time::Duration::from_secs(120)
1981 );
1982 assert_eq!(
1983 batch_config.standard_deadline,
1984 std::time::Duration::from_secs(720)
1985 );
1986 assert_eq!(
1987 batch_config.economy_deadline,
1988 std::time::Duration::from_secs(17_280)
1989 );
1990 assert_eq!(
1991 batch_config.max_intent_age,
1992 Some(std::time::Duration::from_secs(17_310))
1993 );
1994
1995 clear_bdk_env_vars();
1996 }
1997
1998 #[cfg(feature = "bdk")]
1999 #[test]
2000 fn test_bdk_invalid_fee_options_rejected() {
2001 for fee_options in [
2002 Vec::new(),
2003 vec!["immediate".to_string(), "immediate".to_string()],
2004 vec!["urgent".to_string()],
2005 vec![
2006 "immediate".to_string(),
2007 "standard".to_string(),
2008 "economy".to_string(),
2009 "immediate".to_string(),
2010 ],
2011 ] {
2012 let bdk = Bdk {
2013 batch_config: BatchConfig {
2014 fee_options,
2015 ..BatchConfig::default()
2016 },
2017 ..Default::default()
2018 };
2019
2020 let err = bdk.validate().expect_err("invalid fee options should fail");
2021
2022 assert!(err.contains("fee_options"));
2023 }
2024 }
2025
2026 #[cfg(feature = "bdk")]
2027 #[test]
2028 fn test_bdk_target_block_time_zero_rejected() {
2029 let bdk = Bdk {
2030 batch_config: BatchConfig {
2031 target_block_time_secs: 0,
2032 ..BatchConfig::default()
2033 },
2034 ..Default::default()
2035 };
2036
2037 let err = bdk
2038 .validate()
2039 .expect_err("zero target block time should fail");
2040
2041 assert!(err.contains("target_block_time_secs"));
2042 }
2043
2044 #[cfg(feature = "bdk")]
2045 #[test]
2046 fn test_bdk_min_send_amount_sat_zero_rejected() {
2047 let bdk = Bdk {
2048 min_send_amount_sat: 0,
2049 ..Default::default()
2050 };
2051
2052 let err = bdk.validate().expect_err("zero send minimum should fail");
2053
2054 assert!(err.contains("min_send_amount_sat"));
2055 }
2056
2057 #[cfg(all(feature = "fakewallet", feature = "bdk"))]
2058 #[test]
2059 fn test_fakewallet_backend_with_bdk_onchain_rejected() {
2060 let settings = Settings {
2061 payment_backend: vec![PaymentBackend {
2062 backend: PaymentBackendType::FakeWallet,
2063 ..Default::default()
2064 }],
2065 onchain: Some(Onchain {
2066 onchain_backend: OnchainBackend::Bdk,
2067 ..Default::default()
2068 }),
2069 ..Default::default()
2070 };
2071
2072 let err = settings
2073 .validate_backend_pairing()
2074 .expect_err("fake payment backend with BDK onchain should fail");
2075
2076 assert!(err.contains("fakewallet"));
2077 assert!(err.contains("bdk"));
2078 }
2079
2080 #[cfg(all(feature = "fakewallet", feature = "cln"))]
2081 #[test]
2082 fn test_real_backend_with_fakewallet_onchain_rejected() {
2083 let settings = Settings {
2084 payment_backend: vec![PaymentBackend {
2085 backend: PaymentBackendType::Cln,
2086 ..Default::default()
2087 }],
2088 onchain: Some(Onchain {
2089 onchain_backend: OnchainBackend::FakeWallet,
2090 ..Default::default()
2091 }),
2092 ..Default::default()
2093 };
2094
2095 let err = settings
2096 .validate_backend_pairing()
2097 .expect_err("real payment backend with fake onchain should fail");
2098
2099 assert!(err.contains("fakewallet"));
2100 assert!(err.contains("real payment backend"));
2101 }
2102
2103 #[cfg(feature = "fakewallet")]
2104 #[test]
2105 fn test_fakewallet_backend_with_fakewallet_onchain_accepted() {
2106 let settings = Settings {
2107 payment_backend: vec![PaymentBackend {
2108 backend: PaymentBackendType::FakeWallet,
2109 ..Default::default()
2110 }],
2111 onchain: Some(Onchain {
2112 onchain_backend: OnchainBackend::FakeWallet,
2113 ..Default::default()
2114 }),
2115 ..Default::default()
2116 };
2117
2118 settings
2119 .validate_backend_pairing()
2120 .expect("fake-only backend pairing should pass");
2121 }
2122
2123 #[cfg(feature = "fakewallet")]
2124 #[test]
2125 fn test_fakewallet_backend_with_no_onchain_accepted() {
2126 let settings = Settings {
2127 payment_backend: vec![PaymentBackend {
2128 backend: PaymentBackendType::FakeWallet,
2129 ..Default::default()
2130 }],
2131 onchain: Some(Onchain {
2132 onchain_backend: OnchainBackend::None,
2133 ..Default::default()
2134 }),
2135 ..Default::default()
2136 };
2137
2138 settings
2139 .validate_backend_pairing()
2140 .expect("fake payment backend without onchain should pass");
2141 }
2142
2143 #[cfg(feature = "fakewallet")]
2144 #[test]
2145 fn test_no_payment_backend_with_fakewallet_onchain_accepted() {
2146 let settings = Settings {
2147 payment_backend: vec![PaymentBackend {
2148 backend: PaymentBackendType::None,
2149 ..Default::default()
2150 }],
2151 onchain: Some(Onchain {
2152 onchain_backend: OnchainBackend::FakeWallet,
2153 ..Default::default()
2154 }),
2155 ..Default::default()
2156 };
2157
2158 settings
2159 .validate_backend_pairing()
2160 .expect("fake onchain-only backend pairing should pass");
2161 }
2162
2163 #[cfg(all(feature = "fakewallet", feature = "cln"))]
2164 #[test]
2165 fn test_fakewallet_backend_with_real_backend_rejected() {
2166 let settings = Settings {
2167 payment_backend: vec![
2168 PaymentBackend {
2169 backend: PaymentBackendType::FakeWallet,
2170 ..Default::default()
2171 },
2172 PaymentBackend {
2173 backend: PaymentBackendType::Cln,
2174 ..Default::default()
2175 },
2176 ],
2177 ..Default::default()
2178 };
2179
2180 let err = settings
2181 .validate_backend_pairing()
2182 .expect_err("fake payment backend combined with real backend should fail");
2183
2184 assert!(err.contains("fakewallet"));
2185 assert!(err.contains("real payment backend"));
2186 }
2187
2188 #[cfg(feature = "fakewallet")]
2189 #[test]
2190 fn test_fakewallet_custom_payment_method_unit_matching() {
2191 let global = FakeWalletCustomPaymentMethod::Method("paypal".to_string());
2192 let usd_only = FakeWalletCustomPaymentMethod::MethodForUnit {
2193 method: "venmo".to_string(),
2194 unit: CurrencyUnit::Usd,
2195 };
2196
2197 assert!(global.applies_to_unit(&CurrencyUnit::Sat));
2198 assert!(global.applies_to_unit(&CurrencyUnit::Usd));
2199 assert!(!usd_only.applies_to_unit(&CurrencyUnit::Sat));
2200 assert!(usd_only.applies_to_unit(&CurrencyUnit::Usd));
2201 }
2202
2203 #[test]
2204 fn test_info_debug_with_special_chars() {
2205 let info = Info {
2207 url: "http://example.com".to_string(),
2208 listen_host: "127.0.0.1".to_string(),
2209 listen_port: 8080,
2210 mnemonic: Some("特殊字符 !@#$%^&*()".to_string()), ..Default::default()
2212 };
2213
2214 let debug_output = format!("{:?}", info);
2216
2217 assert!(!debug_output.contains("特殊字符 !@#$%^&*()"));
2219 assert!(debug_output.contains("<hashed: "));
2220 }
2221
2222 #[cfg(feature = "fakewallet")]
2223 #[test]
2224 fn test_multi_backend_config_parses() {
2225 use std::{env, fs};
2226
2227 let temp_dir = env::temp_dir().join("cdk_test_multi_backend_config");
2228 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2229 let config_path = temp_dir.join("config.toml");
2230
2231 let config_content = r#"
2232[[payment_backend]]
2233backend = "fakewallet"
2234unit = "sat"
2235min_mint = 1
2236max_mint = 500000
2237min_melt = 1
2238max_melt = 500000
2239
2240[[payment_backend]]
2241backend = "fakewallet"
2242unit = "eur"
2243min_mint = 1
2244max_mint = 1000
2245min_melt = 1
2246max_melt = 1000
2247"#;
2248 fs::write(&config_path, config_content).expect("Failed to write config file");
2249
2250 let settings = Settings::try_new(Some(&config_path)).expect("config should load");
2251
2252 assert_eq!(settings.payment_backend.len(), 2);
2253
2254 assert_eq!(
2255 settings.payment_backend[0].backend,
2256 PaymentBackendType::FakeWallet
2257 );
2258 assert_eq!(settings.payment_backend[0].unit, CurrencyUnit::Sat);
2259 let max_mint_0: u64 = settings.payment_backend[0].max_mint.into();
2260 assert_eq!(max_mint_0, 500_000);
2261
2262 assert_eq!(
2263 settings.payment_backend[1].backend,
2264 PaymentBackendType::FakeWallet
2265 );
2266 assert_eq!(settings.payment_backend[1].unit, CurrencyUnit::Eur);
2267 let max_mint_1: u64 = settings.payment_backend[1].max_mint.into();
2268 assert_eq!(max_mint_1, 1_000);
2269
2270 let _ = fs::remove_dir_all(&temp_dir);
2271 }
2272
2273 #[cfg(feature = "fakewallet")]
2274 #[test]
2275 fn test_single_payment_backend_block_parses() {
2276 use std::{env, fs};
2277
2278 let temp_dir = env::temp_dir().join("cdk_test_single_payment_backend_block");
2279 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2280 let config_path = temp_dir.join("config.toml");
2281
2282 let config_content = r#"
2283[payment_backend]
2284backend = "fakewallet"
2285min_mint = 1
2286max_mint = 500000
2287min_melt = 1
2288max_melt = 500000
2289"#;
2290 fs::write(&config_path, config_content).expect("Failed to write config file");
2291
2292 let settings = Settings::try_new(Some(&config_path)).expect("config should load");
2293
2294 assert_eq!(settings.payment_backend.len(), 1);
2295 assert_eq!(
2296 settings.payment_backend[0].backend,
2297 PaymentBackendType::FakeWallet
2298 );
2299 assert_eq!(settings.payment_backend[0].unit, CurrencyUnit::Sat);
2300
2301 let _ = fs::remove_dir_all(&temp_dir);
2302 }
2303
2304 #[cfg(feature = "fakewallet")]
2305 #[test]
2306 fn test_fakewallet_config_without_supported_units_parses() {
2307 use std::{env, fs};
2308
2309 let temp_dir = env::temp_dir().join("cdk_test_fakewallet_without_supported_units");
2310 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2311 let config_path = temp_dir.join("config.toml");
2312
2313 let config_content = r#"
2314[[payment_backend]]
2315backend = "fakewallet"
2316unit = "sat"
2317min_mint = 100
2318max_mint = 1000000
2319min_melt = 100
2320max_melt = 1000000
2321
2322[fake_wallet]
2323fee_percent = 0.02
2324reserve_fee_min = 1
2325min_delay_time = 1
2326max_delay_time = 3
2327"#;
2328 fs::write(&config_path, config_content).expect("Failed to write config file");
2329
2330 let settings = Settings::try_new(Some(&config_path)).expect("config should parse");
2331
2332 assert_eq!(settings.payment_backend.len(), 1);
2333 assert_eq!(settings.payment_backend[0].unit, CurrencyUnit::Sat);
2334 assert_eq!(
2335 settings
2336 .fake_wallet
2337 .expect("fake wallet section should parse")
2338 .supported_units,
2339 vec![CurrencyUnit::Sat]
2340 );
2341
2342 let _ = fs::remove_dir_all(&temp_dir);
2343 }
2344
2345 #[test]
2350 fn test_env_var_only_config_all_backends() {
2351 let _guard = config_env_lock();
2352
2353 #[cfg(feature = "lnd")]
2355 test_lnd_env_config();
2356
2357 #[cfg(feature = "cln")]
2358 test_cln_env_config();
2359
2360 #[cfg(feature = "fakewallet")]
2361 test_fakewallet_env_config();
2362
2363 #[cfg(feature = "grpc-processor")]
2364 test_grpc_processor_env_config();
2365
2366 #[cfg(feature = "ldk-node")]
2367 test_ldk_node_env_config();
2368 }
2369
2370 #[cfg(all(feature = "prometheus", feature = "fakewallet"))]
2371 #[test]
2372 fn test_prometheus_toml_config_survives_env_overlay() {
2373 use std::{env, fs};
2374
2375 let _guard = config_env_lock();
2376
2377 env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2378 env::remove_var(crate::env_vars::ENV_PROMETHEUS_ENABLED);
2379 env::remove_var(crate::env_vars::ENV_PROMETHEUS_ADDRESS);
2380 env::remove_var(crate::env_vars::ENV_PROMETHEUS_PORT);
2381
2382 let temp_dir =
2383 env::temp_dir().join(format!("cdk_prometheus_config_{}", std::process::id()));
2384 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2385 let config_path = temp_dir.join("config.toml");
2386
2387 let config_content = r#"
2388[info]
2389url = "http://127.0.0.1:8085"
2390listen_host = "127.0.0.1"
2391listen_port = 8085
2392
2393[payment_backend]
2394backend = "fakewallet"
2395min_mint = 1
2396max_mint = 500000
2397min_melt = 1
2398max_melt = 500000
2399
2400[prometheus]
2401enabled = true
2402address = "0.0.0.0"
2403port = 9090
2404"#;
2405 fs::write(&config_path, config_content).expect("Failed to write config file");
2406
2407 let mut settings = Settings::try_new(Some(&config_path)).expect("config should load");
2408 settings.from_env().expect("Failed to apply env vars");
2409
2410 let prometheus = settings
2411 .prometheus
2412 .as_ref()
2413 .expect("Prometheus config should be loaded from TOML");
2414 assert!(prometheus.enabled);
2415 assert_eq!(prometheus.address.as_deref(), Some("0.0.0.0"));
2416 assert_eq!(prometheus.port, Some(9090));
2417
2418 let _ = fs::remove_dir_all(&temp_dir);
2419 }
2420
2421 #[cfg(feature = "lnd")]
2422 fn test_lnd_env_config() {
2423 use std::path::PathBuf;
2424 use std::{env, fs};
2425
2426 let temp_dir = env::temp_dir().join("cdk_test_env_vars");
2428 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2429 let config_path = temp_dir.join("config.toml");
2430
2431 let config_content = r#"
2433[payment_backend]
2434backend = "lnd"
2435min_mint = 1
2436max_mint = 500000
2437min_melt = 1
2438max_melt = 500000
2439"#;
2440 fs::write(&config_path, config_content).expect("Failed to write config file");
2441
2442 env::set_var(crate::env_vars::ENV_PAYMENT_BACKEND, "lnd");
2444 env::set_var(crate::env_vars::ENV_LND_ADDRESS, "https://localhost:10009");
2445 env::set_var(crate::env_vars::ENV_LND_CERT_FILE, "/tmp/test_tls.cert");
2446 env::set_var(
2447 crate::env_vars::ENV_LND_MACAROON_FILE,
2448 "/tmp/test_admin.macaroon",
2449 );
2450 env::set_var(crate::env_vars::ENV_LND_FEE_PERCENT, "0.01");
2451 env::set_var(crate::env_vars::ENV_LND_RESERVE_FEE_MIN, "4");
2452
2453 let mut settings = Settings::try_new(Some(&config_path)).expect("Failed to load config");
2455 settings.from_env().expect("Failed to apply env vars");
2456
2457 assert!(settings.lnd.is_some());
2459 let lnd_config = settings.lnd.as_ref().unwrap();
2460 assert_eq!(lnd_config.address, "https://localhost:10009");
2461 assert_eq!(lnd_config.cert_file, PathBuf::from("/tmp/test_tls.cert"));
2462 assert_eq!(
2463 lnd_config.macaroon_file,
2464 PathBuf::from("/tmp/test_admin.macaroon")
2465 );
2466 assert_eq!(lnd_config.fee_percent, 0.01);
2467 let reserve_fee_u64: u64 = lnd_config.reserve_fee_min.into();
2468 assert_eq!(reserve_fee_u64, 4);
2469
2470 env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2472 env::remove_var(crate::env_vars::ENV_LND_ADDRESS);
2473 env::remove_var(crate::env_vars::ENV_LND_CERT_FILE);
2474 env::remove_var(crate::env_vars::ENV_LND_MACAROON_FILE);
2475 env::remove_var(crate::env_vars::ENV_LND_FEE_PERCENT);
2476 env::remove_var(crate::env_vars::ENV_LND_RESERVE_FEE_MIN);
2477
2478 let _ = fs::remove_dir_all(&temp_dir);
2480 }
2481
2482 #[cfg(feature = "cln")]
2483 fn test_cln_env_config() {
2484 use std::path::PathBuf;
2485 use std::{env, fs};
2486
2487 let temp_dir = env::temp_dir().join("cdk_test_env_vars_cln");
2489 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2490 let config_path = temp_dir.join("config.toml");
2491
2492 let config_content = r#"
2494[payment_backend]
2495backend = "cln"
2496min_mint = 1
2497max_mint = 500000
2498min_melt = 1
2499max_melt = 500000
2500"#;
2501 fs::write(&config_path, config_content).expect("Failed to write config file");
2502
2503 env::set_var(crate::env_vars::ENV_PAYMENT_BACKEND, "cln");
2505 env::set_var(crate::env_vars::ENV_CLN_RPC_PATH, "/tmp/lightning-rpc");
2506 env::set_var(crate::env_vars::ENV_CLN_BOLT12, "false");
2507 env::set_var(crate::env_vars::ENV_CLN_FEE_PERCENT, "0.01");
2508 env::set_var(crate::env_vars::ENV_CLN_RESERVE_FEE_MIN, "4");
2509
2510 let mut settings = Settings::try_new(Some(&config_path)).expect("Failed to load config");
2512 settings.from_env().expect("Failed to apply env vars");
2513
2514 assert!(settings.cln.is_some());
2516 let cln_config = settings.cln.as_ref().unwrap();
2517 assert_eq!(cln_config.rpc_path, PathBuf::from("/tmp/lightning-rpc"));
2518 assert!(!cln_config.bolt12);
2519 assert_eq!(cln_config.fee_percent, 0.01);
2520 let reserve_fee_u64: u64 = cln_config.reserve_fee_min.into();
2521 assert_eq!(reserve_fee_u64, 4);
2522
2523 env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2525 env::remove_var(crate::env_vars::ENV_CLN_RPC_PATH);
2526 env::remove_var(crate::env_vars::ENV_CLN_BOLT12);
2527 env::remove_var(crate::env_vars::ENV_CLN_FEE_PERCENT);
2528 env::remove_var(crate::env_vars::ENV_CLN_RESERVE_FEE_MIN);
2529
2530 let _ = fs::remove_dir_all(&temp_dir);
2532 }
2533
2534 #[cfg(feature = "fakewallet")]
2535 fn test_fakewallet_env_config() {
2536 use std::{env, fs};
2537
2538 let temp_dir = env::temp_dir().join("cdk_test_env_vars_fakewallet");
2540 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2541 let config_path = temp_dir.join("config.toml");
2542
2543 let config_content = r#"
2545[payment_backend]
2546backend = "fakewallet"
2547min_mint = 1
2548max_mint = 500000
2549min_melt = 1
2550max_melt = 500000
2551"#;
2552 fs::write(&config_path, config_content).expect("Failed to write config file");
2553
2554 env::set_var(crate::env_vars::ENV_PAYMENT_BACKEND, "fakewallet");
2556 env::set_var(crate::env_vars::ENV_FAKE_WALLET_SUPPORTED_UNITS, "sat,msat");
2557 env::set_var(crate::env_vars::ENV_FAKE_WALLET_FEE_PERCENT, "0.0");
2558 env::set_var(crate::env_vars::ENV_FAKE_WALLET_RESERVE_FEE_MIN, "0");
2559 env::set_var(
2560 crate::env_vars::ENV_FAKE_WALLET_CUSTOM_PAYMENT_METHODS,
2561 "venmo:msat,cashapp:sat,paypal",
2562 );
2563 env::set_var(crate::env_vars::ENV_FAKE_WALLET_MIN_DELAY, "0");
2564 env::set_var(crate::env_vars::ENV_FAKE_WALLET_MAX_DELAY, "5");
2565
2566 let mut settings = Settings::try_new(Some(&config_path)).expect("Failed to load config");
2568 settings.from_env().expect("Failed to apply env vars");
2569
2570 assert!(settings.fake_wallet.is_some());
2572 let fakewallet_config = settings.fake_wallet.as_ref().unwrap();
2573 assert_eq!(fakewallet_config.fee_percent, 0.0);
2574 let reserve_fee_u64: u64 = fakewallet_config.reserve_fee_min.into();
2575 assert_eq!(reserve_fee_u64, 0);
2576 assert_eq!(
2577 fakewallet_config.custom_payment_methods,
2578 vec![
2579 FakeWalletCustomPaymentMethod::MethodForUnit {
2580 method: "venmo".to_string(),
2581 unit: CurrencyUnit::Msat,
2582 },
2583 FakeWalletCustomPaymentMethod::MethodForUnit {
2584 method: "cashapp".to_string(),
2585 unit: CurrencyUnit::Sat,
2586 },
2587 FakeWalletCustomPaymentMethod::Method("paypal".to_string()),
2588 ]
2589 );
2590 assert_eq!(fakewallet_config.min_delay_time, 0);
2591 assert_eq!(fakewallet_config.max_delay_time, 5);
2592 assert_eq!(
2593 settings
2594 .payment_backend
2595 .iter()
2596 .map(|backend| backend.unit.clone())
2597 .collect::<Vec<_>>(),
2598 vec![CurrencyUnit::Sat, CurrencyUnit::Msat]
2599 );
2600
2601 env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2603 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_SUPPORTED_UNITS);
2604 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_FEE_PERCENT);
2605 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_RESERVE_FEE_MIN);
2606 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_CUSTOM_PAYMENT_METHODS);
2607 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_MIN_DELAY);
2608 env::remove_var(crate::env_vars::ENV_FAKE_WALLET_MAX_DELAY);
2609
2610 let _ = fs::remove_dir_all(&temp_dir);
2612 }
2613
2614 #[cfg(feature = "grpc-processor")]
2615 fn test_grpc_processor_env_config() {
2616 use std::{env, fs};
2617
2618 let temp_dir = env::temp_dir().join("cdk_test_env_vars_grpc");
2620 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2621 let config_path = temp_dir.join("config.toml");
2622
2623 let config_content = r#"
2625[payment_backend]
2626backend = "grpcprocessor"
2627min_mint = 1
2628max_mint = 500000
2629min_melt = 1
2630max_melt = 500000
2631"#;
2632 fs::write(&config_path, config_content).expect("Failed to write config file");
2633
2634 env::set_var(crate::env_vars::ENV_PAYMENT_BACKEND, "grpcprocessor");
2636 env::set_var(
2637 crate::env_vars::ENV_GRPC_PROCESSOR_SUPPORTED_UNITS,
2638 "sat,msat",
2639 );
2640 env::set_var(crate::env_vars::ENV_GRPC_PROCESSOR_ADDRESS, "localhost");
2641 env::set_var(crate::env_vars::ENV_GRPC_PROCESSOR_PORT, "50051");
2642
2643 let mut settings = Settings::try_new(Some(&config_path)).expect("Failed to load config");
2645 settings.from_env().expect("Failed to apply env vars");
2646
2647 assert!(settings.grpc_processor.is_some());
2649 let grpc_config = settings.grpc_processor.as_ref().unwrap();
2650 assert_eq!(grpc_config.address, "localhost");
2651 assert_eq!(grpc_config.port, 50051);
2652
2653 env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2655 env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_SUPPORTED_UNITS);
2656 env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_ADDRESS);
2657 env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_PORT);
2658
2659 let _ = fs::remove_dir_all(&temp_dir);
2661 }
2662
2663 #[cfg(feature = "ldk-node")]
2664 fn test_ldk_node_env_config() {
2665 use std::{env, fs};
2666
2667 let temp_dir = env::temp_dir().join("cdk_test_env_vars_ldk");
2669 fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
2670 let config_path = temp_dir.join("config.toml");
2671
2672 let config_content = r#"
2674[payment_backend]
2675backend = "ldknode"
2676min_mint = 1
2677max_mint = 500000
2678min_melt = 1
2679max_melt = 500000
2680"#;
2681 fs::write(&config_path, config_content).expect("Failed to write config file");
2682
2683 env::set_var(crate::env_vars::ENV_PAYMENT_BACKEND, "ldknode");
2685 env::set_var(crate::env_vars::LDK_NODE_FEE_PERCENT_ENV_VAR, "0.01");
2686 env::set_var(crate::env_vars::LDK_NODE_RESERVE_FEE_MIN_ENV_VAR, "4");
2687 env::set_var(crate::env_vars::LDK_NODE_BITCOIN_NETWORK_ENV_VAR, "regtest");
2688 env::set_var(
2689 crate::env_vars::LDK_NODE_CHAIN_SOURCE_TYPE_ENV_VAR,
2690 "esplora",
2691 );
2692 env::set_var(
2693 crate::env_vars::LDK_NODE_ESPLORA_URL_ENV_VAR,
2694 "http://localhost:3000",
2695 );
2696 env::set_var(
2697 crate::env_vars::LDK_NODE_ELECTRUM_URL_ENV_VAR,
2698 "tcp://localhost:50001",
2699 );
2700 env::set_var(
2701 crate::env_vars::LDK_NODE_STORAGE_DIR_PATH_ENV_VAR,
2702 "/tmp/ldk",
2703 );
2704
2705 let mut settings = Settings::try_new(Some(&config_path)).expect("Failed to load config");
2707 settings.from_env().expect("Failed to apply env vars");
2708
2709 assert!(settings.ldk_node.is_some());
2711 let ldk_config = settings.ldk_node.as_ref().unwrap();
2712 assert_eq!(ldk_config.fee_percent, 0.01);
2713 let reserve_fee_u64: u64 = ldk_config.reserve_fee_min.into();
2714 assert_eq!(reserve_fee_u64, 4);
2715 assert_eq!(ldk_config.bitcoin_network, Some("regtest".to_string()));
2716 assert_eq!(ldk_config.chain_source_type, Some("esplora".to_string()));
2717 assert_eq!(
2718 ldk_config.esplora_url,
2719 Some("http://localhost:3000".to_string())
2720 );
2721 assert_eq!(
2722 ldk_config.electrum_url,
2723 Some("tcp://localhost:50001".to_string())
2724 );
2725 assert_eq!(ldk_config.storage_dir_path, Some("/tmp/ldk".to_string()));
2726
2727 env::remove_var(crate::env_vars::ENV_PAYMENT_BACKEND);
2729 env::remove_var(crate::env_vars::LDK_NODE_FEE_PERCENT_ENV_VAR);
2730 env::remove_var(crate::env_vars::LDK_NODE_RESERVE_FEE_MIN_ENV_VAR);
2731 env::remove_var(crate::env_vars::LDK_NODE_BITCOIN_NETWORK_ENV_VAR);
2732 env::remove_var(crate::env_vars::LDK_NODE_CHAIN_SOURCE_TYPE_ENV_VAR);
2733 env::remove_var(crate::env_vars::LDK_NODE_ESPLORA_URL_ENV_VAR);
2734 env::remove_var(crate::env_vars::LDK_NODE_ELECTRUM_URL_ENV_VAR);
2735 env::remove_var(crate::env_vars::LDK_NODE_STORAGE_DIR_PATH_ENV_VAR);
2736
2737 let _ = fs::remove_dir_all(&temp_dir);
2739 }
2740}