use bitcoin::hashes::Hash;
use bitcoin::{FeeRate, Txid};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::client::BitcoinClient;
use crate::error::{BitcoinError, Result};
use crate::rbf::RbfConfig;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchRbfOperation {
pub original_txids: Vec<Txid>,
pub new_fee_rate: FeeRate,
pub consolidate: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchRbfResult {
pub new_txids: Vec<Txid>,
pub fee_saved: u64,
pub original_count: usize,
pub new_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsolidatedPayment {
pub original_txids: Vec<Txid>,
pub address: String,
pub total_amount: u64,
}
pub struct AdvancedRbfManager {
#[allow(dead_code)]
rbf_config: RbfConfig,
client: BitcoinClient,
}
impl AdvancedRbfManager {
pub fn new(rbf_config: RbfConfig, client: BitcoinClient) -> Self {
Self { rbf_config, client }
}
pub fn batch_rbf(&self, operations: Vec<BatchRbfOperation>) -> Result<Vec<BatchRbfResult>> {
let mut results = Vec::new();
for op in operations {
let result = self.execute_batch_rbf(op)?;
results.push(result);
}
Ok(results)
}
fn execute_batch_rbf(&self, operation: BatchRbfOperation) -> Result<BatchRbfResult> {
if operation.original_txids.is_empty() {
return Err(BitcoinError::Validation(
"No transactions to replace".to_string(),
));
}
let payments = self.analyze_original_transactions(&operation.original_txids)?;
let original_fee_total = self.estimate_individual_fees(&operation.original_txids)?;
let batch_fee = self.estimate_batch_fee(&payments, operation.new_fee_rate)?;
let fee_saved = original_fee_total.saturating_sub(batch_fee);
let new_txids = if operation.consolidate {
self.create_consolidated_transactions(&payments, operation.new_fee_rate)?
} else {
self.create_batch_transaction(&payments, operation.new_fee_rate)?
};
tracing::info!(
original_count = operation.original_txids.len(),
new_count = new_txids.len(),
fee_saved = fee_saved,
"Completed batch RBF operation"
);
Ok(BatchRbfResult {
new_txids,
fee_saved,
original_count: operation.original_txids.len(),
new_count: 1, })
}
fn analyze_original_transactions(&self, txids: &[Txid]) -> Result<Vec<(String, u64)>> {
let mut payments = Vec::new();
for txid in txids {
let tx_result = self.client.get_raw_transaction(txid)?;
let tx = tx_result.transaction()?;
for output in &tx.output {
if output.value.to_sat() > 0 {
let address = format!("address_{}", payments.len());
payments.push((address, output.value.to_sat()));
}
}
}
Ok(payments)
}
fn estimate_individual_fees(&self, txids: &[Txid]) -> Result<u64> {
let mut total_fee = 0u64;
for txid in txids {
let _tx_info = self.client.get_transaction(txid)?;
total_fee += 1000; }
Ok(total_fee)
}
fn estimate_batch_fee(&self, payments: &[(String, u64)], fee_rate: FeeRate) -> Result<u64> {
let num_inputs = 1; let num_outputs = payments.len();
let estimated_vsize = (num_inputs * 180) + (num_outputs * 34) + 10;
let fee = fee_rate.to_sat_per_vb_ceil() * estimated_vsize as u64;
Ok(fee)
}
fn create_consolidated_transactions(
&self,
payments: &[(String, u64)],
fee_rate: FeeRate,
) -> Result<Vec<Txid>> {
let mut consolidated: HashMap<String, u64> = HashMap::new();
for (address, amount) in payments {
*consolidated.entry(address.clone()).or_insert(0) += amount;
}
tracing::info!(
original_payments = payments.len(),
consolidated_payments = consolidated.len(),
"Consolidated payments"
);
self.create_batch_transaction(&consolidated.into_iter().collect::<Vec<_>>(), fee_rate)
}
fn create_batch_transaction(
&self,
payments: &[(String, u64)],
_fee_rate: FeeRate,
) -> Result<Vec<Txid>> {
tracing::info!(num_payments = payments.len(), "Created batch transaction");
Ok(vec![Txid::all_zeros(); 1])
}
pub fn calculate_consolidation_savings(
&self,
original_txids: &[Txid],
fee_rate: FeeRate,
) -> Result<ConsolidationSavings> {
let payments = self.analyze_original_transactions(original_txids)?;
let individual_fee = self.estimate_individual_fees(original_txids)?;
let batch_fee = self.estimate_batch_fee(&payments, fee_rate)?;
let mut unique_addresses = HashMap::new();
for (addr, amt) in &payments {
*unique_addresses.entry(addr.clone()).or_insert(0) += amt;
}
Ok(ConsolidationSavings {
original_tx_count: original_txids.len(),
consolidated_tx_count: 1,
original_payment_count: payments.len(),
consolidated_payment_count: unique_addresses.len(),
individual_fee,
batch_fee,
fee_saved: individual_fee.saturating_sub(batch_fee),
savings_percentage: if individual_fee > 0 {
((individual_fee - batch_fee) as f64 / individual_fee as f64) * 100.0
} else {
0.0
},
})
}
pub fn get_rbf_recommendations(
&self,
pending_txids: &[Txid],
) -> Result<Vec<RbfRecommendation>> {
let mut recommendations = Vec::new();
for txid in pending_txids {
let tx_info = self.client.get_transaction(txid)?;
let confirmations = tx_info.info.confirmations;
if confirmations == 0 {
recommendations.push(RbfRecommendation {
txid: *txid, current_fee_rate: FeeRate::from_sat_per_vb_unchecked(1), recommended_fee_rate: FeeRate::from_sat_per_vb_unchecked(10), reason: RecommendationReason::Stuck,
urgency: RecommendationUrgency::High,
});
}
}
Ok(recommendations)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsolidationSavings {
pub original_tx_count: usize,
pub consolidated_tx_count: usize,
pub original_payment_count: usize,
pub consolidated_payment_count: usize,
pub individual_fee: u64,
pub batch_fee: u64,
pub fee_saved: u64,
pub savings_percentage: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RbfRecommendation {
pub txid: Txid,
pub current_fee_rate: FeeRate,
pub recommended_fee_rate: FeeRate,
pub reason: RecommendationReason,
pub urgency: RecommendationUrgency,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RecommendationReason {
Stuck,
Congestion,
Urgent,
Consolidation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RecommendationUrgency {
Low,
Medium,
High,
Critical,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_batch_rbf_operation_creation() {
let op = BatchRbfOperation {
original_txids: vec![Txid::all_zeros(); 1],
new_fee_rate: FeeRate::from_sat_per_vb_unchecked(10),
consolidate: true,
};
assert_eq!(op.original_txids.len(), 1);
assert!(op.consolidate);
}
#[test]
fn test_consolidation_savings() {
let savings = ConsolidationSavings {
original_tx_count: 5,
consolidated_tx_count: 1,
original_payment_count: 10,
consolidated_payment_count: 5,
individual_fee: 5000,
batch_fee: 2000,
fee_saved: 3000,
savings_percentage: 60.0,
};
assert_eq!(savings.original_tx_count, 5);
assert_eq!(savings.consolidated_tx_count, 1);
assert_eq!(savings.fee_saved, 3000);
assert_eq!(savings.savings_percentage, 60.0);
}
#[test]
fn test_rbf_recommendation() {
let txid = Txid::all_zeros();
let rec = RbfRecommendation {
txid,
current_fee_rate: FeeRate::from_sat_per_vb_unchecked(1),
recommended_fee_rate: FeeRate::from_sat_per_vb_unchecked(10),
reason: RecommendationReason::Stuck,
urgency: RecommendationUrgency::High,
};
assert_eq!(rec.reason, RecommendationReason::Stuck);
assert_eq!(rec.urgency, RecommendationUrgency::High);
}
#[test]
fn test_recommendation_urgency_levels() {
assert_ne!(RecommendationUrgency::Low, RecommendationUrgency::High);
assert_eq!(
RecommendationUrgency::Critical,
RecommendationUrgency::Critical
);
}
}