use crate::client::BitcoinClient;
use crate::error::BitcoinError;
use bitcoin::Txid;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BumpStrategy {
RBF,
CPFP,
Auto,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeeBumpingPolicy {
pub max_fee_rate: f64,
pub min_rbf_increase: f64,
pub max_attempts: usize,
pub bump_interval_secs: u64,
pub target_confirmations: u32,
}
impl Default for FeeBumpingPolicy {
fn default() -> Self {
Self {
max_fee_rate: 100.0, min_rbf_increase: 1.0, max_attempts: 5,
bump_interval_secs: 600, target_confirmations: 6,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeeBumpResult {
pub new_txid: Txid,
pub old_txid: Txid,
pub strategy: BumpStrategy,
pub new_fee_rate: f64,
pub old_fee_rate: f64,
pub additional_fee: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrackedTransaction {
pub txid: Txid,
pub original_fee_rate: f64,
pub current_fee_rate: f64,
pub bump_attempts: usize,
pub last_bump_time: Option<chrono::DateTime<chrono::Utc>>,
pub target_time: Option<chrono::DateTime<chrono::Utc>>,
pub priority: BumpPriority,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BumpPriority {
Low,
Normal,
High,
Critical,
}
impl BumpPriority {
pub fn multiplier(&self) -> f64 {
match self {
Self::Low => 1.0,
Self::Normal => 1.5,
Self::High => 2.0,
Self::Critical => 3.0,
}
}
}
pub struct AutoFeeBumper {
client: BitcoinClient,
policy: FeeBumpingPolicy,
tracked: HashMap<Txid, TrackedTransaction>,
}
impl AutoFeeBumper {
pub fn new(client: BitcoinClient, policy: FeeBumpingPolicy) -> Self {
Self {
client,
policy,
tracked: HashMap::new(),
}
}
pub fn track_transaction(
&mut self,
txid: Txid,
fee_rate: f64,
priority: BumpPriority,
target_time: Option<chrono::DateTime<chrono::Utc>>,
) {
let tracked = TrackedTransaction {
txid,
original_fee_rate: fee_rate,
current_fee_rate: fee_rate,
bump_attempts: 0,
last_bump_time: None,
target_time,
priority,
};
self.tracked.insert(txid, tracked);
}
pub fn untrack_transaction(&mut self, txid: &Txid) -> bool {
self.tracked.remove(txid).is_some()
}
pub fn check_and_bump(&mut self) -> Result<Vec<FeeBumpResult>, BitcoinError> {
let mut results = Vec::new();
let now = chrono::Utc::now();
let current_mempool_fee_rate = self.get_mempool_fee_rate()?;
let txids: Vec<Txid> = self.tracked.keys().copied().collect();
for txid in txids {
if let Some(tracked) = self.tracked.get(&txid) {
if self.is_confirmed(&txid)? {
self.tracked.remove(&txid);
continue;
}
if self.should_bump(tracked, current_mempool_fee_rate, now)? {
match self.bump_transaction(&txid, tracked.priority) {
Ok(result) => {
results.push(result.clone());
if let Some(tracked) = self.tracked.get_mut(&txid) {
tracked.current_fee_rate = result.new_fee_rate;
tracked.bump_attempts += 1;
tracked.last_bump_time = Some(now);
}
}
Err(e) => {
tracing::warn!("Failed to bump transaction {}: {}", txid, e);
}
}
}
}
}
Ok(results)
}
fn should_bump(
&self,
tracked: &TrackedTransaction,
mempool_fee_rate: f64,
now: chrono::DateTime<chrono::Utc>,
) -> Result<bool, BitcoinError> {
if tracked.bump_attempts >= self.policy.max_attempts {
return Ok(false);
}
if let Some(last_bump) = tracked.last_bump_time {
let elapsed = now.signed_duration_since(last_bump);
if elapsed.num_seconds() < self.policy.bump_interval_secs as i64 {
return Ok(false);
}
}
if tracked.current_fee_rate < mempool_fee_rate {
return Ok(true);
}
if let Some(target_time) = tracked.target_time {
let time_remaining = target_time.signed_duration_since(now);
if time_remaining.num_hours() < 1 {
return Ok(true);
}
}
Ok(false)
}
pub fn bump_transaction(
&self,
txid: &Txid,
priority: BumpPriority,
) -> Result<FeeBumpResult, BitcoinError> {
let _tx_info = self.client.get_transaction(txid)?;
let old_fee_rate = self.calculate_current_fee_rate(txid)?;
let mempool_rate = self.get_mempool_fee_rate()?;
let priority_multiplier = priority.multiplier();
let new_fee_rate = (mempool_rate * priority_multiplier)
.max(old_fee_rate + self.policy.min_rbf_increase)
.min(self.policy.max_fee_rate);
Ok(FeeBumpResult {
new_txid: *txid,
old_txid: *txid,
strategy: BumpStrategy::RBF,
new_fee_rate,
old_fee_rate,
additional_fee: ((new_fee_rate - old_fee_rate) * 200.0) as u64, })
}
fn is_confirmed(&self, txid: &Txid) -> Result<bool, BitcoinError> {
match self.client.get_transaction(txid) {
Ok(tx_info) => Ok(tx_info.info.confirmations > 0),
Err(_) => Ok(false),
}
}
fn get_mempool_fee_rate(&self) -> Result<f64, BitcoinError> {
self.client
.estimate_smart_fee(self.policy.target_confirmations as u16)
.map(|opt| opt.unwrap_or(1.0))
}
fn calculate_current_fee_rate(&self, _txid: &Txid) -> Result<f64, BitcoinError> {
Ok(1.0)
}
pub fn tracked_transactions(&self) -> Vec<&TrackedTransaction> {
self.tracked.values().collect()
}
pub fn tracked_count(&self) -> usize {
self.tracked.len()
}
}
#[allow(dead_code)]
pub struct CpfpBuilder {
client: BitcoinClient,
}
impl CpfpBuilder {
pub fn new(client: BitcoinClient) -> Self {
Self { client }
}
pub fn create_cpfp(
&self,
parent_txid: Txid,
target_fee_rate: f64,
) -> Result<String, BitcoinError> {
Ok(format!(
"CPFP transaction for parent {} with fee rate {}",
parent_txid, target_fee_rate
))
}
}
pub struct FeeBudgetManager {
pub total_budget: u64,
pub spent: u64,
pub reserved: u64,
}
impl FeeBudgetManager {
pub fn new(total_budget: u64) -> Self {
Self {
total_budget,
spent: 0,
reserved: 0,
}
}
pub fn can_spend(&self, amount: u64) -> bool {
self.spent + self.reserved + amount <= self.total_budget
}
pub fn reserve(&mut self, amount: u64) -> Result<(), BitcoinError> {
if !self.can_spend(amount) {
return Err(BitcoinError::InsufficientFunds(
"Fee budget exceeded".to_string(),
));
}
self.reserved += amount;
Ok(())
}
pub fn confirm_spend(&mut self, amount: u64) {
self.reserved = self.reserved.saturating_sub(amount);
self.spent += amount;
}
pub fn cancel_reservation(&mut self, amount: u64) {
self.reserved = self.reserved.saturating_sub(amount);
}
pub fn remaining(&self) -> u64 {
self.total_budget.saturating_sub(self.spent + self.reserved)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fee_bumping_policy_default() {
let policy = FeeBumpingPolicy::default();
assert_eq!(policy.max_fee_rate, 100.0);
assert_eq!(policy.min_rbf_increase, 1.0);
assert_eq!(policy.max_attempts, 5);
}
#[test]
fn test_bump_priority_multiplier() {
assert_eq!(BumpPriority::Low.multiplier(), 1.0);
assert_eq!(BumpPriority::Normal.multiplier(), 1.5);
assert_eq!(BumpPriority::High.multiplier(), 2.0);
assert_eq!(BumpPriority::Critical.multiplier(), 3.0);
}
#[test]
fn test_fee_budget_manager() {
let mut manager = FeeBudgetManager::new(10_000);
assert!(manager.can_spend(5_000));
assert_eq!(manager.remaining(), 10_000);
manager.reserve(3_000).unwrap();
assert_eq!(manager.remaining(), 7_000);
manager.confirm_spend(3_000);
assert_eq!(manager.spent, 3_000);
assert_eq!(manager.reserved, 0);
assert_eq!(manager.remaining(), 7_000);
}
#[test]
fn test_fee_budget_exceeded() {
let mut manager = FeeBudgetManager::new(1_000);
let result = manager.reserve(2_000);
assert!(result.is_err());
}
#[test]
fn test_bump_strategy() {
let strategy = BumpStrategy::RBF;
assert_eq!(strategy, BumpStrategy::RBF);
assert_ne!(strategy, BumpStrategy::CPFP);
}
}