use crate::transaction::error::TransactionError;
use crate::transaction::transaction::Transaction;
pub trait FeeModel {
fn compute_fee(&self, tx: &Transaction) -> Result<u64, TransactionError>;
}
#[derive(Debug, Clone, Copy)]
pub struct SatoshisPerKilobyte {
pub value: u64,
}
impl SatoshisPerKilobyte {
pub fn new(value: u64) -> Self {
SatoshisPerKilobyte { value }
}
}
impl FeeModel for SatoshisPerKilobyte {
fn compute_fee(&self, tx: &Transaction) -> Result<u64, TransactionError> {
let mut size: u64 = 4;
size += varint_size(tx.inputs.len() as u64);
for input in &tx.inputs {
size += 40;
let script_length = if let Some(ref script) = input.unlocking_script {
let bin = script.to_binary();
bin.len() as u64
} else {
107
};
size += varint_size(script_length);
size += script_length;
}
size += varint_size(tx.outputs.len() as u64);
for output in &tx.outputs {
size += 8; let script_bytes = output.locking_script.to_binary();
size += varint_size(script_bytes.len() as u64);
size += script_bytes.len() as u64;
}
size += 4;
Ok((size * self.value).div_ceil(1000))
}
}
fn varint_size(val: u64) -> u64 {
if val < 0xfd {
1
} else if val <= 0xffff {
3
} else if val <= 0xffff_ffff {
5
} else {
9
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::script::locking_script::LockingScript;
use crate::script::templates::p2pkh::P2PKH;
use crate::script::templates::ScriptTemplateLock;
use crate::transaction::transaction_input::TransactionInput;
use crate::transaction::transaction_output::TransactionOutput;
#[test]
fn test_fee_model_empty_tx() {
let model = SatoshisPerKilobyte::new(1000);
let tx = Transaction::new();
let fee = model.compute_fee(&tx).unwrap();
assert_eq!(fee, 10);
}
#[test]
fn test_fee_model_one_input_one_output() {
let model = SatoshisPerKilobyte::new(1000);
let mut tx = Transaction::new();
tx.add_input(TransactionInput::default());
let p2pkh = P2PKH::from_public_key_hash([0xab; 20]);
let lock_script = p2pkh.lock().unwrap();
tx.add_output(TransactionOutput {
satoshis: Some(50000),
locking_script: lock_script,
change: false,
});
let fee = model.compute_fee(&tx).unwrap();
assert_eq!(fee, 192);
}
#[test]
fn test_fee_model_standard_rate() {
let model = SatoshisPerKilobyte::new(500);
let tx = Transaction::new();
let fee = model.compute_fee(&tx).unwrap();
assert_eq!(fee, 5);
}
#[test]
fn test_fee_model_ceiling_division() {
let model = SatoshisPerKilobyte::new(1);
let tx = Transaction::new();
let fee = model.compute_fee(&tx).unwrap();
assert_eq!(fee, 1);
}
#[test]
fn test_fee_model_with_signed_input() {
let model = SatoshisPerKilobyte::new(1000);
let mut tx = Transaction::new();
let key = crate::primitives::private_key::PrivateKey::from_hex("1").unwrap();
let p2pkh = P2PKH::from_private_key(key.clone());
let unlock = p2pkh.unlock(b"test preimage").unwrap();
let unlock_len = unlock.to_binary().len() as u64;
let mut input = TransactionInput::default();
input.unlocking_script = Some(unlock);
input.source_txid = Some("00".repeat(32));
tx.add_input(input);
let p2pkh_lock = P2PKH::from_public_key_hash([0xab; 20]);
tx.add_output(TransactionOutput {
satoshis: Some(50000),
locking_script: p2pkh_lock.lock().unwrap(),
change: false,
});
let fee = model.compute_fee(&tx).unwrap();
let expected_size =
4 + 1 + (40 + varint_size(unlock_len) + unlock_len) + 1 + (8 + 1 + 25) + 4;
let expected_fee = (expected_size * 1000 + 999) / 1000;
assert_eq!(fee, expected_fee);
}
}