1111 */
1212import { describe , it } from "mocha" ;
1313import * as assert from "assert" ;
14- import * as utxolib from "@bitgo/utxo-lib" ;
1514import { BitGoPsbt , type HydrationUnspent } from "../../js/fixedScriptWallet/BitGoPsbt.js" ;
1615import { ZcashBitGoPsbt } from "../../js/fixedScriptWallet/ZcashBitGoPsbt.js" ;
16+ import { supportsScriptType } from "../../js/fixedScriptWallet/index.js" ;
1717import { ChainCode } from "../../js/fixedScriptWallet/chains.js" ;
1818import { ECPair } from "../../js/ecpair.js" ;
19- import { Transaction } from "../../js/transaction.js" ;
19+ import { Transaction , ZcashTransaction } from "../../js/transaction.js" ;
20+ import { coinNames , type CoinName , isMainnet } from "../../js/coinName.js" ;
2021import { getDefaultWalletKeys , getKeyTriple } from "../../js/testutils/keys.js" ;
21- import { getCoinNameForNetwork } from "../networks.js" ;
2222
2323const ZCASH_NU5_HEIGHT = 1687105 ;
2424
2525const p2msScriptTypes = [ "p2sh" , "p2shP2wsh" , "p2wsh" ] as const ;
2626
27- function isSupportedNetwork ( n : utxolib . Network ) : boolean {
28- return utxolib . isMainnet ( n ) && n !== utxolib . networks . bitcoinsv && n !== utxolib . networks . ecash ;
27+ // Coins excluded from round-trip tests (use special handling or not supported)
28+ const EXCLUDED_COINS : CoinName [ ] = [ "bsv" , "bcha" , "zec" ] ;
29+
30+ function isSupportedCoin ( coin : CoinName ) : boolean {
31+ return isMainnet ( coin ) && ! EXCLUDED_COINS . includes ( coin ) ;
2932}
3033
3134function createHalfSignedP2msPsbt (
32- network : utxolib . Network ,
35+ coinName : CoinName ,
3336 valueOverride ?: bigint ,
3437) : { psbt : BitGoPsbt ; unspents : HydrationUnspent [ ] } {
35- const coinName = getCoinNameForNetwork ( network ) ;
3638 const rootWalletKeys = getDefaultWalletKeys ( ) ;
3739 const [ userXprv ] = getKeyTriple ( "default" ) ;
3840
3941 const supportedTypes = p2msScriptTypes . filter ( ( scriptType ) =>
40- utxolib . bitgo . outputScripts . isSupportedScriptType ( network , scriptType ) ,
42+ supportsScriptType ( coinName , scriptType ) ,
4143 ) ;
4244
43- const isZcash = utxolib . getMainnet ( network ) === utxolib . networks . zcash ;
45+ const isZcash = coinName === "zec" || coinName === "tzec" ;
4446 const psbt = isZcash
4547 ? ZcashBitGoPsbt . createEmpty ( coinName as "zec" | "tzec" , rootWalletKeys , {
4648 version : 4 ,
@@ -79,11 +81,12 @@ describe("BitGoPsbt.fromHalfSignedLegacyTransaction", function () {
7981 // because BigInt::from(value_js).as_f64() calls JsValue::as_f64(), which
8082 // returns None for JS BigInt (it only works for JS Number).
8183 const rootWalletKeys = getDefaultWalletKeys ( ) ;
82- const { psbt, unspents } = createHalfSignedP2msPsbt ( utxolib . networks . bitcoin ) ;
84+ const { psbt, unspents } = createHalfSignedP2msPsbt ( "btc" ) ;
8385 const txBytes = psbt . getHalfSignedLegacyFormat ( ) ;
86+ const tx = Transaction . fromBytes ( txBytes , "btc" ) ;
8487
8588 assert . doesNotThrow ( ( ) => {
86- BitGoPsbt . fromHalfSignedLegacyTransaction ( txBytes , "btc" , rootWalletKeys , unspents ) ;
89+ BitGoPsbt . fromHalfSignedLegacyTransaction ( tx , "btc" , rootWalletKeys , unspents ) ;
8790 } , "fromHalfSignedLegacyTransaction must not throw for valid JS BigInt values" ) ;
8891 } ) ;
8992
@@ -93,33 +96,29 @@ describe("BitGoPsbt.fromHalfSignedLegacyTransaction", function () {
9396 const rootWalletKeys = getDefaultWalletKeys ( ) ;
9497 // 21 million BTC in satoshis — the maximum possible UTXO value
9598 const maxSats = 21_000_000n * 100_000_000n ;
96- const { psbt, unspents } = createHalfSignedP2msPsbt ( utxolib . networks . bitcoin , maxSats ) ;
99+ const { psbt, unspents } = createHalfSignedP2msPsbt ( "btc" , maxSats ) ;
97100 const txBytes = psbt . getHalfSignedLegacyFormat ( ) ;
101+ const tx = Transaction . fromBytes ( txBytes , "btc" ) ;
98102
99103 assert . doesNotThrow ( ( ) => {
100- BitGoPsbt . fromHalfSignedLegacyTransaction ( txBytes , "btc" , rootWalletKeys , unspents ) ;
104+ BitGoPsbt . fromHalfSignedLegacyTransaction ( tx , "btc" , rootWalletKeys , unspents ) ;
101105 } , "fromHalfSignedLegacyTransaction must handle large satoshi values" ) ;
102106 } ) ;
103107 } ) ;
104108
105109 describe ( "Round-trip: getHalfSignedLegacyFormat → fromHalfSignedLegacyTransaction" , function ( ) {
106- // Zcash uses a non-standard transaction format (version 4 overwintered) that
107- // fromHalfSignedLegacyTransaction does not support; skip it here.
108- const roundTripNetworks = utxolib
109- . getNetworkList ( )
110- . filter ( isSupportedNetwork )
111- . filter ( ( n ) => utxolib . getMainnet ( n ) !== utxolib . networks . zcash ) ;
112-
113- for ( const network of roundTripNetworks ) {
114- const networkName = utxolib . getNetworkName ( network ) ;
115- it ( `${ networkName } : reconstructed PSBT serializes without error` , function ( ) {
110+ // Supported coins for round-trip: all mainnet UTXO coins except special formats
111+ const roundTripCoins = coinNames . filter ( isSupportedCoin ) ;
112+
113+ for ( const coinName of roundTripCoins ) {
114+ it ( `${ coinName } : reconstructed PSBT serializes without error` , function ( ) {
116115 const rootWalletKeys = getDefaultWalletKeys ( ) ;
117- const coinName = getCoinNameForNetwork ( network ) ;
118- const { psbt, unspents } = createHalfSignedP2msPsbt ( network ) ;
116+ const { psbt, unspents } = createHalfSignedP2msPsbt ( coinName ) ;
119117 const txBytes = psbt . getHalfSignedLegacyFormat ( ) ;
118+ const tx = Transaction . fromBytes ( txBytes , coinName ) ;
120119
121120 const reconstructed = BitGoPsbt . fromHalfSignedLegacyTransaction (
122- txBytes ,
121+ tx ,
123122 coinName ,
124123 rootWalletKeys ,
125124 unspents ,
@@ -152,13 +151,14 @@ describe("BitGoPsbt.fromHalfSignedLegacyTransaction", function () {
152151 psbt . sign ( userXprv ) ;
153152
154153 const txBytes = psbt . getHalfSignedLegacyFormat ( ) ;
154+ const tx = Transaction . fromBytes ( txBytes , "btc" ) ;
155155
156156 const unspents : HydrationUnspent [ ] = [
157157 { chain : 0 , index : 0 , value : BigInt ( 10000 ) } , // wallet
158158 { pubkey : ecpair . publicKey , value : BigInt ( 1000 ) } , // replay protection
159159 ] ;
160160 const reconstructed = BitGoPsbt . fromHalfSignedLegacyTransaction (
161- txBytes ,
161+ tx ,
162162 "btc" ,
163163 rootWalletKeys ,
164164 unspents ,
@@ -174,21 +174,25 @@ describe("BitGoPsbt.fromHalfSignedLegacyTransaction", function () {
174174 } ) ;
175175
176176 describe ( "Zcash legacy format round-trip" , function ( ) {
177- it ( "should reject Zcash via dynamic dispatch in fromHalfSignedLegacyTransaction" , function ( ) {
178- // With dynamic dispatch, fromHalfSignedLegacyTransaction now validates the transaction type
179- // and rejects Zcash early with a clear error message, directing to ZcashBitGoPsbt.createEmpty() .
177+ it ( "should reject Zcash via type check in fromHalfSignedLegacyTransaction" , function ( ) {
178+ // fromHalfSignedLegacyTransaction validates the transaction type at call time
179+ // and rejects Zcash with a clear error message.
180180 const rootWalletKeys = getDefaultWalletKeys ( ) ;
181- const { psbt : zcashPsbt , unspents } = createHalfSignedP2msPsbt ( utxolib . networks . zcash ) ;
181+ const { psbt : zcashPsbt , unspents } = createHalfSignedP2msPsbt ( "zec" ) ;
182182
183183 // Step 1: Extract Zcash PSBT as legacy format
184184 const txBytes = zcashPsbt . getHalfSignedLegacyFormat ( ) ;
185185 assert . ok ( txBytes . length > 0 , "ZcashBitGoPsbt.getHalfSignedLegacyFormat() produces bytes" ) ;
186186
187- // Step 2: Call fromHalfSignedLegacyTransaction with Zcash bytes
188- // Expected: Throws clear error after detecting Zcash transaction via dynamic dispatch
187+ // Step 2: Parse the transaction (will be ZcashTransaction)
188+ const tx = Transaction . fromBytes ( txBytes , "zec" ) ;
189+ assert . ok ( tx instanceof ZcashTransaction , "Parsed transaction is ZcashTransaction" ) ;
190+
191+ // Step 3: Call fromHalfSignedLegacyTransaction with Zcash transaction
192+ // Expected: Throws clear error after detecting Zcash transaction
189193 assert . throws (
190194 ( ) => {
191- BitGoPsbt . fromHalfSignedLegacyTransaction ( txBytes , "zec" , rootWalletKeys , unspents ) ;
195+ BitGoPsbt . fromHalfSignedLegacyTransaction ( tx , "zec" , rootWalletKeys , unspents ) ;
192196 } ,
193197 / U s e Z c a s h B i t G o P s b t .f r o m H a l f S i g n e d L e g a c y T r a n s a c t i o n \( \) f o r Z c a s h t r a n s a c t i o n s / ,
194198 ) ;
@@ -197,7 +201,7 @@ describe("BitGoPsbt.fromHalfSignedLegacyTransaction", function () {
197201 it ( "should round-trip Zcash PSBT via ZcashBitGoPsbt.fromHalfSignedLegacyTransaction (with blockHeight)" , function ( ) {
198202 // This test verifies the round-trip: create Zcash PSBT → extract legacy format → reconstruct PSBT
199203 const rootWalletKeys = getDefaultWalletKeys ( ) ;
200- const { psbt, unspents } = createHalfSignedP2msPsbt ( utxolib . networks . zcash ) ;
204+ const { psbt, unspents } = createHalfSignedP2msPsbt ( "zec" ) ;
201205
202206 // Step 1: Extract half-signed legacy format (this is what would be transmitted)
203207 const legacyBytes = psbt . getHalfSignedLegacyFormat ( ) ;
@@ -227,7 +231,7 @@ describe("BitGoPsbt.fromHalfSignedLegacyTransaction", function () {
227231 it ( "should round-trip Zcash PSBT via ZcashBitGoPsbt.fromHalfSignedLegacyTransaction (with consensusBranchId)" , function ( ) {
228232 // This test verifies the round-trip with explicit consensus branch ID instead of block height
229233 const rootWalletKeys = getDefaultWalletKeys ( ) ;
230- const { psbt, unspents } = createHalfSignedP2msPsbt ( utxolib . networks . zcash ) ;
234+ const { psbt, unspents } = createHalfSignedP2msPsbt ( "zec" ) ;
231235
232236 // Step 1: Extract half-signed legacy format
233237 const legacyBytes = psbt . getHalfSignedLegacyFormat ( ) ;
@@ -254,26 +258,22 @@ describe("BitGoPsbt.fromHalfSignedLegacyTransaction", function () {
254258 assert . ok ( serialized . length > 0 , "Reconstructed Zcash PSBT serializes without error" ) ;
255259 } ) ;
256260
257- it ( "should accept pre-decoded transaction instance to avoid re-parsing " , function ( ) {
258- // Dynamic dispatch enhancement: fromHalfSignedLegacyTransaction now accepts
259- // either txBytes OR a pre-decoded ITransaction instance
261+ it ( "should accept pre-decoded transaction instance" , function ( ) {
262+ // fromHalfSignedLegacyTransaction accepts a pre-decoded Transaction instance.
263+ // This is more efficient than parsing bytes twice.
260264 const rootWalletKeys = getDefaultWalletKeys ( ) ;
261- const { psbt, unspents } = createHalfSignedP2msPsbt ( utxolib . networks . bitcoin ) ;
265+ const { psbt, unspents } = createHalfSignedP2msPsbt ( "btc" ) ;
262266 const txBytes = psbt . getHalfSignedLegacyFormat ( ) ;
263267
264- // Method 1: Pass raw bytes (uses dynamic dispatch internally)
265- const psbt1 = BitGoPsbt . fromHalfSignedLegacyTransaction (
266- txBytes ,
267- "btc" ,
268- rootWalletKeys ,
269- unspents ,
270- ) ;
268+ // Parse transaction once and pass the instance
269+ const tx = Transaction . fromBytes ( txBytes , "btc" ) ;
270+ const psbt1 = BitGoPsbt . fromHalfSignedLegacyTransaction ( tx , "btc" , rootWalletKeys , unspents ) ;
271271
272- // Method 2: Pre-decode transaction and pass instance (avoids re-parsing)
273- const tx = Transaction . fromBytes ( txBytes ) ;
274- const psbt2 = BitGoPsbt . fromHalfSignedLegacyTransaction ( tx , "btc" , rootWalletKeys , unspents ) ;
272+ // Parse again to compare
273+ const tx2 = Transaction . fromBytes ( txBytes , "btc" ) ;
274+ const psbt2 = BitGoPsbt . fromHalfSignedLegacyTransaction ( tx2 , "btc" , rootWalletKeys , unspents ) ;
275275
276- // Both methods should produce equivalent results
276+ // Both should produce equivalent results
277277 assert . strictEqual ( psbt1 . inputCount ( ) , psbt2 . inputCount ( ) , "Same input count" ) ;
278278 assert . strictEqual ( psbt1 . outputCount ( ) , psbt2 . outputCount ( ) , "Same output count" ) ;
279279 assert . deepStrictEqual ( psbt1 . serialize ( ) , psbt2 . serialize ( ) , "Identical serialization" ) ;
0 commit comments