use crate::client::BitcoinClient;
use crate::error::BitcoinError;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransactionUrgency {
Low,
Medium,
High,
Critical,
}
impl TransactionUrgency {
pub fn target_blocks(&self) -> u32 {
match self {
Self::Low => 144, Self::Medium => 6, Self::High => 2, Self::Critical => 1, }
}
pub fn fee_multiplier(&self) -> f64 {
match self {
Self::Low => 1.0,
Self::Medium => 1.5,
Self::High => 2.0,
Self::Critical => 3.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeBasedStrategy {
pub target_time: DateTime<Utc>,
pub min_fee_rate: f64,
pub max_fee_rate: f64,
pub allow_bumping: bool,
}
impl TimeBasedStrategy {
pub fn new(target_time: DateTime<Utc>, min_fee_rate: f64, max_fee_rate: f64) -> Self {
Self {
target_time,
min_fee_rate,
max_fee_rate,
allow_bumping: true,
}
}
pub fn calculate_fee_rate(&self, current_fee_rate: f64) -> f64 {
let now = Utc::now();
let time_remaining = self.target_time.signed_duration_since(now);
if time_remaining.num_seconds() <= 0 {
return self.max_fee_rate;
}
let hours_remaining = time_remaining.num_hours() as f64;
let multiplier = if hours_remaining < 1.0 {
3.0 } else if hours_remaining < 6.0 {
2.0 } else if hours_remaining < 24.0 {
1.5 } else {
1.0 };
(current_fee_rate * multiplier)
.max(self.min_fee_rate)
.min(self.max_fee_rate)
}
pub fn should_bump_fee(&self) -> bool {
if !self.allow_bumping {
return false;
}
let now = Utc::now();
let time_remaining = self.target_time.signed_duration_since(now);
time_remaining.num_hours() < 2
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BudgetStrategy {
pub max_fee_satoshis: u64,
pub tx_vbytes: usize,
pub min_fee_rate: f64,
}
impl BudgetStrategy {
pub fn new(max_fee_satoshis: u64, tx_vbytes: usize) -> Self {
Self {
max_fee_satoshis,
tx_vbytes,
min_fee_rate: 1.0,
}
}
pub fn max_fee_rate(&self) -> f64 {
self.max_fee_satoshis as f64 / self.tx_vbytes as f64
}
pub fn calculate_fee_rate(&self, market_fee_rate: f64) -> Result<f64, BitcoinError> {
let max_rate = self.max_fee_rate();
if max_rate < self.min_fee_rate {
return Err(BitcoinError::InsufficientFunds(
"Budget too low for minimum fee rate".to_string(),
));
}
Ok(market_fee_rate.min(max_rate).max(self.min_fee_rate))
}
pub fn fits_budget(&self, fee_rate: f64) -> bool {
let total_fee = (fee_rate * self.tx_vbytes as f64) as u64;
total_fee <= self.max_fee_satoshis
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MultiTxFeeStrategy {
pub total_budget: u64,
pub transactions: Vec<PlannedTransaction>,
pub min_fee_rate: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlannedTransaction {
pub id: String,
pub vbytes: usize,
pub urgency: TransactionUrgency,
pub max_fee: Option<u64>,
}
impl MultiTxFeeStrategy {
pub fn new(total_budget: u64, min_fee_rate: f64) -> Self {
Self {
total_budget,
transactions: Vec::new(),
min_fee_rate,
}
}
pub fn add_transaction(&mut self, tx: PlannedTransaction) {
self.transactions.push(tx);
}
pub fn calculate_fee_allocation(
&self,
market_fee_rate: f64,
) -> Result<Vec<TxFeeAllocation>, BitcoinError> {
if self.transactions.is_empty() {
return Ok(Vec::new());
}
let mut allocations = Vec::new();
let mut remaining_budget = self.total_budget;
for tx in &self.transactions {
let priority_multiplier = tx.urgency.fee_multiplier();
let base_fee = (tx.vbytes as f64 * market_fee_rate * priority_multiplier) as u64;
let allocated_fee = if let Some(max_fee) = tx.max_fee {
base_fee.min(max_fee)
} else {
base_fee
};
let allocated_fee = allocated_fee.min(remaining_budget);
remaining_budget = remaining_budget.saturating_sub(allocated_fee);
let fee_rate = allocated_fee as f64 / tx.vbytes as f64;
allocations.push(TxFeeAllocation {
tx_id: tx.id.clone(),
allocated_fee,
fee_rate,
vbytes: tx.vbytes,
urgency: tx.urgency,
});
}
for alloc in &allocations {
if alloc.fee_rate < self.min_fee_rate {
return Err(BitcoinError::InsufficientFunds(format!(
"Insufficient budget to meet minimum fee rate for tx: {}",
alloc.tx_id
)));
}
}
Ok(allocations)
}
pub fn total_cost_at_rate(&self, fee_rate: f64) -> u64 {
self.transactions
.iter()
.map(|tx| {
let multiplier = tx.urgency.fee_multiplier();
(tx.vbytes as f64 * fee_rate * multiplier) as u64
})
.sum()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TxFeeAllocation {
pub tx_id: String,
pub allocated_fee: u64,
pub fee_rate: f64,
pub vbytes: usize,
pub urgency: TransactionUrgency,
}
pub struct AdaptiveFeeManager {
client: BitcoinClient,
}
impl AdaptiveFeeManager {
pub fn new(client: BitcoinClient) -> Self {
Self { client }
}
pub fn get_market_fee_rate(&self, target_blocks: u32) -> Result<f64, BitcoinError> {
let estimate = self.client.estimate_smart_fee(target_blocks as u16)?;
Ok(estimate.unwrap_or(1.0))
}
pub fn calculate_urgency_fee_rate(
&self,
urgency: TransactionUrgency,
) -> Result<f64, BitcoinError> {
let target_blocks = urgency.target_blocks();
let market_rate = self.get_market_fee_rate(target_blocks)?;
let multiplier = urgency.fee_multiplier();
Ok(market_rate * multiplier)
}
pub fn calculate_time_based_fee_rate(
&self,
strategy: &TimeBasedStrategy,
) -> Result<f64, BitcoinError> {
let market_rate = self.get_market_fee_rate(6)?;
Ok(strategy.calculate_fee_rate(market_rate))
}
pub fn calculate_budget_fee_rate(
&self,
strategy: &BudgetStrategy,
) -> Result<f64, BitcoinError> {
let market_rate = self.get_market_fee_rate(6)?;
strategy.calculate_fee_rate(market_rate)
}
pub fn calculate_multi_tx_allocation(
&self,
strategy: &MultiTxFeeStrategy,
) -> Result<Vec<TxFeeAllocation>, BitcoinError> {
let market_rate = self.get_market_fee_rate(6)?;
strategy.calculate_fee_allocation(market_rate)
}
pub fn get_recommended_fee_rate(
&self,
urgency: TransactionUrgency,
max_fee_rate: Option<f64>,
) -> Result<f64, BitcoinError> {
let mut fee_rate = self.calculate_urgency_fee_rate(urgency)?;
if let Some(max_rate) = max_fee_rate {
fee_rate = fee_rate.min(max_rate);
}
Ok(fee_rate.max(1.0))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transaction_urgency_target_blocks() {
assert_eq!(TransactionUrgency::Low.target_blocks(), 144);
assert_eq!(TransactionUrgency::Medium.target_blocks(), 6);
assert_eq!(TransactionUrgency::High.target_blocks(), 2);
assert_eq!(TransactionUrgency::Critical.target_blocks(), 1);
}
#[test]
fn test_transaction_urgency_multiplier() {
assert_eq!(TransactionUrgency::Low.fee_multiplier(), 1.0);
assert_eq!(TransactionUrgency::Medium.fee_multiplier(), 1.5);
assert_eq!(TransactionUrgency::High.fee_multiplier(), 2.0);
assert_eq!(TransactionUrgency::Critical.fee_multiplier(), 3.0);
}
#[test]
fn test_time_based_strategy() {
let target_time = Utc::now() + chrono::Duration::hours(12);
let strategy = TimeBasedStrategy::new(target_time, 1.0, 100.0);
let fee_rate = strategy.calculate_fee_rate(10.0);
assert!(fee_rate >= 1.0);
assert!(fee_rate <= 100.0);
}
#[test]
fn test_budget_strategy() {
let strategy = BudgetStrategy::new(10_000, 200);
assert_eq!(strategy.max_fee_rate(), 50.0);
assert!(strategy.fits_budget(40.0));
assert!(!strategy.fits_budget(60.0));
}
#[test]
fn test_budget_strategy_calculate_fee_rate() {
let strategy = BudgetStrategy::new(5_000, 200);
let result = strategy.calculate_fee_rate(20.0);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 20.0);
let result = strategy.calculate_fee_rate(30.0);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 25.0); }
#[test]
fn test_multi_tx_strategy() {
let mut strategy = MultiTxFeeStrategy::new(50_000, 1.0);
strategy.add_transaction(PlannedTransaction {
id: "tx1".to_string(),
vbytes: 200,
urgency: TransactionUrgency::High,
max_fee: None,
});
strategy.add_transaction(PlannedTransaction {
id: "tx2".to_string(),
vbytes: 150,
urgency: TransactionUrgency::Low,
max_fee: None,
});
let total_cost = strategy.total_cost_at_rate(10.0);
assert!(total_cost > 0);
}
#[test]
fn test_multi_tx_fee_allocation() {
let mut strategy = MultiTxFeeStrategy::new(10_000, 1.0);
strategy.add_transaction(PlannedTransaction {
id: "tx1".to_string(),
vbytes: 200,
urgency: TransactionUrgency::Medium,
max_fee: Some(4_000),
});
strategy.add_transaction(PlannedTransaction {
id: "tx2".to_string(),
vbytes: 200,
urgency: TransactionUrgency::Low,
max_fee: None,
});
let result = strategy.calculate_fee_allocation(10.0);
assert!(result.is_ok());
let allocations = result.unwrap();
assert_eq!(allocations.len(), 2);
let total_allocated: u64 = allocations.iter().map(|a| a.allocated_fee).sum();
assert!(total_allocated <= 10_000);
}
#[test]
fn test_planned_transaction() {
let tx = PlannedTransaction {
id: "test_tx".to_string(),
vbytes: 250,
urgency: TransactionUrgency::High,
max_fee: Some(10_000),
};
assert_eq!(tx.id, "test_tx");
assert_eq!(tx.vbytes, 250);
assert_eq!(tx.urgency, TransactionUrgency::High);
assert_eq!(tx.max_fee, Some(10_000));
}
}