@@ -16,7 +16,7 @@ pub mod zcash_psbt;
1616
1717use crate :: Network ;
1818pub use dash_psbt:: DashBitGoPsbt ;
19- use miniscript:: bitcoin:: { psbt:: Psbt , secp256k1, CompressedPublicKey , Txid } ;
19+ use miniscript:: bitcoin:: { psbt:: Psbt , secp256k1, CompressedPublicKey , FeeRate , Txid } ;
2020pub use propkv:: {
2121 find_kv, get_zec_consensus_branch_id, BitGoKeyValue , ProprietaryKeySubtype ,
2222 WasmUtxoVersionInfo , BITGO ,
@@ -339,6 +339,45 @@ pub(crate) fn make_psbt_with_xpubs(
339339 psbt
340340}
341341
342+ /// Fee-rate policy applied during PSBT extraction.
343+ ///
344+ /// Controls how rust-bitcoin's absurd-fee-rate guard behaves when extracting a
345+ /// finalized transaction. This is pure plumbing — it carries no per-coin
346+ /// policy; callers that want a coin-aware default must resolve it themselves
347+ /// and pass [`ExtractFeePolicy::Limited`] or [`ExtractFeePolicy::Unchecked`].
348+ ///
349+ /// All fee rates here are in **sat/vB** (rust-bitcoin's `FeeRate` unit). The
350+ /// wasm/JS boundary also uses **sat/vB**; callers holding sat/kB thresholds
351+ /// must convert (÷ 1000) before calling. See `fee_policy_from_js`.
352+ #[ derive( Debug , Clone , Copy ) ]
353+ pub enum ExtractFeePolicy {
354+ /// Use rust-bitcoin's stock absurd-fee-rate check (`Psbt::extract_tx`).
355+ Default ,
356+ /// Skip the absurd-fee-rate check entirely (`Psbt::extract_tx_unchecked_fee_rate`).
357+ Unchecked ,
358+ /// Enforce a maximum fee rate in **sat/vB**
359+ /// (`Psbt::extract_tx_with_fee_rate_limit`).
360+ Limited ( FeeRate ) ,
361+ }
362+
363+ /// Extract a `Transaction` from a rust-bitcoin `Psbt` applying an
364+ /// [`ExtractFeePolicy`]. Shared by the `BitcoinLike` and `Dash` branches,
365+ /// which both hold an inner `Psbt`.
366+ fn extract_inner_with_fee_policy (
367+ psbt : Psbt ,
368+ policy : ExtractFeePolicy ,
369+ ) -> Result < miniscript:: bitcoin:: Transaction , String > {
370+ match policy {
371+ ExtractFeePolicy :: Default => psbt
372+ . extract_tx ( )
373+ . map_err ( |e| format ! ( "Failed to extract transaction: {}" , e) ) ,
374+ ExtractFeePolicy :: Unchecked => Ok ( psbt. extract_tx_unchecked_fee_rate ( ) ) ,
375+ ExtractFeePolicy :: Limited ( max_fee_rate_sat_per_vb) => psbt
376+ . extract_tx_with_fee_rate_limit ( max_fee_rate_sat_per_vb)
377+ . map_err ( |e| format ! ( "Failed to extract transaction: {}" , e) ) ,
378+ }
379+ }
380+
342381impl BitGoPsbt {
343382 /// Deserialize a PSBT from bytes, using network-specific logic
344383 pub fn deserialize ( psbt_bytes : & [ u8 ] , network : Network ) -> Result < BitGoPsbt , DeserializeError > {
@@ -1371,16 +1410,30 @@ impl BitGoPsbt {
13711410 /// * `Ok(Vec<u8>)` - The serialized transaction bytes
13721411 /// * `Err(String)` - If transaction extraction fails
13731412 pub fn extract_tx ( self ) -> Result < Vec < u8 > , String > {
1413+ self . extract_tx_with_fee_policy ( ExtractFeePolicy :: Default )
1414+ }
1415+
1416+ /// Extract the fully-signed transaction from a finalized PSBT with an
1417+ /// explicit fee-rate [`policy`][ExtractFeePolicy].
1418+ ///
1419+ /// This method consumes the PSBT since the underlying `extract_tx()` requires ownership.
1420+ ///
1421+ /// # Requirements
1422+ /// All inputs must be finalized before calling this method.
1423+ ///
1424+ /// # Returns
1425+ /// * `Ok(Vec<u8>)` - The serialized transaction bytes
1426+ /// * `Err(String)` - If transaction extraction fails (including absurd-fee
1427+ /// rejection when a [`Limited`][ExtractFeePolicy::Limited] policy is enforced)
1428+ pub fn extract_tx_with_fee_policy ( self , policy : ExtractFeePolicy ) -> Result < Vec < u8 > , String > {
13741429 use miniscript:: bitcoin:: consensus:: serialize;
13751430
13761431 match self {
13771432 BitGoPsbt :: Zcash ( zcash_psbt, _) => zcash_psbt
1378- . extract_tx ( )
1433+ . extract_tx_with_fee_policy ( policy )
13791434 . map_err ( |e| format ! ( "Failed to extract transaction: {}" , e) ) ,
13801435 BitGoPsbt :: BitcoinLike ( psbt, _) | BitGoPsbt :: Dash ( DashBitGoPsbt { psbt, .. } , _) => {
1381- let tx = psbt
1382- . extract_tx ( )
1383- . map_err ( |e| format ! ( "Failed to extract transaction: {}" , e) ) ?;
1436+ let tx = extract_inner_with_fee_policy ( psbt, policy) ?;
13841437 Ok ( serialize ( & tx) )
13851438 }
13861439 }
@@ -1392,10 +1445,21 @@ impl BitGoPsbt {
13921445 /// * `Ok(Transaction)` - The extracted transaction
13931446 /// * `Err(String)` - If not BitcoinLike or extraction fails
13941447 pub fn extract_bitcoin_tx ( self ) -> Result < miniscript:: bitcoin:: Transaction , String > {
1448+ self . extract_bitcoin_tx_with_fee_policy ( ExtractFeePolicy :: Default )
1449+ }
1450+
1451+ /// Extract the Bitcoin transaction directly (for BitcoinLike networks only)
1452+ /// with an explicit fee-rate [`policy`][ExtractFeePolicy].
1453+ ///
1454+ /// # Returns
1455+ /// * `Ok(Transaction)` - The extracted transaction
1456+ /// * `Err(String)` - If not BitcoinLike or extraction fails
1457+ pub fn extract_bitcoin_tx_with_fee_policy (
1458+ self ,
1459+ policy : ExtractFeePolicy ,
1460+ ) -> Result < miniscript:: bitcoin:: Transaction , String > {
13951461 match self {
1396- BitGoPsbt :: BitcoinLike ( psbt, _) => psbt
1397- . extract_tx ( )
1398- . map_err ( |e| format ! ( "Failed to extract transaction: {}" , e) ) ,
1462+ BitGoPsbt :: BitcoinLike ( psbt, _) => extract_inner_with_fee_policy ( psbt, policy) ,
13991463 _ => Err ( "extract_bitcoin_tx only supported for BitcoinLike networks" . to_string ( ) ) ,
14001464 }
14011465 }
@@ -1406,13 +1470,23 @@ impl BitGoPsbt {
14061470 /// * `Ok(DashTransactionParts)` - The extracted transaction parts
14071471 /// * `Err(String)` - If not Dash or extraction fails
14081472 pub fn extract_dash_tx ( self ) -> Result < crate :: dash:: transaction:: DashTransactionParts , String > {
1473+ self . extract_dash_tx_with_fee_policy ( ExtractFeePolicy :: Default )
1474+ }
1475+
1476+ /// Extract the Dash transaction parts directly with an explicit fee-rate
1477+ /// [`policy`][ExtractFeePolicy].
1478+ ///
1479+ /// # Returns
1480+ /// * `Ok(DashTransactionParts)` - The extracted transaction parts
1481+ /// * `Err(String)` - If not Dash or extraction fails
1482+ pub fn extract_dash_tx_with_fee_policy (
1483+ self ,
1484+ policy : ExtractFeePolicy ,
1485+ ) -> Result < crate :: dash:: transaction:: DashTransactionParts , String > {
14091486 use miniscript:: bitcoin:: consensus:: serialize;
14101487 match self {
14111488 BitGoPsbt :: Dash ( dash_psbt, _) => {
1412- let tx = dash_psbt
1413- . psbt
1414- . extract_tx ( )
1415- . map_err ( |e| format ! ( "Failed to extract transaction: {}" , e) ) ?;
1489+ let tx = extract_inner_with_fee_policy ( dash_psbt. psbt , policy) ?;
14161490 let tx_bytes = serialize ( & tx) ;
14171491 crate :: dash:: transaction:: decode_dash_transaction_parts ( & tx_bytes)
14181492 . map_err ( |e| format ! ( "Failed to decode Dash transaction: {}" , e) )
@@ -1428,11 +1502,24 @@ impl BitGoPsbt {
14281502 /// * `Err(String)` - If not Zcash or extraction fails
14291503 pub fn extract_zcash_tx (
14301504 self ,
1505+ ) -> Result < crate :: zcash:: transaction:: ZcashTransactionParts , String > {
1506+ self . extract_zcash_tx_with_fee_policy ( ExtractFeePolicy :: Default )
1507+ }
1508+
1509+ /// Extract the Zcash transaction parts directly with an explicit fee-rate
1510+ /// [`policy`][ExtractFeePolicy].
1511+ ///
1512+ /// # Returns
1513+ /// * `Ok(ZcashTransactionParts)` - The extracted transaction parts
1514+ /// * `Err(String)` - If not Zcash or extraction fails
1515+ pub fn extract_zcash_tx_with_fee_policy (
1516+ self ,
1517+ policy : ExtractFeePolicy ,
14311518 ) -> Result < crate :: zcash:: transaction:: ZcashTransactionParts , String > {
14321519 match self {
14331520 BitGoPsbt :: Zcash ( zcash_psbt, _) => {
14341521 let bytes = zcash_psbt
1435- . extract_tx ( )
1522+ . extract_tx_with_fee_policy ( policy )
14361523 . map_err ( |e| format ! ( "Failed to extract transaction: {}" , e) ) ?;
14371524 crate :: zcash:: transaction:: decode_zcash_transaction_parts ( & bytes)
14381525 . map_err ( |e| format ! ( "Failed to decode Zcash transaction: {}" , e) )
@@ -4119,6 +4206,67 @@ mod tests {
41194206 ) ;
41204207 } ) ;
41214208
4209+ /// `extract_tx_with_fee_policy` must produce byte-identical output to the
4210+ /// default `extract_tx()` for a normal-fee PSBT across all three policies
4211+ /// (`Default`, `Unchecked`, `Limited`). This pins the param plumbing added
4212+ /// in this change without depending on per-coin fee policy.
4213+ #[ test]
4214+ fn test_extract_tx_with_fee_policy_matches_default ( ) {
4215+ use crate :: fixed_script_wallet:: test_utils:: fixtures:: {
4216+ self , FixtureNamespace , SignatureState , TxFormat ,
4217+ } ;
4218+ use crate :: Network ;
4219+
4220+ let network = Network :: Bitcoin ;
4221+ let fixture = fixtures:: load_psbt_fixture_with_format_and_namespace (
4222+ network. to_utxolib_name ( ) ,
4223+ SignatureState :: Fullsigned ,
4224+ TxFormat :: Psbt ,
4225+ FixtureNamespace :: UtxolibCompat ,
4226+ )
4227+ . expect ( "Failed to load fixture" ) ;
4228+ let mut bitgo_psbt = fixture
4229+ . to_bitgo_psbt ( network)
4230+ . expect ( "Failed to convert to BitGo PSBT" ) ;
4231+
4232+ let secp = crate :: bitcoin:: secp256k1:: Secp256k1 :: new ( ) ;
4233+ bitgo_psbt
4234+ . finalize_mut ( & secp)
4235+ . expect ( "Failed to finalize PSBT" ) ;
4236+
4237+ let default_bytes = bitgo_psbt. clone ( ) . extract_tx ( ) . expect ( "default extract" ) ;
4238+
4239+ // A high sat/vB ceiling that no normal-fee tx would trip.
4240+ let high_limit_sat_per_vb = ExtractFeePolicy :: Limited (
4241+ miniscript:: bitcoin:: FeeRate :: from_sat_per_vb_unchecked ( 1_000_000_000 ) ,
4242+ ) ;
4243+
4244+ assert_eq ! (
4245+ bitgo_psbt
4246+ . clone( )
4247+ . extract_tx_with_fee_policy( ExtractFeePolicy :: Default )
4248+ . expect( "Default policy" ) ,
4249+ default_bytes,
4250+ "Default policy must match extract_tx()"
4251+ ) ;
4252+ assert_eq ! (
4253+ bitgo_psbt
4254+ . clone( )
4255+ . extract_tx_with_fee_policy( ExtractFeePolicy :: Unchecked )
4256+ . expect( "Unchecked policy" ) ,
4257+ default_bytes,
4258+ "Unchecked policy must match extract_tx() on a normal-fee PSBT"
4259+ ) ;
4260+ assert_eq ! (
4261+ bitgo_psbt
4262+ . clone( )
4263+ . extract_tx_with_fee_policy( high_limit_sat_per_vb)
4264+ . expect( "Limited policy" ) ,
4265+ default_bytes,
4266+ "Limited policy with a high sat/vB ceiling must match extract_tx()"
4267+ ) ;
4268+ }
4269+
41224270 /// Test extract_half_signed_legacy_tx for p2ms-based script types
41234271 fn test_extract_half_signed_legacy_tx_for_script_type (
41244272 network : Network ,
0 commit comments