use bitcoin::{Amount, ScriptBuf, Transaction, TxOut};
#[must_use]
pub fn satoshis_to_btc(satoshis: u64) -> f64 {
satoshis as f64 / 100_000_000.0
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
pub fn btc_to_satoshis(btc: f64) -> u64 {
(btc * 100_000_000.0) as u64
}
#[must_use]
pub fn satoshis_to_mbtc(satoshis: u64) -> f64 {
satoshis as f64 / 100_000.0
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
pub fn mbtc_to_satoshis(mbtc: f64) -> u64 {
(mbtc * 100_000.0) as u64
}
#[must_use]
pub fn estimate_transaction_vsize(tx: &Transaction) -> u64 {
let weight = tx.weight();
weight.to_wu().div_ceil(4)
}
#[must_use]
pub fn calculate_fee_rate(fee_satoshis: u64, vsize: u64) -> u64 {
if vsize == 0 {
return 0;
}
fee_satoshis / vsize
}
#[must_use]
pub fn calculate_fee_from_rate(fee_rate_sat_per_vb: u64, vsize: u64) -> u64 {
fee_rate_sat_per_vb * vsize
}
pub const P2PKH_INPUT_VSIZE: u64 = 148;
pub const P2WPKH_INPUT_VSIZE: u64 = 68;
pub const P2SH_P2WPKH_INPUT_VSIZE: u64 = 91;
pub const P2TR_INPUT_VSIZE: u64 = 58;
pub const P2PKH_OUTPUT_VSIZE: u64 = 34;
pub const P2WPKH_OUTPUT_VSIZE: u64 = 31;
pub const P2SH_OUTPUT_VSIZE: u64 = 32;
pub const P2TR_OUTPUT_VSIZE: u64 = 43;
pub const TX_OVERHEAD_VSIZE: u64 = 10;
#[must_use]
pub fn estimate_simple_transaction_vsize(
num_inputs: u64,
num_outputs: u64,
input_type_vsize: u64,
output_type_vsize: u64,
) -> u64 {
TX_OVERHEAD_VSIZE + (num_inputs * input_type_vsize) + (num_outputs * output_type_vsize)
}
#[must_use]
pub fn is_dust(amount_satoshis: u64, fee_rate_sat_per_vb: u64) -> bool {
let dust_threshold = (P2WPKH_INPUT_VSIZE + P2WPKH_OUTPUT_VSIZE) * 3 * fee_rate_sat_per_vb;
amount_satoshis < dust_threshold
}
pub const DUST_THRESHOLD: u64 = 546;
#[must_use]
pub fn calculate_effective_value(
amount_satoshis: u64,
fee_rate_sat_per_vb: u64,
input_vsize: u64,
) -> i64 {
let amount = amount_satoshis as i64;
let cost = (fee_rate_sat_per_vb * input_vsize) as i64;
amount - cost
}
#[must_use]
pub fn amount_to_satoshis(amount: &Amount) -> u64 {
amount.to_sat()
}
#[must_use]
pub fn is_op_return(output: &TxOut) -> bool {
output.script_pubkey.is_op_return()
}
#[must_use]
pub fn script_size(script: &ScriptBuf) -> usize {
script.len()
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn calculate_fee_percentage(fee_satoshis: u64, total_output_satoshis: u64) -> f64 {
if total_output_satoshis == 0 {
return 0.0;
}
(fee_satoshis as f64 / total_output_satoshis as f64) * 100.0
}
#[must_use]
pub fn round_for_privacy(amount_satoshis: u64, round_to: u64) -> u64 {
if round_to == 0 {
return amount_satoshis;
}
((amount_satoshis + round_to / 2) / round_to) * round_to
}
#[must_use]
pub fn weight_to_vsize(weight: u64) -> u64 {
weight.div_ceil(4)
}
#[must_use]
pub fn vsize_to_weight(vsize: u64) -> u64 {
vsize * 4
}
#[must_use]
pub fn calculate_change(
total_input: u64,
target_output: u64,
fee: u64,
min_change: u64,
) -> Option<u64> {
let total_deductions = target_output.saturating_add(fee);
if total_input < total_deductions {
return None; }
let change = total_input - total_deductions;
if change < min_change {
None } else {
Some(change)
}
}
#[must_use]
pub fn is_valid_amount(satoshis: u64) -> bool {
const MAX_SATOSHIS: u64 = 21_000_000 * 100_000_000; satoshis <= MAX_SATOSHIS
}
#[must_use]
pub fn calculate_batch_fee(transaction_vsizes: &[u64], fee_rate_sat_per_vb: u64) -> u64 {
transaction_vsizes.iter().sum::<u64>() * fee_rate_sat_per_vb
}
#[must_use]
pub fn calculate_batch_savings(
individual_vsizes: &[u64],
batched_vsize: u64,
fee_rate_sat_per_vb: u64,
) -> i64 {
let individual_fee = calculate_batch_fee(individual_vsizes, fee_rate_sat_per_vb);
let batched_fee = batched_vsize * fee_rate_sat_per_vb;
individual_fee as i64 - batched_fee as i64
}
#[must_use]
pub fn estimate_p2wpkh_witness_size() -> usize {
107
}
#[must_use]
pub fn estimate_p2tr_keypath_witness_size() -> usize {
65
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_satoshis_to_btc() {
assert_eq!(satoshis_to_btc(100_000_000), 1.0);
assert_eq!(satoshis_to_btc(50_000_000), 0.5);
assert_eq!(satoshis_to_btc(1), 0.00000001);
}
#[test]
fn test_btc_to_satoshis() {
assert_eq!(btc_to_satoshis(1.0), 100_000_000);
assert_eq!(btc_to_satoshis(0.5), 50_000_000);
assert_eq!(btc_to_satoshis(0.00000001), 1);
}
#[test]
fn test_satoshis_to_mbtc() {
assert_eq!(satoshis_to_mbtc(100_000), 1.0);
assert_eq!(satoshis_to_mbtc(50_000), 0.5);
}
#[test]
fn test_mbtc_to_satoshis() {
assert_eq!(mbtc_to_satoshis(1.0), 100_000);
assert_eq!(mbtc_to_satoshis(0.5), 50_000);
}
#[test]
fn test_calculate_fee_rate() {
assert_eq!(calculate_fee_rate(1000, 200), 5);
assert_eq!(calculate_fee_rate(500, 100), 5);
assert_eq!(calculate_fee_rate(0, 100), 0);
assert_eq!(calculate_fee_rate(100, 0), 0);
}
#[test]
fn test_calculate_fee_from_rate() {
assert_eq!(calculate_fee_from_rate(5, 200), 1000);
assert_eq!(calculate_fee_from_rate(10, 150), 1500);
}
#[test]
fn test_estimate_simple_transaction_vsize() {
let vsize =
estimate_simple_transaction_vsize(2, 2, P2WPKH_INPUT_VSIZE, P2WPKH_OUTPUT_VSIZE);
assert_eq!(vsize, 208); }
#[test]
fn test_is_dust() {
assert!(is_dust(200, 1));
assert!(!is_dust(300, 1));
assert!(!is_dust(1000, 1));
assert!(is_dust(800, 3));
assert!(!is_dust(900, 3));
assert!(!is_dust(DUST_THRESHOLD, 1));
}
#[test]
fn test_calculate_effective_value() {
let effective = calculate_effective_value(10_000, 5, P2WPKH_INPUT_VSIZE);
assert_eq!(effective, 9_660); }
#[test]
fn test_calculate_effective_value_negative() {
let effective = calculate_effective_value(100, 10, P2WPKH_INPUT_VSIZE);
assert_eq!(effective, -580); }
#[test]
fn test_calculate_fee_percentage() {
assert_eq!(calculate_fee_percentage(1000, 100_000), 1.0);
assert_eq!(calculate_fee_percentage(5000, 100_000), 5.0);
assert_eq!(calculate_fee_percentage(0, 100_000), 0.0);
assert_eq!(calculate_fee_percentage(1000, 0), 0.0);
}
#[test]
fn test_round_for_privacy() {
assert_eq!(round_for_privacy(12_345, 1000), 12_000);
assert_eq!(round_for_privacy(12_678, 1000), 13_000);
assert_eq!(round_for_privacy(12_500, 1000), 13_000);
assert_eq!(round_for_privacy(100, 10), 100);
assert_eq!(round_for_privacy(105, 10), 110);
}
#[test]
fn test_vsize_constants() {
const _: () = assert!(P2WPKH_INPUT_VSIZE < P2PKH_INPUT_VSIZE);
const _: () = assert!(P2TR_INPUT_VSIZE < P2WPKH_INPUT_VSIZE);
const _: () = assert!(P2WPKH_OUTPUT_VSIZE < P2PKH_OUTPUT_VSIZE);
assert_eq!(P2WPKH_INPUT_VSIZE, 68);
assert_eq!(P2TR_INPUT_VSIZE, 58);
}
#[test]
fn test_weight_to_vsize() {
assert_eq!(weight_to_vsize(400), 100); assert_eq!(weight_to_vsize(401), 101); assert_eq!(weight_to_vsize(402), 101); assert_eq!(weight_to_vsize(403), 101); assert_eq!(weight_to_vsize(404), 101); assert_eq!(weight_to_vsize(0), 0); }
#[test]
fn test_vsize_to_weight() {
assert_eq!(vsize_to_weight(100), 400);
assert_eq!(vsize_to_weight(0), 0);
assert_eq!(vsize_to_weight(1), 4);
}
#[test]
fn test_calculate_change() {
let change = calculate_change(100_000, 50_000, 1_000, 600);
assert_eq!(change, Some(49_000));
let no_change = calculate_change(51_000, 50_000, 1_000, 600);
assert_eq!(no_change, None);
let dust = calculate_change(51_500, 50_000, 1_000, 600);
assert_eq!(dust, None);
let insufficient = calculate_change(40_000, 50_000, 1_000, 600);
assert_eq!(insufficient, None);
let min_threshold = calculate_change(51_600, 50_000, 1_000, 600);
assert_eq!(min_threshold, Some(600)); }
#[test]
fn test_is_valid_amount() {
assert!(is_valid_amount(0));
assert!(is_valid_amount(100_000_000)); assert!(is_valid_amount(2_100_000_000_000_000));
assert!(!is_valid_amount(2_100_000_000_000_001));
assert!(!is_valid_amount(u64::MAX));
}
#[test]
fn test_calculate_batch_fee() {
let vsizes = vec![200, 150, 180];
let fee = calculate_batch_fee(&vsizes, 5);
assert_eq!(fee, 2_650);
let empty_fee = calculate_batch_fee(&[], 5);
assert_eq!(empty_fee, 0);
let single_fee = calculate_batch_fee(&[100], 10);
assert_eq!(single_fee, 1_000);
}
#[test]
fn test_calculate_batch_savings() {
let individual_vsizes = vec![200, 200, 200]; let batched_vsize = 450; let savings = calculate_batch_savings(&individual_vsizes, batched_vsize, 5);
assert_eq!(savings, 750);
let no_savings = calculate_batch_savings(&[100], 150, 5);
assert_eq!(no_savings, -250);
let equal = calculate_batch_savings(&[100, 100], 200, 5);
assert_eq!(equal, 0);
}
#[test]
fn test_estimate_witness_sizes() {
let p2wpkh_witness = estimate_p2wpkh_witness_size();
assert!(p2wpkh_witness > 0);
assert!(p2wpkh_witness < 150);
let p2tr_witness = estimate_p2tr_keypath_witness_size();
assert!(p2tr_witness > 0);
assert!(p2tr_witness < 100);
assert!(p2tr_witness < p2wpkh_witness);
}
}