bsv/transaction/
fee_model.rs1use crate::transaction::error::TransactionError;
7use crate::transaction::transaction::Transaction;
8
9pub trait FeeModel {
14 fn compute_fee(&self, tx: &Transaction) -> Result<u64, TransactionError>;
16}
17
18#[derive(Debug, Clone, Copy)]
23pub struct SatoshisPerKilobyte {
24 pub value: u64,
26}
27
28impl SatoshisPerKilobyte {
29 pub fn new(value: u64) -> Self {
31 SatoshisPerKilobyte { value }
32 }
33}
34
35impl FeeModel for SatoshisPerKilobyte {
36 fn compute_fee(&self, tx: &Transaction) -> Result<u64, TransactionError> {
48 let mut size: u64 = 4; size += varint_size(tx.inputs.len() as u64);
52
53 for input in &tx.inputs {
54 size += 40; let script_length = if let Some(ref script) = input.unlocking_script {
57 let bin = script.to_binary();
58 bin.len() as u64
59 } else {
60 107
62 };
63
64 size += varint_size(script_length);
65 size += script_length;
66 }
67
68 size += varint_size(tx.outputs.len() as u64);
70
71 for output in &tx.outputs {
72 size += 8; let script_bytes = output.locking_script.to_binary();
74 size += varint_size(script_bytes.len() as u64);
75 size += script_bytes.len() as u64;
76 }
77
78 size += 4; Ok((size * self.value).div_ceil(1000))
82 }
83}
84
85fn varint_size(val: u64) -> u64 {
87 if val < 0xfd {
88 1
89 } else if val <= 0xffff {
90 3
91 } else if val <= 0xffff_ffff {
92 5
93 } else {
94 9
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::script::locking_script::LockingScript;
102 use crate::script::templates::p2pkh::P2PKH;
103 use crate::script::templates::ScriptTemplateLock;
104 use crate::transaction::transaction_input::TransactionInput;
105 use crate::transaction::transaction_output::TransactionOutput;
106
107 #[test]
108 fn test_fee_model_empty_tx() {
109 let model = SatoshisPerKilobyte::new(1000);
110 let tx = Transaction::new();
111 let fee = model.compute_fee(&tx).unwrap();
112 assert_eq!(fee, 10);
115 }
116
117 #[test]
118 fn test_fee_model_one_input_one_output() {
119 let model = SatoshisPerKilobyte::new(1000);
120 let mut tx = Transaction::new();
121
122 tx.add_input(TransactionInput::default());
124
125 let p2pkh = P2PKH::from_public_key_hash([0xab; 20]);
127 let lock_script = p2pkh.lock().unwrap();
128 tx.add_output(TransactionOutput {
129 satoshis: Some(50000),
130 locking_script: lock_script,
131 change: false,
132 });
133
134 let fee = model.compute_fee(&tx).unwrap();
135 assert_eq!(fee, 192);
139 }
140
141 #[test]
142 fn test_fee_model_standard_rate() {
143 let model = SatoshisPerKilobyte::new(500);
144 let tx = Transaction::new();
145 let fee = model.compute_fee(&tx).unwrap();
146 assert_eq!(fee, 5);
148 }
149
150 #[test]
151 fn test_fee_model_ceiling_division() {
152 let model = SatoshisPerKilobyte::new(1);
154 let tx = Transaction::new();
155 let fee = model.compute_fee(&tx).unwrap();
156 assert_eq!(fee, 1);
158 }
159
160 #[test]
161 fn test_fee_model_with_signed_input() {
162 let model = SatoshisPerKilobyte::new(1000);
163 let mut tx = Transaction::new();
164
165 let key = crate::primitives::private_key::PrivateKey::from_hex("1").unwrap();
167 let p2pkh = P2PKH::from_private_key(key.clone());
168 let unlock = p2pkh.unlock(b"test preimage").unwrap();
169 let unlock_len = unlock.to_binary().len() as u64;
170
171 let mut input = TransactionInput::default();
172 input.unlocking_script = Some(unlock);
173 input.source_txid = Some("00".repeat(32));
174 tx.add_input(input);
175
176 let p2pkh_lock = P2PKH::from_public_key_hash([0xab; 20]);
177 tx.add_output(TransactionOutput {
178 satoshis: Some(50000),
179 locking_script: p2pkh_lock.lock().unwrap(),
180 change: false,
181 });
182
183 let fee = model.compute_fee(&tx).unwrap();
184
185 let expected_size =
187 4 + 1 + (40 + varint_size(unlock_len) + unlock_len) + 1 + (8 + 1 + 25) + 4;
188 let expected_fee = (expected_size * 1000 + 999) / 1000;
189 assert_eq!(fee, expected_fee);
190 }
191}