Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions scripts/test-endpoints.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 54 additions & 14 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,27 +373,35 @@ async fn get_block_by_height(
}
}

fn get_fee_rate_blocking(rpc: &Client, blocks: u16) -> Result<f64, bitcoincore_rpc::Error> {
/// 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<Option<f64>, 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<AppState>) -> impl IntoResponse {
let rpc = state.rpc.clone();
match tokio::task::spawn_blocking(move || {
CONFIRMATION_TARGETS
let estimates: BTreeMap<String, f64> = CONFIRMATION_TARGETS
.iter()
.map(|&blocks| Ok((blocks.to_string(), get_fee_rate_blocking(&rpc, blocks)?)))
.collect::<Result<BTreeMap<_, _>, 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
{
Expand Down Expand Up @@ -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
);
}
}