Skip to content

Commit ab41679

Browse files
committed
Add fee_amount() and fee_rate() functions to PsbtUtils trait
PsbtUtils.fee_amount(), calculates the PSBT total transaction fee amount in Sats. PsbtUtils.fee_rate(), calculates the PSBT FeeRate, the value is only accurate AFTER the PSBT is finalized.
1 parent 7b12f35 commit ab41679

File tree

1 file changed

+113
-1
lines changed

1 file changed

+113
-1
lines changed

src/psbt/mod.rs

+113-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,37 @@ 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: u64 = inputs.iter().map(|i| i.value).sum();
58+
let output_amount: u64 = self.unsigned_tx.output.iter().map(|o| o.value).sum();
59+
input_amount
60+
.checked_sub(output_amount)
61+
.expect("input amount must be greater than output amount")
62+
})
63+
}
64+
65+
fn fee_rate(&self) -> Option<FeeRate> {
66+
let fee_amount = self.fee_amount();
67+
fee_amount.map(|fee| {
68+
let weight = self.clone().extract_tx().weight();
69+
FeeRate::from_wu(fee, weight)
70+
})
71+
}
4072
}
4173

4274
#[cfg(test)]
4375
mod test {
4476
use crate::bitcoin::TxIn;
4577
use crate::psbt::Psbt;
4678
use crate::wallet::AddressIndex;
79+
use crate::wallet::AddressIndex::New;
4780
use crate::wallet::{get_funded_wallet, test::get_test_wpkh};
48-
use crate::SignOptions;
81+
use crate::{psbt, FeeRate, SignOptions};
4982
use std::str::FromStr;
5083

5184
// from bip 174
@@ -118,4 +151,83 @@ mod test {
118151

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

0 commit comments

Comments
 (0)