Skip to content

Commit 7d07e48

Browse files
committed
Add fee_amount() and fee_rate() functions to PsbtUtils trait
1 parent 13cf72f commit 7d07e48

File tree

1 file changed

+115
-1
lines changed

1 file changed

+115
-1
lines changed

src/psbt/mod.rs

+115-1
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,22 @@
99
// You may not use this file except in accordance with one or both of these
1010
// licenses.
1111

12+
use crate::FeeRate;
1213
use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
1314
use bitcoin::TxOut;
1415

1516
pub trait PsbtUtils {
1617
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut>;
18+
19+
/// The total transaction fee amount, sum of input amounts minus sum of output amounts, in Sats.
20+
/// If the PSBT is missing a TxOut for an input returns None.
21+
fn fee_amount(&self) -> Option<u64>;
22+
23+
/// The transaction's fee rate. This value will only be accurate if calculated AFTER the
24+
/// `PartiallySignedTransaction` is finalized and all witness/signature data is added to the
25+
/// transaction.
26+
/// If the PSBT is missing a TxOut for an input returns None.
27+
fn fee_rate(&self) -> Option<FeeRate>;
1728
}
1829

1930
impl PsbtUtils for Psbt {
@@ -37,15 +48,39 @@ impl PsbtUtils for Psbt {
3748
None
3849
}
3950
}
51+
52+
fn fee_amount(&self) -> Option<u64> {
53+
let tx = &self.unsigned_tx;
54+
let utxos: Option<Vec<TxOut>> = (0..tx.input.len()).map(|i| self.get_utxo_for(i)).collect();
55+
56+
utxos.map(|inputs| {
57+
let input_amount = inputs.iter().fold(0, |acc, i| acc + i.value);
58+
59+
let output_amount = &self
60+
.unsigned_tx
61+
.output
62+
.iter()
63+
.fold(0, |acc, o| acc + o.value);
64+
65+
input_amount - output_amount
66+
})
67+
}
68+
69+
fn fee_rate(&self) -> Option<FeeRate> {
70+
let fee_amount = self.fee_amount();
71+
let weight = self.clone().extract_tx().weight();
72+
fee_amount.map(|fee| FeeRate::from_wu(fee, weight))
73+
}
4074
}
4175

4276
#[cfg(test)]
4377
mod test {
4478
use crate::bitcoin::TxIn;
4579
use crate::psbt::Psbt;
4680
use crate::wallet::AddressIndex;
81+
use crate::wallet::AddressIndex::New;
4782
use crate::wallet::{get_funded_wallet, test::get_test_wpkh};
48-
use crate::SignOptions;
83+
use crate::{psbt, FeeRate, SignOptions};
4984
use std::str::FromStr;
5085

5186
// from bip 174
@@ -118,4 +153,83 @@ mod test {
118153

119154
let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
120155
}
156+
157+
#[test]
158+
fn test_psbt_fee_rate_with_witness_utxo() {
159+
use psbt::PsbtUtils;
160+
161+
let expected_fee_rate = 1.2345;
162+
163+
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
164+
let addr = wallet.get_address(New).unwrap();
165+
let mut builder = wallet.build_tx();
166+
builder.drain_to(addr.script_pubkey()).drain_wallet();
167+
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
168+
let (mut psbt, _) = builder.finish().unwrap();
169+
let fee_amount = psbt.fee_amount();
170+
assert!(fee_amount.is_some());
171+
172+
let unfinalized_fee_rate = psbt.fee_rate().unwrap();
173+
174+
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
175+
assert!(finalized);
176+
177+
let finalized_fee_rate = psbt.fee_rate().unwrap();
178+
assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate);
179+
assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb());
180+
}
181+
182+
#[test]
183+
fn test_psbt_fee_rate_with_nonwitness_utxo() {
184+
use psbt::PsbtUtils;
185+
186+
let expected_fee_rate = 1.2345;
187+
188+
let (wallet, _, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
189+
let addr = wallet.get_address(New).unwrap();
190+
let mut builder = wallet.build_tx();
191+
builder.drain_to(addr.script_pubkey()).drain_wallet();
192+
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
193+
let (mut psbt, _) = builder.finish().unwrap();
194+
let fee_amount = psbt.fee_amount();
195+
assert!(fee_amount.is_some());
196+
let unfinalized_fee_rate = psbt.fee_rate().unwrap();
197+
198+
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
199+
assert!(finalized);
200+
201+
let finalized_fee_rate = psbt.fee_rate().unwrap();
202+
assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate);
203+
assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb());
204+
}
205+
206+
#[test]
207+
fn test_psbt_fee_rate_with_missing_txout() {
208+
use psbt::PsbtUtils;
209+
210+
let expected_fee_rate = 1.2345;
211+
212+
let (wpkh_wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
213+
let addr = wpkh_wallet.get_address(New).unwrap();
214+
let mut builder = wpkh_wallet.build_tx();
215+
builder.drain_to(addr.script_pubkey()).drain_wallet();
216+
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
217+
let (mut wpkh_psbt, _) = builder.finish().unwrap();
218+
219+
wpkh_psbt.inputs[0].witness_utxo = None;
220+
wpkh_psbt.inputs[0].non_witness_utxo = None;
221+
assert!(wpkh_psbt.fee_amount().is_none());
222+
assert!(wpkh_psbt.fee_rate().is_none());
223+
224+
let (pkh_wallet, _, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
225+
let addr = pkh_wallet.get_address(New).unwrap();
226+
let mut builder = pkh_wallet.build_tx();
227+
builder.drain_to(addr.script_pubkey()).drain_wallet();
228+
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
229+
let (mut pkh_psbt, _) = builder.finish().unwrap();
230+
231+
pkh_psbt.inputs[0].non_witness_utxo = None;
232+
assert!(pkh_psbt.fee_amount().is_none());
233+
assert!(pkh_psbt.fee_rate().is_none());
234+
}
121235
}

0 commit comments

Comments
 (0)