@@ -12,10 +12,11 @@ pub use p2tr_musig2_input::{
1212 parse_musig2_nonces, parse_musig2_partial_sigs, parse_musig2_participants, Musig2Error ,
1313 Musig2Input , Musig2PartialSig , Musig2Participants , Musig2PubNonce ,
1414} ;
15+ pub use propkv:: { BitGoKeyValue , ProprietaryKeySubtype , BITGO } ;
1516pub use sighash:: validate_sighash_type;
1617
1718use crate :: { bitgo_psbt:: zcash_psbt:: ZcashPsbt , networks:: Network } ;
18- use miniscript:: bitcoin:: psbt:: Psbt ;
19+ use miniscript:: bitcoin:: { psbt:: Psbt , secp256k1 } ;
1920
2021#[ derive( Debug ) ]
2122pub enum DeserializeError {
@@ -146,6 +147,88 @@ impl BitGoPsbt {
146147 BitGoPsbt :: Zcash ( zcash_psbt, _network) => zcash_psbt. into_bitcoin_psbt ( ) ,
147148 }
148149 }
150+
151+ pub fn finalize_input < C : secp256k1:: Verification > (
152+ & mut self ,
153+ secp : & secp256k1:: Secp256k1 < C > ,
154+ input_index : usize ,
155+ ) -> Result < ( ) , String > {
156+ use miniscript:: psbt:: PsbtExt ;
157+
158+ match self {
159+ BitGoPsbt :: BitcoinLike ( ref mut psbt, _network) => {
160+ // Use custom bitgo p2trMusig2 input finalization for MuSig2 inputs
161+ if Musig2Input :: is_musig2_input ( & psbt. inputs [ input_index] ) {
162+ Musig2Input :: finalize_input ( psbt, secp, input_index)
163+ . map_err ( |e| e. to_string ( ) ) ?;
164+ return Ok ( ( ) ) ;
165+ }
166+ // other inputs can be finalized using the standard miniscript::psbt::finalize_input
167+ psbt. finalize_inp_mut ( secp, input_index)
168+ . map_err ( |e| e. to_string ( ) ) ?;
169+ Ok ( ( ) )
170+ }
171+ BitGoPsbt :: Zcash ( _zcash_psbt, _network) => {
172+ todo ! ( "Zcash PSBT finalization not yet implemented" ) ;
173+ }
174+ }
175+ }
176+
177+ /// Finalize all inputs in the PSBT, attempting each input even if some fail.
178+ /// Similar to miniscript::psbt::PsbtExt::finalize_mut.
179+ ///
180+ /// # Returns
181+ /// - `Ok(())` if all inputs were successfully finalized
182+ /// - `Err(Vec<String>)` containing error messages for each failed input
183+ ///
184+ /// # Note
185+ /// This method will attempt to finalize ALL inputs, collecting errors for any that fail.
186+ /// It does not stop at the first error.
187+ pub fn finalize_mut < C : secp256k1:: Verification > (
188+ & mut self ,
189+ secp : & secp256k1:: Secp256k1 < C > ,
190+ ) -> Result < ( ) , Vec < String > > {
191+ let num_inputs = match self {
192+ BitGoPsbt :: BitcoinLike ( psbt, _network) => psbt. inputs . len ( ) ,
193+ BitGoPsbt :: Zcash ( zcash_psbt, _network) => zcash_psbt. psbt . inputs . len ( ) ,
194+ } ;
195+
196+ let mut errors = vec ! [ ] ;
197+ for index in 0 ..num_inputs {
198+ match self . finalize_input ( secp, index) {
199+ Ok ( ( ) ) => { }
200+ Err ( e) => {
201+ errors. push ( format ! ( "Input {}: {}" , index, e) ) ;
202+ }
203+ }
204+ }
205+
206+ if errors. is_empty ( ) {
207+ Ok ( ( ) )
208+ } else {
209+ Err ( errors)
210+ }
211+ }
212+
213+ /// Finalize all inputs and consume the PSBT, returning the finalized PSBT.
214+ /// Similar to miniscript::psbt::PsbtExt::finalize.
215+ ///
216+ /// # Returns
217+ /// - `Ok(Psbt)` if all inputs were successfully finalized
218+ /// - `Err(String)` containing a formatted error message if any input failed
219+ pub fn finalize < C : secp256k1:: Verification > (
220+ mut self ,
221+ secp : & secp256k1:: Secp256k1 < C > ,
222+ ) -> Result < Psbt , String > {
223+ match self . finalize_mut ( secp) {
224+ Ok ( ( ) ) => Ok ( self . into_psbt ( ) ) ,
225+ Err ( errors) => Err ( format ! (
226+ "Failed to finalize {} input(s): {}" ,
227+ errors. len( ) ,
228+ errors. join( "; " )
229+ ) ) ,
230+ }
231+ }
149232}
150233
151234#[ cfg( test) ]
@@ -303,46 +386,18 @@ mod tests {
303386 output. script_pubkey . to_hex_string ( )
304387 }
305388
306- fn test_wallet_script_type (
307- script_type : fixtures:: ScriptType ,
389+ fn assert_matches_wallet_scripts (
308390 network : Network ,
309391 tx_format : fixtures:: TxFormat ,
392+ fixture : & fixtures:: PsbtFixture ,
393+ wallet_keys : & RootWalletKeys ,
394+ input_index : usize ,
395+ input_fixture : & fixtures:: PsbtInputFixture ,
310396 ) -> Result < ( ) , String > {
311- let fixture = fixtures:: load_psbt_fixture_with_format (
312- network. to_utxolib_name ( ) ,
313- fixtures:: SignatureState :: Fullsigned ,
314- tx_format,
315- )
316- . expect ( "Failed to load fixture" ) ;
317- let xprvs = fixtures:: parse_wallet_keys ( & fixture) . expect ( "Failed to parse wallet keys" ) ;
318- let secp = crate :: bitcoin:: secp256k1:: Secp256k1 :: new ( ) ;
319- let wallet_keys = RootWalletKeys :: new (
320- xprvs
321- . iter ( )
322- . map ( |x| Xpub :: from_priv ( & secp, x) )
323- . collect :: < Vec < _ > > ( )
324- . try_into ( )
325- . expect ( "Failed to convert to XpubTriple" ) ,
326- ) ;
327-
328- // Check if the script type is supported by the network
329- let output_script_support = network. output_script_support ( ) ;
330- let input_fixture = fixture. find_input_with_script_type ( script_type) ;
331- if !script_type. is_supported_by ( & output_script_support) {
332- // Script type not supported by network - skip test (no fixture expected)
333- assert ! (
334- input_fixture. is_err( ) ,
335- "Expected error for unsupported script type"
336- ) ;
337- return Ok ( ( ) ) ;
338- }
339-
340- let ( input_index, input_fixture) = input_fixture. unwrap ( ) ;
341-
342397 let ( chain, index) =
343398 parse_fixture_paths ( input_fixture) . expect ( "Failed to parse fixture paths" ) ;
344399 let scripts = WalletScripts :: from_wallet_keys (
345- & wallet_keys,
400+ wallet_keys,
346401 chain,
347402 index,
348403 & network. output_script_support ( ) ,
@@ -421,21 +476,96 @@ mod tests {
421476 ) ) ;
422477 }
423478 }
479+ Ok ( ( ) )
480+ }
481+
482+ fn assert_finalize_input (
483+ mut bitgo_psbt : BitGoPsbt ,
484+ input_index : usize ,
485+ _network : Network ,
486+ _tx_format : fixtures:: TxFormat ,
487+ ) -> Result < ( ) , String > {
488+ let secp = crate :: bitcoin:: secp256k1:: Secp256k1 :: new ( ) ;
489+ bitgo_psbt
490+ . finalize_input ( & secp, input_index)
491+ . map_err ( |e| e. to_string ( ) ) ?;
492+ Ok ( ( ) )
493+ }
494+
495+ fn test_wallet_script_type (
496+ script_type : fixtures:: ScriptType ,
497+ network : Network ,
498+ tx_format : fixtures:: TxFormat ,
499+ ) -> Result < ( ) , String > {
500+ let fixture = fixtures:: load_psbt_fixture_with_format (
501+ network. to_utxolib_name ( ) ,
502+ fixtures:: SignatureState :: Fullsigned ,
503+ tx_format,
504+ )
505+ . expect ( "Failed to load fixture" ) ;
506+ let wallet_keys =
507+ fixtures:: parse_wallet_keys ( & fixture) . expect ( "Failed to parse wallet keys" ) ;
508+ let secp = crate :: bitcoin:: secp256k1:: Secp256k1 :: new ( ) ;
509+ let wallet_keys = RootWalletKeys :: new (
510+ wallet_keys
511+ . iter ( )
512+ . map ( |x| Xpub :: from_priv ( & secp, x) )
513+ . collect :: < Vec < _ > > ( )
514+ . try_into ( )
515+ . expect ( "Failed to convert to XpubTriple" ) ,
516+ ) ;
517+
518+ // Check if the script type is supported by the network
519+ let output_script_support = network. output_script_support ( ) ;
520+ let input_fixture = fixture. find_input_with_script_type ( script_type) ;
521+ if !script_type. is_supported_by ( & output_script_support) {
522+ // Script type not supported by network - skip test (no fixture expected)
523+ assert ! (
524+ input_fixture. is_err( ) ,
525+ "Expected error for unsupported script type"
526+ ) ;
527+ return Ok ( ( ) ) ;
528+ }
529+
530+ let ( input_index, input_fixture) = input_fixture. unwrap ( ) ;
531+
532+ assert_matches_wallet_scripts (
533+ network,
534+ tx_format,
535+ & fixture,
536+ & wallet_keys,
537+ input_index,
538+ input_fixture,
539+ ) ?;
540+
541+ assert_finalize_input (
542+ fixture. to_bitgo_psbt ( network) . unwrap ( ) ,
543+ input_index,
544+ network,
545+ tx_format,
546+ ) ?;
424547
425548 Ok ( ( ) )
426549 }
427550
428551 crate :: test_psbt_fixtures!( test_p2sh_script_generation_from_fixture, network, format, {
429552 test_wallet_script_type( fixtures:: ScriptType :: P2sh , network, format) . unwrap( ) ;
430- } ) ;
553+ } , ignore: [
554+ // TODO: sighash support
555+ BitcoinCash , Ecash , BitcoinGold ,
556+ // TODO: zec support
557+ Zcash ,
558+ ] ) ;
431559
432560 crate :: test_psbt_fixtures!(
433561 test_p2sh_p2wsh_script_generation_from_fixture,
434562 network,
435563 format,
436564 {
437565 test_wallet_script_type( fixtures:: ScriptType :: P2shP2wsh , network, format) . unwrap( ) ;
438- }
566+ } ,
567+ // TODO: sighash support
568+ ignore: [ BitcoinGold ]
439569 ) ;
440570
441571 crate :: test_psbt_fixtures!(
@@ -444,7 +574,9 @@ mod tests {
444574 format,
445575 {
446576 test_wallet_script_type( fixtures:: ScriptType :: P2wsh , network, format) . unwrap( ) ;
447- }
577+ } ,
578+ // TODO: sighash support
579+ ignore: [ BitcoinGold ]
448580 ) ;
449581
450582 crate :: test_psbt_fixtures!( test_p2tr_script_generation_from_fixture, network, format, {
@@ -469,6 +601,34 @@ mod tests {
469601 }
470602 ) ;
471603
604+ crate :: test_psbt_fixtures!( test_extract_transaction, network, format, {
605+ let fixture = fixtures:: load_psbt_fixture_with_format(
606+ network. to_utxolib_name( ) ,
607+ fixtures:: SignatureState :: Fullsigned ,
608+ format,
609+ )
610+ . expect( "Failed to load fixture" ) ;
611+ let bitgo_psbt = fixture
612+ . to_bitgo_psbt( network)
613+ . expect( "Failed to convert to BitGo PSBT" ) ;
614+ let fixture_extracted_transaction = fixture
615+ . extracted_transaction
616+ . expect( "Failed to extract transaction" ) ;
617+
618+ // // Use BitGoPsbt::finalize() which handles MuSig2 inputs
619+ let secp = crate :: bitcoin:: secp256k1:: Secp256k1 :: new( ) ;
620+ let finalized_psbt = bitgo_psbt. finalize( & secp) . expect( "Failed to finalize PSBT" ) ;
621+ let extracted_transaction = finalized_psbt
622+ . extract_tx( )
623+ . expect( "Failed to extract transaction" ) ;
624+ use miniscript:: bitcoin:: consensus:: serialize;
625+ let extracted_transaction_hex = hex:: encode( serialize( & extracted_transaction) ) ;
626+ assert_eq!(
627+ extracted_transaction_hex, fixture_extracted_transaction,
628+ "Extracted transaction should match"
629+ ) ;
630+ } , ignore: [ BitcoinGold , BitcoinCash , Ecash , Zcash ] ) ;
631+
472632 #[ test]
473633 fn test_serialize_bitcoin_psbt ( ) {
474634 // Test that Bitcoin-like PSBTs can be serialized
0 commit comments