@@ -525,11 +525,100 @@ impl BitGoPsbt {
525525 unspents : & [ HydrationUnspentInput ] ,
526526 ) -> Result < Self , String > {
527527 use miniscript:: bitcoin:: consensus:: Decodable ;
528- use miniscript:: bitcoin:: { PublicKey , Transaction } ;
528+ use miniscript:: bitcoin:: Transaction ;
529529
530530 let tx = Transaction :: consensus_decode ( & mut & tx_bytes[ ..] )
531531 . map_err ( |e| format ! ( "Failed to decode transaction: {}" , e) ) ?;
532532
533+ let version = tx. version . 0 ;
534+ let lock_time = tx. lock_time . to_consensus_u32 ( ) ;
535+
536+ let mut psbt = Self :: new ( network, wallet_keys, Some ( version) , Some ( lock_time) ) ;
537+
538+ Self :: hydrate_psbt ( & mut psbt, & tx, wallet_keys, unspents) ?;
539+
540+ Ok ( psbt)
541+ }
542+
543+ /// Convert a half-signed legacy Zcash transaction to a PSBT with Zcash metadata.
544+ ///
545+ /// This is the Zcash-specific inverse of `get_half_signed_legacy_format()`.
546+ /// It decodes the Zcash wire format (which includes version_group_id, expiry_height, sapling_fields),
547+ /// extracts partial signatures, and reconstructs a Zcash PSBT.
548+ ///
549+ /// Unlike `from_half_signed_legacy_transaction`, this requires a `block_height` to determine
550+ /// the correct `consensus_branch_id` via network upgrade activation height lookup.
551+ ///
552+ /// # Arguments
553+ /// * `tx_bytes` - Zcash transaction bytes (overwintered format)
554+ /// * `network` - Zcash network (Zcash or ZcashTestnet)
555+ /// * `wallet_keys` - The wallet's root keys
556+ /// * `unspents` - Chain, index, value for each input
557+ /// * `block_height` - Block height to determine consensus branch ID
558+ ///
559+ /// # Returns
560+ /// Thin wrapper over `from_half_signed_legacy_transaction_zcash_with_consensus_branch_id`.
561+ /// Resolves consensus_branch_id from block height.
562+ /// Convert a half-signed legacy Zcash transaction to a PSBT with explicit consensus branch ID.
563+ ///
564+ /// This is similar to `from_half_signed_legacy_transaction_zcash`, but takes an explicit
565+ /// `consensus_branch_id` instead of deriving it from block height. Use this when you
566+ /// already know the consensus branch ID (e.g., 0xC2D6D0B4 for NU5, 0x76B809BB for Sapling).
567+ ///
568+ /// # Arguments
569+ /// * `tx_bytes` - Zcash transaction bytes (overwintered format)
570+ /// * `network` - Zcash network (Zcash or ZcashTestnet)
571+ /// * `wallet_keys` - The wallet's root keys
572+ /// * `unspents` - Chain, index, value for each input
573+ /// * `consensus_branch_id` - Explicit consensus branch ID for sighash computation
574+ ///
575+ /// # Returns
576+ /// A BitGoPsbt::Zcash instance with restored Sapling fields
577+ /// Helper that accepts already-decoded Zcash transaction parts (avoiding re-parsing).
578+ pub fn from_half_signed_legacy_transaction_zcash_with_consensus_branch_id_from_parts (
579+ parts : & crate :: zcash:: transaction:: ZcashTransactionParts ,
580+ network : Network ,
581+ wallet_keys : & crate :: fixed_script_wallet:: RootWalletKeys ,
582+ unspents : & [ HydrationUnspentInput ] ,
583+ consensus_branch_id : u32 ,
584+ ) -> Result < Self , String > {
585+ let tx = & parts. transaction ;
586+ let version = tx. version . 0 ;
587+ let lock_time = tx. lock_time . to_consensus_u32 ( ) ;
588+
589+ // Create Zcash PSBT using explicit consensus_branch_id
590+ let mut psbt = Self :: new_zcash (
591+ network,
592+ wallet_keys,
593+ consensus_branch_id,
594+ Some ( version) ,
595+ Some ( lock_time) ,
596+ parts. version_group_id ,
597+ parts. expiry_height ,
598+ ) ;
599+
600+ Self :: hydrate_psbt ( & mut psbt, tx, wallet_keys, unspents) ?;
601+
602+ // Restore Sapling fields in the Zcash PSBT variant
603+ if let BitGoPsbt :: Zcash ( ref mut zcash_psbt, _) = psbt {
604+ zcash_psbt. sapling_fields = parts. sapling_fields . clone ( ) ;
605+ }
606+
607+ Ok ( psbt)
608+ }
609+
610+ /// Helper that accepts already-decoded Zcash transaction parts (avoiding re-parsing).
611+ /// Private helper: hydrate inputs and outputs in an already-created PSBT.
612+ /// Shared logic for both Bitcoin-like and Zcash variants.
613+ fn hydrate_psbt (
614+ psbt : & mut BitGoPsbt ,
615+ tx : & miniscript:: bitcoin:: Transaction ,
616+ wallet_keys : & crate :: fixed_script_wallet:: RootWalletKeys ,
617+ unspents : & [ HydrationUnspentInput ] ,
618+ ) -> Result < ( ) , String > {
619+ use miniscript:: bitcoin:: PublicKey ;
620+
621+ // Validate input count
533622 if tx. input . len ( ) != unspents. len ( ) {
534623 return Err ( format ! (
535624 "Input count mismatch: tx has {} inputs, got {} unspents" ,
@@ -538,11 +627,6 @@ impl BitGoPsbt {
538627 ) ) ;
539628 }
540629
541- let version = tx. version . 0 ;
542- let lock_time = tx. lock_time . to_consensus_u32 ( ) ;
543-
544- let mut psbt = Self :: new ( network, wallet_keys, Some ( version) , Some ( lock_time) ) ;
545-
546630 // Parse each input from the legacy tx
547631 let input_results: Vec < legacy_txformat:: LegacyInputResult > = tx
548632 . input
@@ -554,6 +638,7 @@ impl BitGoPsbt {
554638 } )
555639 . collect :: < Result < Vec < _ > , _ > > ( ) ?;
556640
641+ // Hydrate inputs
557642 for ( i, ( tx_in, unspent) ) in tx. input . iter ( ) . zip ( unspents. iter ( ) ) . enumerate ( ) {
558643 match ( & input_results[ i] , unspent) {
559644 (
@@ -573,7 +658,7 @@ impl BitGoPsbt {
573658 psbt_wallet_input:: WalletInputOptions {
574659 sign_path : None ,
575660 sequence : Some ( tx_in. sequence . 0 ) ,
576- prev_tx : None , // psbt-lite: no nonWitnessUtxo
661+ prev_tx : None ,
577662 } ,
578663 )
579664 . map_err ( |e| format ! ( "Input {}: failed to add wallet input: {}" , i, e) ) ?;
@@ -652,12 +737,12 @@ impl BitGoPsbt {
652737 }
653738 }
654739
655- // Add outputs (plain script+value, no wallet metadata)
740+ // Add outputs
656741 for tx_out in & tx. output {
657742 psbt. add_output ( tx_out. script_pubkey . clone ( ) , tx_out. value . to_sat ( ) ) ;
658743 }
659744
660- Ok ( psbt )
745+ Ok ( ( ) )
661746 }
662747
663748 fn new_internal (
0 commit comments