From 7ae33411d043ab1ef62b9798322a97784c19afe4 Mon Sep 17 00:00:00 2001 From: Allan Douglas Date: Wed, 14 Jan 2026 18:46:54 +0000 Subject: [PATCH] fix: convert fee estimates from BTC/kB to sat/vB Bitcoin Core's estimatesmartfee returns BTC/kB, but esplora API expects sat/vB. Fixed by using to_sat() and dividing by 1000. Also fixed the default fallback from 0.0001 BTC/kB to 1.0 sat/vB. Added: - Unit test for fee rate conversion formula - Bash tests to verify fee values are in sat/vB range - Bash test to compare fee estimates with blockstream --- scripts/test-endpoints.sh | 37 +++++++++++++++++++++ src/main.rs | 68 +++++++++++++++++++++++++++++++-------- 2 files changed, 91 insertions(+), 14 deletions(-) diff --git a/scripts/test-endpoints.sh b/scripts/test-endpoints.sh index ef6890e..a22c042 100755 --- a/scripts/test-endpoints.sh +++ b/scripts/test-endpoints.sh @@ -226,6 +226,43 @@ else print_fail "JSON with keys 1, 6, 144" "$minipool_fees" fi +# Test /api/fee-estimates values are in sat/vB (not BTC/kB) +# Valid sat/vB values should be between 0.5 and 10000 (not tiny like 0.00001) +print_test "/api/fee-estimates (values in sat/vB range)" +fee_1_block=$(echo "$minipool_fees" | jq -r '.["1"] // 0') +if [ -n "$fee_1_block" ]; then + # Check if fee is in reasonable sat/vB range (0.5 to 10000) + # BTC/kB values would be like 0.00001 which would fail this check + is_valid=$(echo "$fee_1_block" | awk '{if ($1 >= 0.5 && $1 <= 10000) print "yes"; else print "no"}') + if [ "$is_valid" = "yes" ]; then + print_pass + echo " (1-block fee: $fee_1_block sat/vB)" + else + print_fail "0.5 to 10000 sat/vB" "$fee_1_block (likely BTC/kB if tiny)" + fi +else + print_skip "could not parse fee estimate" +fi + +# Compare with blockstream to verify values are close +print_test "/api/fee-estimates (compare with blockstream)" +blockstream_fees=$(curl -s "$BLOCKSTREAM_URL/api/fee-estimates" 2>/dev/null) +minipool_fee_1=$(echo "$minipool_fees" | jq -r '.["1"] // 0') +blockstream_fee_1=$(echo "$blockstream_fees" | jq -r '.["1"] // 0') +if [ -n "$minipool_fee_1" ] && [ -n "$blockstream_fee_1" ] && [ "$blockstream_fee_1" != "0" ]; then + # Allow 50% variance since fee estimates can differ between nodes + ratio=$(echo "$minipool_fee_1 $blockstream_fee_1" | awk '{if ($2 > 0) print $1/$2; else print 0}') + is_close=$(echo "$ratio" | awk '{if ($1 >= 0.5 && $1 <= 2.0) print "yes"; else print "no"}') + if [ "$is_close" = "yes" ]; then + print_pass + echo " (minipool: $minipool_fee_1, blockstream: $blockstream_fee_1, ratio: $ratio)" + else + print_fail "within 50% of $blockstream_fee_1" "$minipool_fee_1 (ratio: $ratio)" + fi +else + print_skip "could not compare fee estimates" +fi + print_header "Testing Error Handling" # Test invalid block hash diff --git a/src/main.rs b/src/main.rs index 9cb2954..9e821e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -373,27 +373,35 @@ async fn get_block_by_height( } } -fn get_fee_rate_blocking(rpc: &Client, blocks: u16) -> Result { +/// Convert fee rate from Bitcoin Core format (BTC/kB) to esplora format (sat/vB) +fn btc_per_kb_to_sat_per_vb(fee_rate: bitcoincore_rpc::bitcoin::Amount) -> f64 { + // BTC/kB to sat/vB: multiply by 100_000_000 (sats/BTC), divide by 1000 (bytes/kB) + // Simplified: sat/kB / 1000 = sat/vB + fee_rate.to_sat() as f64 / 1000.0 +} + +fn get_fee_rate_blocking(rpc: &Client, blocks: u16) -> Result, bitcoincore_rpc::Error> { let estimate = rpc.estimate_smart_fee(blocks, None)?; - Ok(estimate - .fee_rate - .map(|fee_rate: bitcoincore_rpc::bitcoin::Amount| fee_rate.to_btc()) - .unwrap_or_else(|| { - warn!( - "No fee rate estimate available for {} blocks, using default", - blocks - ); - 0.0001 - })) + Ok(estimate.fee_rate.map(btc_per_kb_to_sat_per_vb)) } async fn get_fee_estimates(State(state): State) -> impl IntoResponse { let rpc = state.rpc.clone(); match tokio::task::spawn_blocking(move || { - CONFIRMATION_TARGETS + let estimates: BTreeMap = CONFIRMATION_TARGETS .iter() - .map(|&blocks| Ok((blocks.to_string(), get_fee_rate_blocking(&rpc, blocks)?))) - .collect::, bitcoincore_rpc::Error>>() + .filter_map(|&blocks| { + match get_fee_rate_blocking(&rpc, blocks) { + Ok(Some(fee)) => Some((blocks.to_string(), fee)), + Ok(None) => None, // No estimate available, skip this target + Err(e) => { + warn!("Error getting fee estimate for {} blocks: {}", blocks, e); + None + } + } + }) + .collect(); + Ok::<_, bitcoincore_rpc::Error>(estimates) }) .await { @@ -1053,4 +1061,36 @@ mod tests { let proof = compute_merkle_proof(&txids, 5); assert!(proof.is_empty()); } + + #[test] + fn test_btc_per_kb_to_sat_per_vb() { + use bitcoincore_rpc::bitcoin::Amount; + + // 0.00001 BTC/kB = 1000 sat/kB = 1 sat/vB + assert!( + (btc_per_kb_to_sat_per_vb(Amount::from_btc(0.00001).unwrap()) - 1.0).abs() < 0.0001 + ); + + // 0.0001 BTC/kB = 10000 sat/kB = 10 sat/vB + assert!( + (btc_per_kb_to_sat_per_vb(Amount::from_btc(0.0001).unwrap()) - 10.0).abs() < 0.0001 + ); + + // 0.00001198 BTC/kB = 1198 sat/kB = 1.198 sat/vB (real example from blockstream) + assert!( + (btc_per_kb_to_sat_per_vb(Amount::from_btc(0.00001198).unwrap()) - 1.198).abs() + < 0.0001 + ); + + // 0.0005 BTC/kB = 50000 sat/kB = 50 sat/vB (high fee scenario) + assert!( + (btc_per_kb_to_sat_per_vb(Amount::from_btc(0.0005).unwrap()) - 50.0).abs() < 0.0001 + ); + + // 0 BTC/kB = 0 sat/vB + assert_eq!( + btc_per_kb_to_sat_per_vb(Amount::from_btc(0.0).unwrap()), + 0.0 + ); + } }