1use std::time::Duration;
2
3use serde::{Deserialize, Serialize};
4
5pub const DEFAULT_TARGET_BLOCK_TIME_SECS: u64 = 600;
7
8#[derive(Debug, Clone)]
16pub struct FeeEstimationConfig {
17 pub fallback_sat_per_vb: f64,
19 pub cache_ttl_secs: u64,
21 pub quote_max_input_count: usize,
23 pub quote_fixed_safety_sat: u64,
25 pub quote_safety_multiplier: f64,
27}
28
29impl Default for FeeEstimationConfig {
30 fn default() -> Self {
31 Self {
32 fallback_sat_per_vb: 2.0,
33 cache_ttl_secs: 60,
34 quote_max_input_count: 24,
35 quote_fixed_safety_sat: 500,
36 quote_safety_multiplier: 1.25,
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
43pub struct BatchConfig {
44 pub poll_interval: Duration,
46 pub max_batch_size: usize,
48 pub target_block_time: Duration,
50 pub standard_deadline: Duration,
52 pub economy_deadline: Duration,
54 pub max_intent_age: Option<Duration>,
57 pub fee_options: Vec<PaymentTier>,
60 pub fee_estimation: FeeEstimationConfig,
62}
63
64impl Default for BatchConfig {
65 fn default() -> Self {
66 let poll_interval = Duration::from_secs(30);
67 let target_block_time = Duration::from_secs(DEFAULT_TARGET_BLOCK_TIME_SECS);
68 let standard_deadline =
69 Self::deadline_for_target_blocks(PaymentTier::Standard, target_block_time);
70 let economy_deadline =
71 Self::deadline_for_target_blocks(PaymentTier::Economy, target_block_time);
72
73 Self {
74 poll_interval,
75 max_batch_size: 50,
76 target_block_time,
77 standard_deadline,
78 economy_deadline,
79 max_intent_age: Some(economy_deadline.saturating_add(poll_interval)),
80 fee_options: vec![PaymentTier::Immediate],
81 fee_estimation: FeeEstimationConfig::default(),
82 }
83 }
84}
85
86impl BatchConfig {
87 pub fn deadline_for_target_blocks(tier: PaymentTier, target_block_time: Duration) -> Duration {
89 Duration::from_secs(
90 target_block_time
91 .as_secs()
92 .saturating_mul(u64::from(tier.estimated_blocks())),
93 )
94 }
95
96 pub fn validate(&self) -> Result<(), String> {
98 if self.target_block_time.is_zero() {
99 return Err("BDK batch_config.target_block_time must be greater than zero".to_string());
100 }
101
102 if !self.fee_estimation.fallback_sat_per_vb.is_finite()
103 || self.fee_estimation.fallback_sat_per_vb <= 0.0
104 || self.fee_estimation.fallback_sat_per_vb.ceil() > f64::from(u32::MAX)
105 {
106 return Err(
107 "BDK batch_config.fee_estimation.fallback_sat_per_vb must be finite, greater than zero, and fit in u32 after rounding"
108 .to_string(),
109 );
110 }
111
112 validate_fee_options(&self.fee_options)
113 }
114
115 pub fn tier_for_fee_index(&self, fee_index: Option<u32>) -> Result<PaymentTier, u32> {
117 let Some(fee_index) = fee_index else {
118 return Ok(PaymentTier::Immediate);
119 };
120
121 self.fee_options
122 .get(fee_index as usize)
123 .copied()
124 .ok_or(fee_index)
125 }
126}
127
128#[derive(Debug, Clone)]
130pub struct SyncConfig {
131 pub apply_chunk_size: usize,
133 pub lock_hold_warn_ms: u64,
135}
136
137impl Default for SyncConfig {
138 fn default() -> Self {
139 Self {
140 apply_chunk_size: 16,
141 lock_hold_warn_ms: 500,
142 }
143 }
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
152pub enum PaymentTier {
153 #[default]
155 Immediate,
156 Standard,
159 Economy,
162}
163
164impl PaymentTier {
165 pub fn from_config_name(s: &str) -> Option<Self> {
167 match s.trim().to_ascii_lowercase().as_str() {
168 "immediate" => Some(Self::Immediate),
169 "standard" => Some(Self::Standard),
170 "economy" => Some(Self::Economy),
171 _ => None,
172 }
173 }
174
175 pub fn config_name(self) -> &'static str {
177 match self {
178 Self::Immediate => "immediate",
179 Self::Standard => "standard",
180 Self::Economy => "economy",
181 }
182 }
183
184 pub fn estimated_blocks(self) -> u32 {
186 match self {
187 Self::Immediate => 1,
188 Self::Standard => 6,
189 Self::Economy => 144,
190 }
191 }
192
193 pub fn from_optional_str(s: Option<&str>) -> Self {
198 let Some(value) = s else {
199 return Self::default();
200 };
201
202 if value.eq_ignore_ascii_case("immediate") {
203 Self::Immediate
204 } else if value.eq_ignore_ascii_case("standard") {
205 Self::Standard
206 } else if value.eq_ignore_ascii_case("economy") {
207 Self::Economy
208 } else {
209 Self::default()
210 }
211 }
212}
213
214pub fn validate_fee_options(fee_options: &[PaymentTier]) -> Result<(), String> {
216 if fee_options.is_empty() {
217 return Err("BDK batch_config.fee_options must not be empty".to_string());
218 }
219
220 if fee_options.len() > 3 {
221 return Err("BDK batch_config.fee_options must contain at most 3 entries".to_string());
222 }
223
224 for (idx, tier) in fee_options.iter().enumerate() {
225 if fee_options[..idx].contains(tier) {
226 return Err(format!(
227 "BDK batch_config.fee_options contains duplicate tier '{}'",
228 tier.config_name()
229 ));
230 }
231 }
232
233 Ok(())
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
241pub struct PaymentMetadata {
242 pub entries: std::collections::HashMap<String, String>,
244}
245
246impl PaymentMetadata {
247 pub fn from_optional_json(json: Option<&str>) -> Self {
252 let Some(s) = json else {
253 return Self::default();
254 };
255 if let Ok(meta) = serde_json::from_str::<PaymentMetadata>(s) {
257 return meta;
258 }
259 if let Ok(entries) = serde_json::from_str::<std::collections::HashMap<String, String>>(s) {
261 return Self { entries };
262 }
263 Self::default()
264 }
265}