Skip to content

Commit 408199c

Browse files
Merge pull request #8360 from BitGo/CGARD-183
feat(sdk-coin-avaxc): enable token batch
2 parents ca7f269 + 3c360d4 commit 408199c

3 files changed

Lines changed: 229 additions & 7 deletions

File tree

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

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -215,14 +215,31 @@ export class AvaxC extends AbstractEthLikeNewCoins {
215215
await this.validateHopPrebuild(wallet, txPrebuild.hopTransaction, { recipients });
216216
} else if (txParams.recipients.length > 1) {
217217
// Check total amount for batch transaction
218-
let expectedTotalAmount = new BigNumber(0);
219-
for (let i = 0; i < txParams.recipients.length; i++) {
220-
expectedTotalAmount = expectedTotalAmount.plus(txParams.recipients[i].amount);
218+
if (txParams.tokenName) {
219+
const expectedTotalAmount = new BigNumber(0);
220+
if (!expectedTotalAmount.isEqualTo(txPrebuild.recipients[0].amount)) {
221+
throw new Error('batch token transaction amount in txPrebuild should be zero for token transfers');
222+
}
223+
} else {
224+
let expectedTotalAmount = new BigNumber(0);
225+
for (let i = 0; i < txParams.recipients.length; i++) {
226+
expectedTotalAmount = expectedTotalAmount.plus(txParams.recipients[i].amount);
227+
}
228+
if (!expectedTotalAmount.isEqualTo(txPrebuild.recipients[0].amount)) {
229+
throw new Error(
230+
'batch transaction amount in txPrebuild received from BitGo servers does not match txParams supplied by client'
231+
);
232+
}
221233
}
222-
if (!expectedTotalAmount.isEqualTo(txPrebuild.recipients[0].amount)) {
223-
throw new Error(
224-
'batch transaction amount in txPrebuild received from BitGo servers does not match txParams supplied by client'
225-
);
234+
235+
// Check batch transaction is sent to the batcher contract address for the chain
236+
const network = this.getNetwork();
237+
const batcherContractAddress = network?.batcherContractAddress as string;
238+
if (
239+
!batcherContractAddress ||
240+
batcherContractAddress.toLowerCase() !== txPrebuild.recipients[0].address.toLowerCase()
241+
) {
242+
throw new Error('recipient address of txPrebuild does not match batcher address');
226243
}
227244
} else {
228245
// Check recipient address and amount for normal transaction
@@ -1045,6 +1062,9 @@ export class AvaxC extends AbstractEthLikeNewCoins {
10451062
expireTime: params.txPrebuild.expireTime,
10461063
hopTransaction: params.txPrebuild.hopTransaction,
10471064
custodianTransactionId: params.custodianTransactionId,
1065+
contractSequenceId: params.txPrebuild.nextContractSequenceId as number,
1066+
sequenceId: params.sequenceId,
1067+
...(params.txPrebuild.isBatch ? { isBatch: params.txPrebuild.isBatch } : {}),
10481068
};
10491069

10501070
return { halfSigned: txParams };

modules/sdk-coin-avaxc/src/iface.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export interface AvaxcTransactionParams extends TransactionParams {
3333
gasLimit?: number;
3434
hopParams?: HopParams;
3535
hop?: boolean;
36+
tokenName?: string;
3637
}
3738

3839
export interface VerifyAvaxcTransactionOptions extends VerifyTransactionOptions {
@@ -150,6 +151,7 @@ export interface TxPreBuild extends BaseTransactionPrebuild {
150151
expireTime?: number;
151152
hopTransaction?: string;
152153
eip1559?: EIP1559;
154+
isBatch?: boolean;
153155
recipients?: Recipient[];
154156
txPrebuild?: {
155157
halfSigned: {

modules/sdk-coin-avaxc/test/unit/avaxc.ts

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { TavaxP } from '@bitgo/sdk-coin-avaxp';
1414
import { decodeTransaction, parseTransaction, walletSimpleABI } from './helpers';
1515
import * as sinon from 'sinon';
1616
import { BN } from 'ethereumjs-util';
17+
import { EthereumNetwork } from '@bitgo/statics';
1718

1819
nock.enableNetConnect();
1920

@@ -375,6 +376,35 @@ describe('Avalanche C-Chain', function () {
375376
halfSignedRawTx.halfSigned.recipients[0].amount.should.equals(customRecipients[0].amount);
376377
halfSignedRawTx.halfSigned.recipients[0].data.should.equals(customRecipients[0].data);
377378
});
379+
380+
it('should include isBatch, contractSequenceId, and sequenceId in half-signed txParams for batch transactions', async function () {
381+
const builder = getBuilder('tavaxc') as TransactionBuilder;
382+
builder.fee({
383+
fee: '280000000000',
384+
gasLimit: '7000000',
385+
});
386+
builder.counter(1);
387+
builder.type(TransactionType.Send);
388+
builder.contract(account_1.address);
389+
builder.transfer().amount('1').to(account_2.address).expirationTime(10000).contractSequenceId(1);
390+
391+
const unsignedTx = await builder.build();
392+
const unsignedTxForBroadcasting = unsignedTx.toBroadcastFormat();
393+
394+
const halfSignedRawTx = await tavaxCoin.signTransaction({
395+
txPrebuild: {
396+
txHex: unsignedTxForBroadcasting,
397+
isBatch: true,
398+
nextContractSequenceId: 42,
399+
},
400+
prv: account_1.owner_2,
401+
sequenceId: '7',
402+
});
403+
404+
halfSignedRawTx.halfSigned.isBatch.should.equal(true);
405+
halfSignedRawTx.halfSigned.contractSequenceId.should.equal(42);
406+
halfSignedRawTx.halfSigned.sequenceId.should.equal('7');
407+
});
378408
});
379409

380410
describe('Transaction Verification', () => {
@@ -783,6 +813,176 @@ describe('Avalanche C-Chain', function () {
783813
.verifyTransaction({ txParams, txPrebuild, wallet, verification })
784814
.should.be.rejectedWith('coin in txPrebuild did not match that in txParams supplied by client');
785815
});
816+
817+
describe('Batch transaction verification', () => {
818+
let batcherContractAddress: string;
819+
820+
beforeEach(function () {
821+
batcherContractAddress = (tavaxCoin.staticsCoin?.network as EthereumNetwork)?.batcherContractAddress as string;
822+
});
823+
it('should verify a native coin batch transaction with matching total amount', async function () {
824+
const wallet = new Wallet(bitgo, tavaxCoin, {});
825+
826+
const txParams = {
827+
recipients: [
828+
{ amount: '1000000000000', address: address1 },
829+
{ amount: '2500000000000', address: address2 },
830+
],
831+
wallet: wallet,
832+
walletPassphrase: 'fakeWalletPassphrase',
833+
};
834+
835+
const txPrebuild = {
836+
recipients: [{ amount: '3500000000000', address: batcherContractAddress }],
837+
nextContractSequenceId: 0,
838+
gasPrice: 20000000000,
839+
gasLimit: 500000,
840+
isBatch: true,
841+
coin: 'tavaxc',
842+
walletId: 'fakeWalletId',
843+
walletContractAddress: 'fakeWalletContractAddress',
844+
};
845+
846+
const verification = {};
847+
848+
const isTransactionVerified = await tavaxCoin.verifyTransaction({
849+
txParams,
850+
txPrebuild,
851+
wallet,
852+
verification,
853+
});
854+
isTransactionVerified.should.equal(true);
855+
});
856+
857+
it('should verify a token batch transaction with zero native amount', async function () {
858+
const wallet = new Wallet(bitgo, tavaxCoin, {});
859+
860+
const txParams = {
861+
recipients: [
862+
{ amount: '1000000000000', address: address1 },
863+
{ amount: '2500000000000', address: address2 },
864+
],
865+
wallet: wallet,
866+
walletPassphrase: 'fakeWalletPassphrase',
867+
tokenName: 'tavaxc:USDC',
868+
};
869+
870+
const txPrebuild = {
871+
recipients: [{ amount: '0', address: batcherContractAddress }],
872+
nextContractSequenceId: 0,
873+
gasPrice: 20000000000,
874+
gasLimit: 500000,
875+
isBatch: true,
876+
coin: 'tavaxc',
877+
walletId: 'fakeWalletId',
878+
walletContractAddress: 'fakeWalletContractAddress',
879+
};
880+
881+
const verification = {};
882+
883+
const isTransactionVerified = await tavaxCoin.verifyTransaction({
884+
txParams,
885+
txPrebuild,
886+
wallet,
887+
verification,
888+
});
889+
isTransactionVerified.should.equal(true);
890+
});
891+
892+
it('should reject a token batch transaction with non-zero native amount', async function () {
893+
const wallet = new Wallet(bitgo, tavaxCoin, {});
894+
895+
const txParams = {
896+
recipients: [
897+
{ amount: '1000000000000', address: address1 },
898+
{ amount: '2500000000000', address: address2 },
899+
],
900+
wallet: wallet,
901+
walletPassphrase: 'fakeWalletPassphrase',
902+
tokenName: 'tavaxc:USDC',
903+
};
904+
905+
const txPrebuild = {
906+
recipients: [{ amount: '1000000000000', address: batcherContractAddress }],
907+
nextContractSequenceId: 0,
908+
gasPrice: 20000000000,
909+
gasLimit: 500000,
910+
isBatch: true,
911+
coin: 'tavaxc',
912+
walletId: 'fakeWalletId',
913+
walletContractAddress: 'fakeWalletContractAddress',
914+
};
915+
916+
const verification = {};
917+
918+
await tavaxCoin
919+
.verifyTransaction({ txParams, txPrebuild, wallet, verification })
920+
.should.be.rejectedWith('batch token transaction amount in txPrebuild should be zero for token transfers');
921+
});
922+
923+
it('should reject a native coin batch transaction with mismatched total amount', async function () {
924+
const wallet = new Wallet(bitgo, tavaxCoin, {});
925+
926+
const txParams = {
927+
recipients: [
928+
{ amount: '1000000000000', address: address1 },
929+
{ amount: '2500000000000', address: address2 },
930+
],
931+
wallet: wallet,
932+
walletPassphrase: 'fakeWalletPassphrase',
933+
};
934+
935+
const txPrebuild = {
936+
recipients: [{ amount: '9999999999999', address: batcherContractAddress }],
937+
nextContractSequenceId: 0,
938+
gasPrice: 20000000000,
939+
gasLimit: 500000,
940+
isBatch: true,
941+
coin: 'tavaxc',
942+
walletId: 'fakeWalletId',
943+
walletContractAddress: 'fakeWalletContractAddress',
944+
};
945+
946+
const verification = {};
947+
948+
await tavaxCoin
949+
.verifyTransaction({ txParams, txPrebuild, wallet, verification })
950+
.should.be.rejectedWith(
951+
'batch transaction amount in txPrebuild received from BitGo servers does not match txParams supplied by client'
952+
);
953+
});
954+
955+
it('should reject a batch transaction sent to wrong batcher contract address', async function () {
956+
const wallet = new Wallet(bitgo, tavaxCoin, {});
957+
const wrongBatcherAddress = '0x0000000000000000000000000000000000000001';
958+
959+
const txParams = {
960+
recipients: [
961+
{ amount: '1000000000000', address: address1 },
962+
{ amount: '2500000000000', address: address2 },
963+
],
964+
wallet: wallet,
965+
walletPassphrase: 'fakeWalletPassphrase',
966+
};
967+
968+
const txPrebuild = {
969+
recipients: [{ amount: '3500000000000', address: wrongBatcherAddress }],
970+
nextContractSequenceId: 0,
971+
gasPrice: 20000000000,
972+
gasLimit: 500000,
973+
isBatch: true,
974+
coin: 'tavaxc',
975+
walletId: 'fakeWalletId',
976+
walletContractAddress: 'fakeWalletContractAddress',
977+
};
978+
979+
const verification = {};
980+
981+
await tavaxCoin
982+
.verifyTransaction({ txParams, txPrebuild, wallet, verification })
983+
.should.be.rejectedWith('recipient address of txPrebuild does not match batcher address');
984+
});
985+
});
786986
});
787987

788988
describe('Hop Transaction Parameters', () => {

0 commit comments

Comments
 (0)