1use crate::client::BitcoinClient;
7use crate::error::BitcoinError;
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13pub enum TransactionUrgency {
14 Low,
16 Medium,
18 High,
20 Critical,
22}
23
24impl TransactionUrgency {
25 pub fn target_blocks(&self) -> u32 {
27 match self {
28 Self::Low => 144, Self::Medium => 6, Self::High => 2, Self::Critical => 1, }
33 }
34
35 pub fn fee_multiplier(&self) -> f64 {
37 match self {
38 Self::Low => 1.0,
39 Self::Medium => 1.5,
40 Self::High => 2.0,
41 Self::Critical => 3.0,
42 }
43 }
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct TimeBasedStrategy {
49 pub target_time: DateTime<Utc>,
51 pub min_fee_rate: f64,
53 pub max_fee_rate: f64,
55 pub allow_bumping: bool,
57}
58
59impl TimeBasedStrategy {
60 pub fn new(target_time: DateTime<Utc>, min_fee_rate: f64, max_fee_rate: f64) -> Self {
62 Self {
63 target_time,
64 min_fee_rate,
65 max_fee_rate,
66 allow_bumping: true,
67 }
68 }
69
70 pub fn calculate_fee_rate(&self, current_fee_rate: f64) -> f64 {
72 let now = Utc::now();
73 let time_remaining = self.target_time.signed_duration_since(now);
74
75 if time_remaining.num_seconds() <= 0 {
76 return self.max_fee_rate;
78 }
79
80 let hours_remaining = time_remaining.num_hours() as f64;
82 let multiplier = if hours_remaining < 1.0 {
83 3.0 } else if hours_remaining < 6.0 {
85 2.0 } else if hours_remaining < 24.0 {
87 1.5 } else {
89 1.0 };
91
92 (current_fee_rate * multiplier)
93 .max(self.min_fee_rate)
94 .min(self.max_fee_rate)
95 }
96
97 pub fn should_bump_fee(&self) -> bool {
99 if !self.allow_bumping {
100 return false;
101 }
102
103 let now = Utc::now();
104 let time_remaining = self.target_time.signed_duration_since(now);
105
106 time_remaining.num_hours() < 2
108 }
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct BudgetStrategy {
114 pub max_fee_satoshis: u64,
116 pub tx_vbytes: usize,
118 pub min_fee_rate: f64,
120}
121
122impl BudgetStrategy {
123 pub fn new(max_fee_satoshis: u64, tx_vbytes: usize) -> Self {
125 Self {
126 max_fee_satoshis,
127 tx_vbytes,
128 min_fee_rate: 1.0,
129 }
130 }
131
132 pub fn max_fee_rate(&self) -> f64 {
134 self.max_fee_satoshis as f64 / self.tx_vbytes as f64
135 }
136
137 pub fn calculate_fee_rate(&self, market_fee_rate: f64) -> Result<f64, BitcoinError> {
139 let max_rate = self.max_fee_rate();
140
141 if max_rate < self.min_fee_rate {
142 return Err(BitcoinError::InsufficientFunds(
143 "Budget too low for minimum fee rate".to_string(),
144 ));
145 }
146
147 Ok(market_fee_rate.min(max_rate).max(self.min_fee_rate))
148 }
149
150 pub fn fits_budget(&self, fee_rate: f64) -> bool {
152 let total_fee = (fee_rate * self.tx_vbytes as f64) as u64;
153 total_fee <= self.max_fee_satoshis
154 }
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct MultiTxFeeStrategy {
160 pub total_budget: u64,
162 pub transactions: Vec<PlannedTransaction>,
164 pub min_fee_rate: f64,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct PlannedTransaction {
171 pub id: String,
173 pub vbytes: usize,
175 pub urgency: TransactionUrgency,
177 pub max_fee: Option<u64>,
179}
180
181impl MultiTxFeeStrategy {
182 pub fn new(total_budget: u64, min_fee_rate: f64) -> Self {
184 Self {
185 total_budget,
186 transactions: Vec::new(),
187 min_fee_rate,
188 }
189 }
190
191 pub fn add_transaction(&mut self, tx: PlannedTransaction) {
193 self.transactions.push(tx);
194 }
195
196 pub fn calculate_fee_allocation(
198 &self,
199 market_fee_rate: f64,
200 ) -> Result<Vec<TxFeeAllocation>, BitcoinError> {
201 if self.transactions.is_empty() {
202 return Ok(Vec::new());
203 }
204
205 let mut allocations = Vec::new();
207
208 let mut remaining_budget = self.total_budget;
210
211 for tx in &self.transactions {
212 let priority_multiplier = tx.urgency.fee_multiplier();
213 let base_fee = (tx.vbytes as f64 * market_fee_rate * priority_multiplier) as u64;
214
215 let allocated_fee = if let Some(max_fee) = tx.max_fee {
217 base_fee.min(max_fee)
218 } else {
219 base_fee
220 };
221
222 let allocated_fee = allocated_fee.min(remaining_budget);
223 remaining_budget = remaining_budget.saturating_sub(allocated_fee);
224
225 let fee_rate = allocated_fee as f64 / tx.vbytes as f64;
226
227 allocations.push(TxFeeAllocation {
228 tx_id: tx.id.clone(),
229 allocated_fee,
230 fee_rate,
231 vbytes: tx.vbytes,
232 urgency: tx.urgency,
233 });
234 }
235
236 for alloc in &allocations {
238 if alloc.fee_rate < self.min_fee_rate {
239 return Err(BitcoinError::InsufficientFunds(format!(
240 "Insufficient budget to meet minimum fee rate for tx: {}",
241 alloc.tx_id
242 )));
243 }
244 }
245
246 Ok(allocations)
247 }
248
249 pub fn total_cost_at_rate(&self, fee_rate: f64) -> u64 {
251 self.transactions
252 .iter()
253 .map(|tx| {
254 let multiplier = tx.urgency.fee_multiplier();
255 (tx.vbytes as f64 * fee_rate * multiplier) as u64
256 })
257 .sum()
258 }
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct TxFeeAllocation {
264 pub tx_id: String,
266 pub allocated_fee: u64,
268 pub fee_rate: f64,
270 pub vbytes: usize,
272 pub urgency: TransactionUrgency,
274}
275
276pub struct AdaptiveFeeManager {
278 client: BitcoinClient,
279}
280
281impl AdaptiveFeeManager {
282 pub fn new(client: BitcoinClient) -> Self {
284 Self { client }
285 }
286
287 pub fn get_market_fee_rate(&self, target_blocks: u32) -> Result<f64, BitcoinError> {
289 let estimate = self.client.estimate_smart_fee(target_blocks as u16)?;
290 Ok(estimate.unwrap_or(1.0))
291 }
292
293 pub fn calculate_urgency_fee_rate(
295 &self,
296 urgency: TransactionUrgency,
297 ) -> Result<f64, BitcoinError> {
298 let target_blocks = urgency.target_blocks();
299 let market_rate = self.get_market_fee_rate(target_blocks)?;
300 let multiplier = urgency.fee_multiplier();
301 Ok(market_rate * multiplier)
302 }
303
304 pub fn calculate_time_based_fee_rate(
306 &self,
307 strategy: &TimeBasedStrategy,
308 ) -> Result<f64, BitcoinError> {
309 let market_rate = self.get_market_fee_rate(6)?;
310 Ok(strategy.calculate_fee_rate(market_rate))
311 }
312
313 pub fn calculate_budget_fee_rate(
315 &self,
316 strategy: &BudgetStrategy,
317 ) -> Result<f64, BitcoinError> {
318 let market_rate = self.get_market_fee_rate(6)?;
319 strategy.calculate_fee_rate(market_rate)
320 }
321
322 pub fn calculate_multi_tx_allocation(
324 &self,
325 strategy: &MultiTxFeeStrategy,
326 ) -> Result<Vec<TxFeeAllocation>, BitcoinError> {
327 let market_rate = self.get_market_fee_rate(6)?;
328 strategy.calculate_fee_allocation(market_rate)
329 }
330
331 pub fn get_recommended_fee_rate(
333 &self,
334 urgency: TransactionUrgency,
335 max_fee_rate: Option<f64>,
336 ) -> Result<f64, BitcoinError> {
337 let mut fee_rate = self.calculate_urgency_fee_rate(urgency)?;
338
339 if let Some(max_rate) = max_fee_rate {
341 fee_rate = fee_rate.min(max_rate);
342 }
343
344 Ok(fee_rate.max(1.0))
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[test]
354 fn test_transaction_urgency_target_blocks() {
355 assert_eq!(TransactionUrgency::Low.target_blocks(), 144);
356 assert_eq!(TransactionUrgency::Medium.target_blocks(), 6);
357 assert_eq!(TransactionUrgency::High.target_blocks(), 2);
358 assert_eq!(TransactionUrgency::Critical.target_blocks(), 1);
359 }
360
361 #[test]
362 fn test_transaction_urgency_multiplier() {
363 assert_eq!(TransactionUrgency::Low.fee_multiplier(), 1.0);
364 assert_eq!(TransactionUrgency::Medium.fee_multiplier(), 1.5);
365 assert_eq!(TransactionUrgency::High.fee_multiplier(), 2.0);
366 assert_eq!(TransactionUrgency::Critical.fee_multiplier(), 3.0);
367 }
368
369 #[test]
370 fn test_time_based_strategy() {
371 let target_time = Utc::now() + chrono::Duration::hours(12);
372 let strategy = TimeBasedStrategy::new(target_time, 1.0, 100.0);
373
374 let fee_rate = strategy.calculate_fee_rate(10.0);
375 assert!(fee_rate >= 1.0);
376 assert!(fee_rate <= 100.0);
377 }
378
379 #[test]
380 fn test_budget_strategy() {
381 let strategy = BudgetStrategy::new(10_000, 200);
382
383 assert_eq!(strategy.max_fee_rate(), 50.0);
384 assert!(strategy.fits_budget(40.0));
385 assert!(!strategy.fits_budget(60.0));
386 }
387
388 #[test]
389 fn test_budget_strategy_calculate_fee_rate() {
390 let strategy = BudgetStrategy::new(5_000, 200);
391
392 let result = strategy.calculate_fee_rate(20.0);
394 assert!(result.is_ok());
395 assert_eq!(result.unwrap(), 20.0);
396
397 let result = strategy.calculate_fee_rate(30.0);
399 assert!(result.is_ok());
400 assert_eq!(result.unwrap(), 25.0); }
402
403 #[test]
404 fn test_multi_tx_strategy() {
405 let mut strategy = MultiTxFeeStrategy::new(50_000, 1.0);
406
407 strategy.add_transaction(PlannedTransaction {
408 id: "tx1".to_string(),
409 vbytes: 200,
410 urgency: TransactionUrgency::High,
411 max_fee: None,
412 });
413
414 strategy.add_transaction(PlannedTransaction {
415 id: "tx2".to_string(),
416 vbytes: 150,
417 urgency: TransactionUrgency::Low,
418 max_fee: None,
419 });
420
421 let total_cost = strategy.total_cost_at_rate(10.0);
422 assert!(total_cost > 0);
423 }
424
425 #[test]
426 fn test_multi_tx_fee_allocation() {
427 let mut strategy = MultiTxFeeStrategy::new(10_000, 1.0);
428
429 strategy.add_transaction(PlannedTransaction {
430 id: "tx1".to_string(),
431 vbytes: 200,
432 urgency: TransactionUrgency::Medium,
433 max_fee: Some(4_000),
434 });
435
436 strategy.add_transaction(PlannedTransaction {
437 id: "tx2".to_string(),
438 vbytes: 200,
439 urgency: TransactionUrgency::Low,
440 max_fee: None,
441 });
442
443 let result = strategy.calculate_fee_allocation(10.0);
444 assert!(result.is_ok());
445
446 let allocations = result.unwrap();
447 assert_eq!(allocations.len(), 2);
448
449 let total_allocated: u64 = allocations.iter().map(|a| a.allocated_fee).sum();
450 assert!(total_allocated <= 10_000);
451 }
452
453 #[test]
454 fn test_planned_transaction() {
455 let tx = PlannedTransaction {
456 id: "test_tx".to_string(),
457 vbytes: 250,
458 urgency: TransactionUrgency::High,
459 max_fee: Some(10_000),
460 };
461
462 assert_eq!(tx.id, "test_tx");
463 assert_eq!(tx.vbytes, 250);
464 assert_eq!(tx.urgency, TransactionUrgency::High);
465 assert_eq!(tx.max_fee, Some(10_000));
466 }
467}