Skip to content

Commit 0adbf08

Browse files
authored
fix(sdk-coin-*): pass isFirstSigner through transfer() in all EVM TransactionBuilders
2 parents 811540d + 961e195 commit 0adbf08

24 files changed

Lines changed: 124 additions & 46 deletions

File tree

modules/sdk-coin-apechain/src/lib/transactionBuilder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ export class TransactionBuilder extends AbstractTransactionBuilder {
1515
}
1616

1717
/** @inheritdoc */
18-
transfer(data?: string): TransferBuilder {
18+
transfer(data?: string, isFirstSigner?: boolean): TransferBuilder {
1919
if (this._type !== TransactionType.Send) {
2020
throw new BuildTransactionError('Transfers can only be set for send transactions');
2121
}
2222
if (!this._transfer) {
23-
this._transfer = new TransferBuilder(data);
23+
this._transfer = new TransferBuilder(data, isFirstSigner);
2424
}
2525
return this._transfer;
2626
}

modules/sdk-coin-arbeth/src/lib/transactionBuilder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ export class TransactionBuilder extends EthLikeTransactionBuilder {
1717
}
1818

1919
/** @inheritdoc */
20-
transfer(data?: string): TransferBuilder {
20+
transfer(data?: string, isFirstSigner?: boolean): TransferBuilder {
2121
if (this._type !== TransactionType.Send) {
2222
throw new BuildTransactionError('Transfers can only be set for send transactions');
2323
}
2424
if (!this._transfer) {
25-
this._transfer = new TransferBuilder(data);
25+
this._transfer = new TransferBuilder(data, isFirstSigner);
2626
}
2727
return this._transfer;
2828
}

modules/sdk-coin-avaxc/src/lib/transactionBuilder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ export class TransactionBuilder extends EthTransactionBuilder {
3030
}
3131

3232
/** @inheritdoc */
33-
transfer(data?: string): TransferBuilder {
33+
transfer(data?: string, isFirstSigner?: boolean): TransferBuilder {
3434
if (this._type !== TransactionType.Send) {
3535
throw new BuildTransactionError('Transfers can only be set for send transactions');
3636
}
3737
if (!this._transfer) {
38-
this._transfer = new TransferBuilder(data);
38+
this._transfer = new TransferBuilder(data, isFirstSigner);
3939
}
4040
return this._transfer;
4141
}

modules/sdk-coin-bera/src/lib/transactionBuilder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ export class TransactionBuilder extends EthLikeTransactionBuilder {
1515
}
1616

1717
/** @inheritdoc */
18-
transfer(data?: string): TransferBuilder {
18+
transfer(data?: string, isFirstSigner?: boolean): TransferBuilder {
1919
if (this._type !== TransactionType.Send) {
2020
throw new BuildTransactionError('Transfers can only be set for send transactions');
2121
}
2222
if (!this._transfer) {
23-
this._transfer = new TransferBuilder(data);
23+
this._transfer = new TransferBuilder(data, isFirstSigner);
2424
}
2525
return this._transfer;
2626
}

modules/sdk-coin-bsc/src/lib/transactionBuilder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ export class TransactionBuilder extends AbstractTransactionBuilder {
1414
}
1515

1616
/** @inheritdoc */
17-
transfer(data?: string): TransferBuilder {
17+
transfer(data?: string, isFirstSigner?: boolean): TransferBuilder {
1818
if (this._type !== TransactionType.Send) {
1919
throw new BuildTransactionError('Transfers can only be set for send transactions');
2020
}
2121
if (!this._transfer) {
22-
this._transfer = new TransferBuilder(data);
22+
this._transfer = new TransferBuilder(data, isFirstSigner);
2323
}
2424
return this._transfer;
2525
}

modules/sdk-coin-celo/src/lib/transactionBuilder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,12 +174,12 @@ export class TransactionBuilder extends EthTransactionBuilder {
174174
}
175175

176176
/** @inheritdoc */
177-
transfer(data?: string): TransferBuilder {
177+
transfer(data?: string, isFirstSigner?: boolean): TransferBuilder {
178178
if (this._type !== TransactionType.Send) {
179179
throw new BuildTransactionError('Transfers can only be set for send transactions');
180180
}
181181
if (!this._transfer) {
182-
this._transfer = new TransferBuilder(data);
182+
this._transfer = new TransferBuilder(data, isFirstSigner);
183183
}
184184
return this._transfer;
185185
}

modules/sdk-coin-coredao/src/lib/transactionBuilder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ export class TransactionBuilder extends AbstractTransactionBuilder {
1515
}
1616

1717
/** @inheritdoc */
18-
transfer(data?: string): TransferBuilder {
18+
transfer(data?: string, isFirstSigner?: boolean): TransferBuilder {
1919
if (this._type !== TransactionType.Send) {
2020
throw new BuildTransactionError('Transfers can only be set for send transactions');
2121
}
2222
if (!this._transfer) {
23-
this._transfer = new TransferBuilder(data);
23+
this._transfer = new TransferBuilder(data, isFirstSigner);
2424
}
2525
return this._transfer;
2626
}

modules/sdk-coin-etc/src/lib/transactionBuilder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ export class TransactionBuilder extends EthTransactionBuilder {
3030
}
3131

3232
/** @inheritdoc */
33-
transfer(data?: string): TransferBuilder {
33+
transfer(data?: string, isFirstSigner?: boolean): TransferBuilder {
3434
if (this._type !== TransactionType.Send) {
3535
throw new BuildTransactionError('Transfers can only be set for send transactions');
3636
}
3737
if (!this._transfer) {
38-
this._transfer = new TransferBuilder(data);
38+
this._transfer = new TransferBuilder(data, isFirstSigner);
3939
}
4040
return this._transfer;
4141
}

modules/sdk-coin-etc/test/unit/transactionBuilder/send.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
});

modules/sdk-coin-ethlike/src/lib/transactionBuilder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ export class EthLikeTransactionBuilder extends AbstractTransactionBuilder {
1717
}
1818

1919
/** @inheritdoc */
20-
transfer(data?: string): TransferBuilder {
20+
transfer(data?: string, isFirstSigner?: boolean): TransferBuilder {
2121
if (this._type !== TransactionType.Send) {
2222
throw new BuildTransactionError('Transfers can only be set for send transactions');
2323
}
2424
if (!this._transfer) {
25-
this._transfer = new TransferBuilder(data);
25+
this._transfer = new TransferBuilder(data, isFirstSigner);
2626
}
2727
return this._transfer;
2828
}

0 commit comments

Comments
 (0)