Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion modules/sdk-coin-xdc/src/xdcToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
*/
import { EthLikeTokenConfig, coins } from '@bitgo/statics';
import { BitGoBase, CoinConstructor, NamedCoinConstructor, common, MPCAlgorithm } from '@bitgo/sdk-core';
import { CoinNames, EthLikeToken, recoveryBlockchainExplorerQuery } from '@bitgo/abstract-eth';
import {
CoinNames,
EthLikeToken,
recoveryBlockchainExplorerQuery,
VerifyEthTransactionOptions,
} from '@bitgo/abstract-eth';

import { TransactionBuilder } from './lib';
export { EthLikeTokenConfig };
Expand Down Expand Up @@ -52,4 +57,34 @@ export class XdcToken extends EthLikeToken {
getMPCAlgorithm(): MPCAlgorithm {
return 'ecdsa';
}

/**
* Verify if a tss transaction is valid
*
* @param {VerifyEthTransactionOptions} params
* @param {TransactionParams} params.txParams - params object passed to send
* @param {TransactionPrebuild} params.txPrebuild - prebuild object returned by server
* @param {Wallet} params.wallet - Wallet object to obtain keys to verify against
* @returns {boolean}
*/
async verifyTssTransaction(params: VerifyEthTransactionOptions): Promise<boolean> {
const { txParams, txPrebuild, wallet } = params;
if (
!txParams?.recipients &&
!(
txParams.prebuildTx?.consolidateId ||
(txParams.type && ['acceleration', 'fillNonce', 'transferToken'].includes(txParams.type))
)
) {
throw new Error(`missing txParams`);
}
if (!wallet || !txPrebuild) {
throw new Error(`missing params`);
}
if (txParams.hop && txParams.recipients && txParams.recipients.length > 1) {
throw new Error(`tx cannot be both a batch and hop transaction`);
}

return true;
}
}
28 changes: 28 additions & 0 deletions modules/sdk-coin-xdc/test/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,34 @@ const getBalanceResponseNonBitGoRecovery: Record<string, unknown> = {
message: 'OK',
};

// Mock data for txdc:tmt token transfer TSS transaction
export const mockTokenTransferData = {
txRequestId: '2475368d-f604-46e3-a743-e32f663fa350',
walletId: '695e1ca4fb4a739c8c6f9b49120c55c7',
serializedTxHex:
'f86a0485045d964b8083061a8094b283ec8dad644effc5c4c50bb7bb21442ac3c2db80b844a9059cbb000000000000000000000000421cdf5e890070c28db0fd8e4bf87deac0cd0ffc00000000000000000000000000000000000000000000000000000000000f4240808080',
signableHex:
'f86a0485045d964b8083061a8094b283ec8dad644effc5c4c50bb7bb21442ac3c2db80b844a9059cbb000000000000000000000000421cdf5e890070c28db0fd8e4bf87deac0cd0ffc00000000000000000000000000000000000000000000000000000000000f4240338080',
tokenContractAddress: '0xb283ec8dad644effc5c4c50bb7bb21442ac3c2db',
recipientAddress: '0x421cdf5e890070c28db0fd8e4bf87deac0cd0ffc',
senderAddress: '0x6aafaddf545f96772140f0008190c176a065df9a',
tokenAmount: '1000000',
feeInfo: {
fee: 7500000000000000,
feeString: '7500000000000000',
},
txPrebuild: {
txHex:
'f86a0485045d964b8083061a8094b283ec8dad644effc5c4c50bb7bb21442ac3c2db80b844a9059cbb000000000000000000000000421cdf5e890070c28db0fd8e4bf87deac0cd0ffc00000000000000000000000000000000000000000000000000000000000f4240808080',
recipients: [
{
address: '0x421cdf5e890070c28db0fd8e4bf87deac0cd0ffc',
amount: '1000000',
},
],
},
};

export const mockDataNonBitGoRecovery = {
recoveryDestination: '0xd76b586901850f2c656db0cbef795c0851bbec35',
userKeyData:
Expand Down
244 changes: 243 additions & 1 deletion modules/sdk-coin-xdc/test/unit/xdcToken.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import 'should';
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
import { BitGoAPI } from '@bitgo/sdk-api';
import { IWallet } from '@bitgo/sdk-core';

import { register } from '../../src';
import { register, XdcToken } from '../../src';
import { mockTokenTransferData } from '../resources';

describe('XDC Token:', function () {
let bitgo: TestBitGoAPI;
Expand All @@ -27,4 +29,244 @@ describe('XDC Token:', function () {
xdcTokenCoin.network.should.equal('Mainnet');
xdcTokenCoin.decimalPlaces.should.equal(6);
});

describe('Token Registration and TransactionBuilder', function () {
const mainnetTokens = ['xdc:usdc', 'xdc:lbt', 'xdc:gama', 'xdc:srx', 'xdc:weth'];
const testnetTokens = ['txdc:tmt'];

describe('Mainnet tokens', function () {
mainnetTokens.forEach((tokenName) => {
it(`${tokenName} should be registered as XdcToken`, function () {
const token = bitgo.coin(tokenName);
token.should.be.instanceOf(XdcToken);
});

it(`${tokenName} should create TransactionBuilder without error`, function () {
const token = bitgo.coin(tokenName) as XdcToken;
// @ts-expect-error - accessing protected method for testing
(() => token.getTransactionBuilder()).should.not.throw();
});

it(`${tokenName} should use XDC-specific TransactionBuilder`, function () {
const token = bitgo.coin(tokenName) as XdcToken;
// @ts-expect-error - accessing protected method for testing
const builder = token.getTransactionBuilder();
builder.should.have.property('_common');
// Verify it's using XDC's getCommon, not EVM's
// XDC's TransactionBuilder should create successfully without SHARED_EVM_SDK feature
builder.constructor.name.should.equal('TransactionBuilder');
});
});
});

describe('Testnet tokens', function () {
testnetTokens.forEach((tokenName) => {
it(`${tokenName} should be registered as XdcToken`, function () {
const token = bitgo.coin(tokenName);
token.should.be.instanceOf(XdcToken);
});

it(`${tokenName} should create TransactionBuilder without error`, function () {
const token = bitgo.coin(tokenName) as XdcToken;
// @ts-expect-error - accessing protected method for testing
(() => token.getTransactionBuilder()).should.not.throw();
});

it(`${tokenName} should use XDC-specific TransactionBuilder`, function () {
const token = bitgo.coin(tokenName) as XdcToken;
// @ts-expect-error - accessing protected method for testing
const builder = token.getTransactionBuilder();
builder.should.have.property('_common');
builder.constructor.name.should.equal('TransactionBuilder');
});

it(`${tokenName} should have correct base chain`, function () {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const token: any = bitgo.coin(tokenName);
token.getBaseChain().should.equal('txdc');
});

it(`${tokenName} should not throw "Cannot use common sdk module" error`, function () {
const token = bitgo.coin(tokenName) as XdcToken;
let errorThrown = false;
let errorMessage = '';

try {
// @ts-expect-error - accessing protected method for testing
const builder = token.getTransactionBuilder();
// Try to use the builder to ensure it's fully functional
// @ts-expect-error - type expects TransactionType enum
builder.type('Send');
} catch (e) {
errorThrown = true;
errorMessage = (e as Error).message;
}

errorThrown.should.equal(false);
errorMessage.should.not.match(/Cannot use common sdk module/);
});
});
});

it('should verify all XDC tokens use XdcToken class, not EthLikeErc20Token', function () {
const allTokens = [...mainnetTokens, ...testnetTokens];

allTokens.forEach((tokenName) => {
const token = bitgo.coin(tokenName);
token.should.be.instanceOf(XdcToken);
token.constructor.name.should.equal('XdcToken');
token.constructor.name.should.not.equal('EthLikeErc20Token');
});
});
});

describe('verifyTssTransaction', function () {
it('should return true for valid token transfer params', async function () {
const token = bitgo.coin('txdc:tmt') as XdcToken;
const mockWallet = {} as unknown as IWallet;

const result = await token.verifyTssTransaction({
txParams: {
recipients: [
{
address: mockTokenTransferData.recipientAddress,
amount: mockTokenTransferData.tokenAmount,
},
],
},
txPrebuild: mockTokenTransferData.txPrebuild as unknown as Parameters<
typeof token.verifyTssTransaction
>[0]['txPrebuild'],
wallet: mockWallet,
});

result.should.equal(true);
});

it('should return true for transferToken type without recipients', async function () {
const token = bitgo.coin('txdc:tmt') as XdcToken;
const mockWallet = {} as unknown as IWallet;

const result = await token.verifyTssTransaction({
txParams: {
type: 'transferToken',
},
txPrebuild: mockTokenTransferData.txPrebuild as unknown as Parameters<
typeof token.verifyTssTransaction
>[0]['txPrebuild'],
wallet: mockWallet,
});

result.should.equal(true);
});

it('should throw error when txParams.recipients is missing and no valid type', async function () {
const token = bitgo.coin('txdc:tmt') as XdcToken;
const mockWallet = {} as unknown as IWallet;

await token
.verifyTssTransaction({
txParams: {},
txPrebuild: mockTokenTransferData.txPrebuild as unknown as Parameters<
typeof token.verifyTssTransaction
>[0]['txPrebuild'],
wallet: mockWallet,
})
.should.be.rejectedWith('missing txParams');
});

it('should throw error when wallet is missing', async function () {
const token = bitgo.coin('txdc:tmt') as XdcToken;

await token
.verifyTssTransaction({
txParams: {
recipients: [
{
address: mockTokenTransferData.recipientAddress,
amount: mockTokenTransferData.tokenAmount,
},
],
},
txPrebuild: mockTokenTransferData.txPrebuild as unknown as Parameters<
typeof token.verifyTssTransaction
>[0]['txPrebuild'],
wallet: undefined as unknown as IWallet,
})
.should.be.rejectedWith('missing params');
});

it('should throw error when txPrebuild is missing', async function () {
const token = bitgo.coin('txdc:tmt') as XdcToken;
const mockWallet = {} as unknown as IWallet;

await token
.verifyTssTransaction({
txParams: {
recipients: [
{
address: mockTokenTransferData.recipientAddress,
amount: mockTokenTransferData.tokenAmount,
},
],
},
txPrebuild: undefined as unknown as Parameters<typeof token.verifyTssTransaction>[0]['txPrebuild'],
wallet: mockWallet,
})
.should.be.rejectedWith('missing params');
});

it('should throw error for batch + hop transaction', async function () {
const token = bitgo.coin('txdc:tmt') as XdcToken;
const mockWallet = {} as unknown as IWallet;

await token
.verifyTssTransaction({
txParams: {
hop: true,
recipients: [
{ address: '0x1111111111111111111111111111111111111111', amount: '1000' },
{ address: '0x2222222222222222222222222222222222222222', amount: '2000' },
],
},
txPrebuild: mockTokenTransferData.txPrebuild as unknown as Parameters<
typeof token.verifyTssTransaction
>[0]['txPrebuild'],
wallet: mockWallet,
})
.should.be.rejectedWith('tx cannot be both a batch and hop transaction');
});

it('should not throw EIP155 error when verifying token transaction', async function () {
// This test ensures that verifyTssTransaction does NOT parse the txHex
// which would fail with "Incompatible EIP155-based V" error
const token = bitgo.coin('txdc:tmt') as XdcToken;
const mockWallet = {} as unknown as IWallet;

// Use the signableHex (with v=51) which would fail if parsed
const txPrebuildWithSignableHex = {
...mockTokenTransferData.txPrebuild,
txHex: mockTokenTransferData.signableHex,
};

// This should NOT throw EIP155 error because verifyTssTransaction
// does not parse the transaction
const result = await token.verifyTssTransaction({
txParams: {
recipients: [
{
address: mockTokenTransferData.recipientAddress,
amount: mockTokenTransferData.tokenAmount,
},
],
},
txPrebuild: txPrebuildWithSignableHex as unknown as Parameters<
typeof token.verifyTssTransaction
>[0]['txPrebuild'],
wallet: mockWallet,
});

result.should.equal(true);
});
});
});
2 changes: 1 addition & 1 deletion modules/statics/src/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2888,7 +2888,7 @@ export function xdcErc20(
decimalPlaces: number,
contractAddress: string,
asset: UnderlyingAsset,
features: CoinFeature[] = [...AccountCoin.DEFAULT_FEATURES, CoinFeature.EIP1559],
features: CoinFeature[] = AccountCoin.DEFAULT_FEATURES,
prefix = '',
suffix: string = name.toUpperCase(),
network: AccountNetwork = Networks.main.xdc,
Expand Down