@@ -89,3 +89,81 @@ describe('should sign and build from serialized', () => {
8989 should . equal ( signedTx . toBroadcastFormat ( ) , testData . SEND_TX_AMOUNT_ZERO_BROADCAST ) ;
9090 } ) ;
9191} ) ;
92+
93+ /**
94+ * Regression tests for WCN-560: ETC TransactionBuilder.transfer() must pass isFirstSigner to the
95+ * TransferBuilder so that from(txHex, true) correctly decodes first-signer calldata, and
96+ * getHalfSignedTxByFirstSigner produces a valid second-signer half-signed tx (instead of treating
97+ * the ABI string-offset 0xC0 as the recipient address).
98+ */
99+ describe ( 'ETC first-signer round-trip (WCN-560 regression)' , ( ) => {
100+ const contractAddress = '0x7073b82be1d932c70afe505e1fe211916e978c34' ;
101+ const recipient = testData . ACCOUNT_2 ; // '0x33ffaefff29455fbcb1f7ddabb6ef48f4dd87536'
102+ const amount = '1000000000' ;
103+ const expireTime = 1590066728 ;
104+ const sequenceId = 5 ;
105+ const key = testData . KEYPAIR_PRV . getKeys ( ) . prv as string ;
106+
107+ /** Build an unsigned first-signer tx with known parameters and return its hex. */
108+ async function buildFirstSignerTxHex ( ) : Promise < string > {
109+ const txBuilder = getBuilder ( 'tetc' ) as TransactionBuilder ;
110+ txBuilder . fee ( { fee : '1000000000' , gasLimit : '12100000' } ) ;
111+ txBuilder . counter ( 2 ) ;
112+ txBuilder . type ( TransactionType . Send ) ;
113+ txBuilder . contract ( contractAddress ) ;
114+ const transfer = txBuilder . transfer ( ) as any ;
115+ transfer . amount ( amount ) . to ( recipient ) . expirationTime ( expireTime ) . contractSequenceId ( sequenceId ) . key ( key ) ;
116+ transfer . isFirstSigner ( true ) ;
117+ const tx = await txBuilder . build ( ) ;
118+ return tx . toBroadcastFormat ( ) ;
119+ }
120+
121+ it ( 'from(firstSignerTxHex, true) decodes recipient and amount correctly (not 0xC0)' , async ( ) => {
122+ const firstSignerTxHex = await buildFirstSignerTxHex ( ) ;
123+
124+ // This was broken before the fix: ETC's transfer() dropped isFirstSigner, so the
125+ // TransferBuilder decoded the first-signer ABI with second-signer offsets, producing
126+ // address=0x...00c0 (the ABI dynamic-string offset) instead of the real recipient.
127+ const txBuilder = getBuilder ( 'tetc' ) as TransactionBuilder ;
128+ txBuilder . from ( firstSignerTxHex , true ) ;
129+ const tx = await txBuilder . build ( ) ;
130+
131+ should . equal ( tx . outputs . length , 1 ) ;
132+ should . equal ( tx . outputs [ 0 ] . address . toLowerCase ( ) , recipient . toLowerCase ( ) ) ;
133+ should . equal ( tx . outputs [ 0 ] . value , amount ) ;
134+
135+ // Explicitly assert the old-bug value is absent
136+ should . notEqual ( tx . outputs [ 0 ] . address . toLowerCase ( ) , '0x00000000000000000000000000000000000000c0' ) ;
137+ } ) ;
138+
139+ it ( 'full round-trip: first-signer → add inner sig → second-signer half-signed → verify recipient' , async ( ) => {
140+ const firstSignerTxHex = await buildFirstSignerTxHex ( ) ;
141+
142+ // Simulate getHalfSignedTxByFirstSigner:
143+ // 1. Parse the first-signer tx (Trust HSM input)
144+ // 2. Inject the operationHashSig returned by Trust
145+ // 3. Switch to second-signer encoding (removes the method-id prefix string)
146+ const txBuilder = getBuilder ( 'tetc' ) as TransactionBuilder ;
147+ txBuilder . from ( firstSignerTxHex , true ) ;
148+ const transfer = txBuilder . transfer ( ) as any ;
149+ const mockOperationHashSig = '0x' + '1b' . repeat ( 65 ) ; // 65-byte sig from Trust HSM
150+ transfer . setSignature ( mockOperationHashSig ) ;
151+ transfer . isFirstSigner ( false ) ;
152+ const halfSignedTx = await txBuilder . build ( ) ;
153+ const halfSignedTxHex = halfSignedTx . toBroadcastFormat ( ) ;
154+
155+ // Direct calldata decode must give the correct recipient and amount
156+ const { to, amount : decodedAmount } = decodeTransferData ( halfSignedTx . toJson ( ) . data ) ;
157+ should . equal ( to . toLowerCase ( ) , recipient . toLowerCase ( ) ) ;
158+ should . equal ( decodedAmount , amount ) ;
159+ // Guard against the old bug: 0xC0 is the ABI string offset, NOT a valid recipient
160+ should . notEqual ( to . toLowerCase ( ) , '0x00000000000000000000000000000000000000c0' ) ;
161+
162+ // Re-parsing the half-signed tx as second-signer must also yield correct outputs
163+ const verifyBuilder = getBuilder ( 'tetc' ) as TransactionBuilder ;
164+ verifyBuilder . from ( halfSignedTxHex ) ;
165+ const verifiedTx = await verifyBuilder . build ( ) ;
166+ should . equal ( verifiedTx . outputs [ 0 ] . address . toLowerCase ( ) , recipient . toLowerCase ( ) ) ;
167+ should . equal ( verifiedTx . outputs [ 0 ] . value , amount ) ;
168+ } ) ;
169+ } ) ;
0 commit comments