9
9
// You may not use this file except in accordance with one or both of these
10
10
// licenses.
11
11
12
+ use crate :: FeeRate ;
12
13
use bitcoin:: util:: psbt:: PartiallySignedTransaction as Psbt ;
13
14
use bitcoin:: TxOut ;
14
15
15
16
pub trait PsbtUtils {
16
17
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 > ;
17
28
}
18
29
19
30
impl PsbtUtils for Psbt {
@@ -37,15 +48,37 @@ impl PsbtUtils for Psbt {
37
48
None
38
49
}
39
50
}
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
+ }
40
72
}
41
73
42
74
#[ cfg( test) ]
43
75
mod test {
44
76
use crate :: bitcoin:: TxIn ;
45
77
use crate :: psbt:: Psbt ;
46
78
use crate :: wallet:: AddressIndex ;
79
+ use crate :: wallet:: AddressIndex :: New ;
47
80
use crate :: wallet:: { get_funded_wallet, test:: get_test_wpkh} ;
48
- use crate :: SignOptions ;
81
+ use crate :: { psbt , FeeRate , SignOptions } ;
49
82
use std:: str:: FromStr ;
50
83
51
84
// from bip 174
@@ -118,4 +151,83 @@ mod test {
118
151
119
152
let _ = wallet. sign ( & mut psbt, SignOptions :: default ( ) ) . unwrap ( ) ;
120
153
}
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
+ }
121
233
}
0 commit comments