Skip to main content

cdk_bdk/
types.rs

1use std::time::Duration;
2
3use serde::{Deserialize, Serialize};
4
5/// Default average Bitcoin block interval used for delayed batch deadlines.
6pub const DEFAULT_TARGET_BLOCK_TIME_SECS: u64 = 600;
7
8/// Configuration for BDK fee estimation.
9///
10/// Fee rates are cached per payment tier. Melt quote fees use a conservative
11/// weight estimate because the quote is created before BDK performs final coin
12/// selection. These knobs expose the operator-facing tradeoffs: fallback fee
13/// rate, maximum quote size, and quote safety padding. Internal constants cover
14/// the lower-level wallet sampling and input weight assumptions.
15#[derive(Debug, Clone)]
16pub struct FeeEstimationConfig {
17    /// Fee rate used when chain-source estimation fails, in sat/vB.
18    pub fallback_sat_per_vb: f64,
19    /// How long a per-tier fee-rate estimate is cached, in seconds.
20    pub cache_ttl_secs: u64,
21    /// Maximum input count reserved at quote time.
22    pub quote_max_input_count: usize,
23    /// Fixed safety margin added to quote-time fee estimates, in sats.
24    pub quote_fixed_safety_sat: u64,
25    /// Multiplicative safety margin applied after the raw quote fee estimate.
26    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/// Configuration for the background batch processor
42#[derive(Debug, Clone)]
43pub struct BatchConfig {
44    /// How often the batch processor wakes up to check for ready intents
45    pub poll_interval: Duration,
46    /// Maximum number of intents to include in a single batch
47    pub max_batch_size: usize,
48    /// Average block interval used to derive default delayed tier deadlines.
49    pub target_block_time: Duration,
50    /// How long standard-tier intents wait before being eligible
51    pub standard_deadline: Duration,
52    /// How long economy-tier intents wait before being eligible
53    pub economy_deadline: Duration,
54    /// Maximum age for a pending intent before it is expired and removed.
55    /// Set to `None` to disable automatic expiry.
56    pub max_intent_age: Option<Duration>,
57    /// Fee tiers exposed in melt quotes. The configured order defines the
58    /// backend-owned `fee_index` values.
59    pub fee_options: Vec<PaymentTier>,
60    /// Fee estimation configuration
61    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    /// Derive a delayed tier deadline from its advertised confirmation target.
88    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    /// Validate operator-selected fee tiers.
97    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    /// Resolve a wallet-selected fee index to the configured tier.
116    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/// Configuration for blockchain synchronization
129#[derive(Debug, Clone)]
130pub struct SyncConfig {
131    /// Number of blocks to apply per wallet-lock acquisition (RPC path)
132    pub apply_chunk_size: usize,
133    /// Warn if a single lock acquisition exceeds this duration (milliseconds)
134    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/// Batching tier for on-chain send intents
147///
148/// Controls when a send intent is eligible for inclusion in a batch.
149/// `Immediate` intents are processed right away; `Standard` and `Economy`
150/// intents wait until their respective deadlines.
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
152pub enum PaymentTier {
153    /// Process immediately without waiting for other intents
154    #[default]
155    Immediate,
156    /// Process when the standard deadline is reached or an immediate batch
157    /// is available
158    Standard,
159    /// Process when the economy deadline is reached or an immediate batch
160    /// is available
161    Economy,
162}
163
164impl PaymentTier {
165    /// Parse a tier from a configuration name.
166    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    /// Stable configuration name for this tier.
176    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    /// Target confirmation blocks advertised for this tier.
185    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    /// Parse a tier from an optional string value.
194    ///
195    /// Returns `Immediate` when `None` is provided or the string is
196    /// unrecognized.
197    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
214/// Validate an ordered list of exposed fee tiers.
215pub 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/// Opaque key-value metadata attached to a send intent
237///
238/// Stored for future extensions. In v1 no behavior is driven by metadata
239/// values. Future features like payjoin may consume this metadata.
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
241pub struct PaymentMetadata {
242    /// Key-value pairs
243    pub entries: std::collections::HashMap<String, String>,
244}
245
246impl PaymentMetadata {
247    /// Create metadata from an optional JSON string.
248    ///
249    /// Accepts either a bare `{"key": "value"}` object (interpreted as the
250    /// entries map) or the full struct form `{"entries": {"key": "value"}}`.
251    pub fn from_optional_json(json: Option<&str>) -> Self {
252        let Some(s) = json else {
253            return Self::default();
254        };
255        // Try deserializing as full struct first
256        if let Ok(meta) = serde_json::from_str::<PaymentMetadata>(s) {
257            return meta;
258        }
259        // Fall back to interpreting the JSON as a bare key-value map
260        if let Ok(entries) = serde_json::from_str::<std::collections::HashMap<String, String>>(s) {
261            return Self { entries };
262        }
263        Self::default()
264    }
265}