@@ -454,6 +454,52 @@ impl BitGoPsbt {
454454 ) )
455455 }
456456
457+ /// Create a new empty PSBT with the same network parameters as an existing PSBT
458+ ///
459+ /// This is useful for reconstructing PSBTs - it copies:
460+ /// - Network
461+ /// - Transaction version and locktime
462+ /// - For Zcash: consensus_branch_id, version_group_id, expiry_height
463+ ///
464+ /// # Arguments
465+ /// * `template` - The existing PSBT to copy network parameters from
466+ /// * `wallet_keys` - The wallet's root keys for the new PSBT
467+ ///
468+ /// # Returns
469+ /// * `Ok(Self)` - A new empty PSBT with the same network parameters
470+ /// * `Err(String)` - If the template is a Zcash PSBT missing the consensus branch ID
471+ pub fn new_like (
472+ template : & BitGoPsbt ,
473+ wallet_keys : & crate :: fixed_script_wallet:: RootWalletKeys ,
474+ ) -> Result < Self , String > {
475+ let network = template. network ( ) ;
476+ let version = template. psbt ( ) . unsigned_tx . version . 0 ;
477+ let lock_time = template. psbt ( ) . unsigned_tx . lock_time . to_consensus_u32 ( ) ;
478+
479+ match template {
480+ BitGoPsbt :: Zcash ( zcash_psbt, _) => {
481+ // For Zcash, extract all required parameters from the template
482+ let consensus_branch_id = propkv:: get_zec_consensus_branch_id ( & zcash_psbt. psbt )
483+ . ok_or ( "Template PSBT missing ZecConsensusBranchId" ) ?;
484+ Ok ( Self :: new_zcash (
485+ network,
486+ wallet_keys,
487+ consensus_branch_id,
488+ Some ( version) ,
489+ Some ( lock_time) ,
490+ zcash_psbt. version_group_id ,
491+ zcash_psbt. expiry_height ,
492+ ) )
493+ }
494+ _ => Ok ( Self :: new (
495+ network,
496+ wallet_keys,
497+ Some ( version) ,
498+ Some ( lock_time) ,
499+ ) ) ,
500+ }
501+ }
502+
457503 fn new_internal (
458504 network : Network ,
459505 wallet_keys : & crate :: fixed_script_wallet:: RootWalletKeys ,
@@ -1075,6 +1121,37 @@ impl BitGoPsbt {
10751121 }
10761122 }
10771123
1124+ /// Extract the finalized transaction bytes with network-appropriate serialization
1125+ ///
1126+ /// This method extracts the fully-signed transaction from a finalized PSBT,
1127+ /// serializing it with the correct format for the network:
1128+ /// - For Zcash: includes version_group_id, expiry_height, and sapling fields
1129+ /// - For other networks: uses standard Bitcoin transaction serialization
1130+ ///
1131+ /// This method consumes the PSBT since the underlying `extract_tx()` requires ownership.
1132+ ///
1133+ /// # Requirements
1134+ /// All inputs must be finalized before calling this method.
1135+ ///
1136+ /// # Returns
1137+ /// * `Ok(Vec<u8>)` - The serialized transaction bytes
1138+ /// * `Err(String)` - If transaction extraction fails
1139+ pub fn extract_tx ( self ) -> Result < Vec < u8 > , String > {
1140+ use miniscript:: bitcoin:: consensus:: serialize;
1141+
1142+ match self {
1143+ BitGoPsbt :: Zcash ( zcash_psbt, _) => zcash_psbt
1144+ . extract_tx ( )
1145+ . map_err ( |e| format ! ( "Failed to extract transaction: {}" , e) ) ,
1146+ BitGoPsbt :: BitcoinLike ( psbt, _) | BitGoPsbt :: Dash ( DashBitGoPsbt { psbt, .. } , _) => {
1147+ let tx = psbt
1148+ . extract_tx ( )
1149+ . map_err ( |e| format ! ( "Failed to extract transaction: {}" , e) ) ?;
1150+ Ok ( serialize ( & tx) )
1151+ }
1152+ }
1153+ }
1154+
10781155 pub fn into_psbt ( self ) -> Psbt {
10791156 match self {
10801157 BitGoPsbt :: BitcoinLike ( psbt, _network) => psbt,
@@ -3212,27 +3289,29 @@ mod tests {
32123289 format,
32133290 )
32143291 . expect( "Failed to load fixture" ) ;
3215- let bitgo_psbt = fixture
3292+ let mut bitgo_psbt = fixture
32163293 . to_bitgo_psbt( network)
32173294 . expect( "Failed to convert to BitGo PSBT" ) ;
32183295 let fixture_extracted_transaction = fixture
32193296 . extracted_transaction
32203297 . expect( "Failed to extract transaction" ) ;
32213298
3222- // // Use BitGoPsbt::finalize() which handles MuSig2 inputs
3299+ // Finalize and extract using the network-aware extract_tx() method
32233300 let secp = crate :: bitcoin:: secp256k1:: Secp256k1 :: new( ) ;
3224- let finalized_psbt = bitgo_psbt. finalize( & secp) . expect( "Failed to finalize PSBT" ) ;
3225- let extracted_transaction = finalized_psbt
3301+ bitgo_psbt
3302+ . finalize_mut( & secp)
3303+ . expect( "Failed to finalize PSBT" ) ;
3304+
3305+ let extracted_tx_bytes = bitgo_psbt
32263306 . extract_tx( )
32273307 . expect( "Failed to extract transaction" ) ;
3228- use miniscript :: bitcoin :: consensus :: serialize ;
3229- let extracted_transaction_hex = hex :: encode ( serialize ( & extracted_transaction ) ) ;
3308+ let extracted_transaction_hex = hex :: encode ( extracted_tx_bytes ) ;
3309+
32303310 assert_eq!(
32313311 extracted_transaction_hex, fixture_extracted_transaction,
32323312 "Extracted transaction should match"
32333313 ) ;
3234- // Zcash fixtures were created with legacy Bitcoin sighash; implementation uses ZIP-243
3235- } , ignore: [ Zcash ] ) ;
3314+ } ) ;
32363315
32373316 #[ test]
32383317 fn test_add_paygo_attestation ( ) {
@@ -3418,32 +3497,37 @@ mod tests {
34183497 format,
34193498 )
34203499 . expect( "Failed to load fixture" ) ;
3421-
3500+
34223501 let bitgo_psbt = fixture
34233502 . to_bitgo_psbt( network)
34243503 . expect( "Failed to convert to BitGo PSBT" ) ;
3425-
3504+
34263505 // Get wallet keys from fixture
34273506 let wallet_xprv = fixture
34283507 . get_wallet_xprvs( )
34293508 . expect( "Failed to get wallet keys" ) ;
34303509 let wallet_keys = wallet_xprv. to_root_wallet_keys( ) ;
3431-
3510+
34323511 // Create replay protection with the replay protection script from fixture
34333512 let replay_protection = crate :: fixed_script_wallet:: ReplayProtection :: new( vec![
3434- miniscript:: bitcoin:: ScriptBuf :: from_hex( "a91420b37094d82a513451ff0ccd9db23aba05bc5ef387" )
3435- . expect( "Failed to parse replay protection output script" ) ,
3513+ miniscript:: bitcoin:: ScriptBuf :: from_hex(
3514+ "a91420b37094d82a513451ff0ccd9db23aba05bc5ef387" ,
3515+ )
3516+ . expect( "Failed to parse replay protection output script" ) ,
34363517 ] ) ;
3437-
3518+
34383519 // Parse the transaction (no PayGo verification in tests)
34393520 let parsed = bitgo_psbt
34403521 . parse_transaction_with_wallet_keys( & wallet_keys, & replay_protection, & [ ] )
34413522 . expect( "Failed to parse transaction" ) ;
3442-
3523+
34433524 // Basic validations
34443525 assert!( !parsed. inputs. is_empty( ) , "Should have at least one input" ) ;
3445- assert!( !parsed. outputs. is_empty( ) , "Should have at least one output" ) ;
3446-
3526+ assert!(
3527+ !parsed. outputs. is_empty( ) ,
3528+ "Should have at least one output"
3529+ ) ;
3530+
34473531 // Verify at least one replay protection input exists
34483532 let replay_protection_inputs = parsed
34493533 . inputs
@@ -3454,18 +3538,15 @@ mod tests {
34543538 replay_protection_inputs > 0 ,
34553539 "Should have at least one replay protection input"
34563540 ) ;
3457-
3541+
34583542 // Verify at least one wallet input exists
34593543 let wallet_inputs = parsed
34603544 . inputs
34613545 . iter( )
34623546 . filter( |i| i. script_id. is_some( ) )
34633547 . count( ) ;
3464- assert!(
3465- wallet_inputs > 0 ,
3466- "Should have at least one wallet input"
3467- ) ;
3468-
3548+ assert!( wallet_inputs > 0 , "Should have at least one wallet input" ) ;
3549+
34693550 // Count internal (wallet) and external outputs
34703551 let internal_outputs = parsed
34713552 . outputs
@@ -3477,13 +3558,13 @@ mod tests {
34773558 . iter( )
34783559 . filter( |o| o. script_id. is_none( ) )
34793560 . count( ) ;
3480-
3561+
34813562 assert_eq!(
34823563 internal_outputs + external_outputs,
34833564 parsed. outputs. len( ) ,
34843565 "All outputs should be either internal or external"
34853566 ) ;
3486-
3567+
34873568 // Verify spend amount only includes external outputs
34883569 let calculated_spend_amount: u64 = parsed
34893570 . outputs
@@ -3495,23 +3576,23 @@ mod tests {
34953576 parsed. spend_amount, calculated_spend_amount,
34963577 "Spend amount should equal sum of external output values"
34973578 ) ;
3498-
3579+
34993580 // Verify total values
35003581 let total_input_value: u64 = parsed. inputs. iter( ) . map( |i| i. value) . sum( ) ;
35013582 let total_output_value: u64 = parsed. outputs. iter( ) . map( |o| o. value) . sum( ) ;
3502-
3583+
35033584 assert_eq!(
35043585 parsed. miner_fee,
35053586 total_input_value - total_output_value,
35063587 "Miner fee should equal inputs minus outputs"
35073588 ) ;
3508-
3589+
35093590 // Verify virtual size is reasonable
35103591 assert!(
35113592 parsed. virtual_size > 0 ,
35123593 "Virtual size should be greater than 0"
35133594 ) ;
3514-
3595+
35153596 // Verify outputs (fixtures now have 3 external outputs)
35163597 assert_eq!(
35173598 external_outputs, 3 ,
@@ -3526,7 +3607,7 @@ mod tests {
35263607 parsed. spend_amount > 0 ,
35273608 "Spend amount should be greater than 0 when there are external outputs"
35283609 ) ;
3529- } , ignore : [ ] ) ;
3610+ } ) ;
35303611
35313612 #[ test]
35323613 fn test_serialize_bitcoin_psbt ( ) {
@@ -3636,19 +3717,9 @@ mod tests {
36363717 . parse_outputs ( & other_wallet_keys, & [ ] )
36373718 . expect ( "Failed to parse outputs with other wallet keys" ) ;
36383719
3639- // Create empty PSBT with same version and locktime as original
3640- let original_version = original_psbt. psbt ( ) . unsigned_tx . version . 0 as i32 ;
3641- let original_locktime = original_psbt
3642- . psbt ( )
3643- . unsigned_tx
3644- . lock_time
3645- . to_consensus_u32 ( ) ;
3646- let mut reconstructed = BitGoPsbt :: new (
3647- network,
3648- & wallet_keys,
3649- Some ( original_version) ,
3650- Some ( original_locktime) ,
3651- ) ;
3720+ // Create empty PSBT with same network parameters as original (handles Zcash automatically)
3721+ let mut reconstructed = BitGoPsbt :: new_like ( & original_psbt, & wallet_keys)
3722+ . expect ( "Failed to create PSBT from template" ) ;
36523723
36533724 // Track which inputs are wallet inputs vs replay protection
36543725 let mut wallet_input_indices = Vec :: new ( ) ;
@@ -3958,15 +4029,14 @@ mod tests {
39584029 let reconstructed_bytes = reconstructed
39594030 . serialize ( )
39604031 . expect ( "Failed to serialize reconstructed" ) ;
3961- assert_equal_psbt ( & original_bytes, & reconstructed_bytes) ;
4032+ assert_equal_psbt ( & original_bytes, & reconstructed_bytes, network ) ;
39624033 }
39634034
39644035 // Note: Only testing PsbtLite format for now because full PSBT format
39654036 // uses non_witness_utxo instead of witness_utxo for non-segwit inputs
3966- // Zcash: Transaction decoding fails because Zcash tx format differs from Bitcoin
39674037 crate :: test_psbt_fixtures!( test_psbt_reconstruction, network, format, {
39684038 test_psbt_reconstruction_for_network( network, format) ;
3969- } , ignore : [ Zcash ] ) ;
4039+ } ) ;
39704040
39714041 #[ test]
39724042 fn test_dogecoin_single_input_single_output_large_amount ( ) {
0 commit comments