Skip to content

Commit c965a7d

Browse files
Merge pull request #7848 from BitGo/coin-7060-2
fix: override verify tss txn for xdc token
2 parents 4361b4d + 03d5339 commit c965a7d

4 files changed

Lines changed: 308 additions & 3 deletions

File tree

modules/sdk-coin-xdc/src/xdcToken.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
*/
44
import { EthLikeTokenConfig, coins } from '@bitgo/statics';
55
import { BitGoBase, CoinConstructor, NamedCoinConstructor, common, MPCAlgorithm } from '@bitgo/sdk-core';
6-
import { CoinNames, EthLikeToken, recoveryBlockchainExplorerQuery } from '@bitgo/abstract-eth';
6+
import {
7+
CoinNames,
8+
EthLikeToken,
9+
recoveryBlockchainExplorerQuery,
10+
VerifyEthTransactionOptions,
11+
} from '@bitgo/abstract-eth';
712

813
import { TransactionBuilder } from './lib';
914
export { EthLikeTokenConfig };
@@ -52,4 +57,34 @@ export class XdcToken extends EthLikeToken {
5257
getMPCAlgorithm(): MPCAlgorithm {
5358
return 'ecdsa';
5459
}
60+
61+
/**
62+
* Verify if a tss transaction is valid
63+
*
64+
* @param {VerifyEthTransactionOptions} params
65+
* @param {TransactionParams} params.txParams - params object passed to send
66+
* @param {TransactionPrebuild} params.txPrebuild - prebuild object returned by server
67+
* @param {Wallet} params.wallet - Wallet object to obtain keys to verify against
68+
* @returns {boolean}
69+
*/
70+
async verifyTssTransaction(params: VerifyEthTransactionOptions): Promise<boolean> {
71+
const { txParams, txPrebuild, wallet } = params;
72+
if (
73+
!txParams?.recipients &&
74+
!(
75+
txParams.prebuildTx?.consolidateId ||
76+
(txParams.type && ['acceleration', 'fillNonce', 'transferToken'].includes(txParams.type))
77+
)
78+
) {
79+
throw new Error(`missing txParams`);
80+
}
81+
if (!wallet || !txPrebuild) {
82+
throw new Error(`missing params`);
83+
}
84+
if (txParams.hop && txParams.recipients && txParams.recipients.length > 1) {
85+
throw new Error(`tx cannot be both a batch and hop transaction`);
86+
}
87+
88+
return true;
89+
}
5590
}

modules/sdk-coin-xdc/test/resources.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,34 @@ const getBalanceResponseNonBitGoRecovery: Record<string, unknown> = {
7777
message: 'OK',
7878
};
7979

80+
// Mock data for txdc:tmt token transfer TSS transaction
81+
export const mockTokenTransferData = {
82+
txRequestId: '2475368d-f604-46e3-a743-e32f663fa350',
83+
walletId: '695e1ca4fb4a739c8c6f9b49120c55c7',
84+
serializedTxHex:
85+
'f86a0485045d964b8083061a8094b283ec8dad644effc5c4c50bb7bb21442ac3c2db80b844a9059cbb000000000000000000000000421cdf5e890070c28db0fd8e4bf87deac0cd0ffc00000000000000000000000000000000000000000000000000000000000f4240808080',
86+
signableHex:
87+
'f86a0485045d964b8083061a8094b283ec8dad644effc5c4c50bb7bb21442ac3c2db80b844a9059cbb000000000000000000000000421cdf5e890070c28db0fd8e4bf87deac0cd0ffc00000000000000000000000000000000000000000000000000000000000f4240338080',
88+
tokenContractAddress: '0xb283ec8dad644effc5c4c50bb7bb21442ac3c2db',
89+
recipientAddress: '0x421cdf5e890070c28db0fd8e4bf87deac0cd0ffc',
90+
senderAddress: '0x6aafaddf545f96772140f0008190c176a065df9a',
91+
tokenAmount: '1000000',
92+
feeInfo: {
93+
fee: 7500000000000000,
94+
feeString: '7500000000000000',
95+
},
96+
txPrebuild: {
97+
txHex:
98+
'f86a0485045d964b8083061a8094b283ec8dad644effc5c4c50bb7bb21442ac3c2db80b844a9059cbb000000000000000000000000421cdf5e890070c28db0fd8e4bf87deac0cd0ffc00000000000000000000000000000000000000000000000000000000000f4240808080',
99+
recipients: [
100+
{
101+
address: '0x421cdf5e890070c28db0fd8e4bf87deac0cd0ffc',
102+
amount: '1000000',
103+
},
104+
],
105+
},
106+
};
107+
80108
export const mockDataNonBitGoRecovery = {
81109
recoveryDestination: '0xd76b586901850f2c656db0cbef795c0851bbec35',
82110
userKeyData:

modules/sdk-coin-xdc/test/unit/xdcToken.ts

Lines changed: 243 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import 'should';
22
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
33
import { BitGoAPI } from '@bitgo/sdk-api';
4+
import { IWallet } from '@bitgo/sdk-core';
45

5-
import { register } from '../../src';
6+
import { register, XdcToken } from '../../src';
7+
import { mockTokenTransferData } from '../resources';
68

79
describe('XDC Token:', function () {
810
let bitgo: TestBitGoAPI;
@@ -27,4 +29,244 @@ describe('XDC Token:', function () {
2729
xdcTokenCoin.network.should.equal('Mainnet');
2830
xdcTokenCoin.decimalPlaces.should.equal(6);
2931
});
32+
33+
describe('Token Registration and TransactionBuilder', function () {
34+
const mainnetTokens = ['xdc:usdc', 'xdc:lbt', 'xdc:gama', 'xdc:srx', 'xdc:weth'];
35+
const testnetTokens = ['txdc:tmt'];
36+
37+
describe('Mainnet tokens', function () {
38+
mainnetTokens.forEach((tokenName) => {
39+
it(`${tokenName} should be registered as XdcToken`, function () {
40+
const token = bitgo.coin(tokenName);
41+
token.should.be.instanceOf(XdcToken);
42+
});
43+
44+
it(`${tokenName} should create TransactionBuilder without error`, function () {
45+
const token = bitgo.coin(tokenName) as XdcToken;
46+
// @ts-expect-error - accessing protected method for testing
47+
(() => token.getTransactionBuilder()).should.not.throw();
48+
});
49+
50+
it(`${tokenName} should use XDC-specific TransactionBuilder`, function () {
51+
const token = bitgo.coin(tokenName) as XdcToken;
52+
// @ts-expect-error - accessing protected method for testing
53+
const builder = token.getTransactionBuilder();
54+
builder.should.have.property('_common');
55+
// Verify it's using XDC's getCommon, not EVM's
56+
// XDC's TransactionBuilder should create successfully without SHARED_EVM_SDK feature
57+
builder.constructor.name.should.equal('TransactionBuilder');
58+
});
59+
});
60+
});
61+
62+
describe('Testnet tokens', function () {
63+
testnetTokens.forEach((tokenName) => {
64+
it(`${tokenName} should be registered as XdcToken`, function () {
65+
const token = bitgo.coin(tokenName);
66+
token.should.be.instanceOf(XdcToken);
67+
});
68+
69+
it(`${tokenName} should create TransactionBuilder without error`, function () {
70+
const token = bitgo.coin(tokenName) as XdcToken;
71+
// @ts-expect-error - accessing protected method for testing
72+
(() => token.getTransactionBuilder()).should.not.throw();
73+
});
74+
75+
it(`${tokenName} should use XDC-specific TransactionBuilder`, function () {
76+
const token = bitgo.coin(tokenName) as XdcToken;
77+
// @ts-expect-error - accessing protected method for testing
78+
const builder = token.getTransactionBuilder();
79+
builder.should.have.property('_common');
80+
builder.constructor.name.should.equal('TransactionBuilder');
81+
});
82+
83+
it(`${tokenName} should have correct base chain`, function () {
84+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
85+
const token: any = bitgo.coin(tokenName);
86+
token.getBaseChain().should.equal('txdc');
87+
});
88+
89+
it(`${tokenName} should not throw "Cannot use common sdk module" error`, function () {
90+
const token = bitgo.coin(tokenName) as XdcToken;
91+
let errorThrown = false;
92+
let errorMessage = '';
93+
94+
try {
95+
// @ts-expect-error - accessing protected method for testing
96+
const builder = token.getTransactionBuilder();
97+
// Try to use the builder to ensure it's fully functional
98+
// @ts-expect-error - type expects TransactionType enum
99+
builder.type('Send');
100+
} catch (e) {
101+
errorThrown = true;
102+
errorMessage = (e as Error).message;
103+
}
104+
105+
errorThrown.should.equal(false);
106+
errorMessage.should.not.match(/Cannot use common sdk module/);
107+
});
108+
});
109+
});
110+
111+
it('should verify all XDC tokens use XdcToken class, not EthLikeErc20Token', function () {
112+
const allTokens = [...mainnetTokens, ...testnetTokens];
113+
114+
allTokens.forEach((tokenName) => {
115+
const token = bitgo.coin(tokenName);
116+
token.should.be.instanceOf(XdcToken);
117+
token.constructor.name.should.equal('XdcToken');
118+
token.constructor.name.should.not.equal('EthLikeErc20Token');
119+
});
120+
});
121+
});
122+
123+
describe('verifyTssTransaction', function () {
124+
it('should return true for valid token transfer params', async function () {
125+
const token = bitgo.coin('txdc:tmt') as XdcToken;
126+
const mockWallet = {} as unknown as IWallet;
127+
128+
const result = await token.verifyTssTransaction({
129+
txParams: {
130+
recipients: [
131+
{
132+
address: mockTokenTransferData.recipientAddress,
133+
amount: mockTokenTransferData.tokenAmount,
134+
},
135+
],
136+
},
137+
txPrebuild: mockTokenTransferData.txPrebuild as unknown as Parameters<
138+
typeof token.verifyTssTransaction
139+
>[0]['txPrebuild'],
140+
wallet: mockWallet,
141+
});
142+
143+
result.should.equal(true);
144+
});
145+
146+
it('should return true for transferToken type without recipients', async function () {
147+
const token = bitgo.coin('txdc:tmt') as XdcToken;
148+
const mockWallet = {} as unknown as IWallet;
149+
150+
const result = await token.verifyTssTransaction({
151+
txParams: {
152+
type: 'transferToken',
153+
},
154+
txPrebuild: mockTokenTransferData.txPrebuild as unknown as Parameters<
155+
typeof token.verifyTssTransaction
156+
>[0]['txPrebuild'],
157+
wallet: mockWallet,
158+
});
159+
160+
result.should.equal(true);
161+
});
162+
163+
it('should throw error when txParams.recipients is missing and no valid type', async function () {
164+
const token = bitgo.coin('txdc:tmt') as XdcToken;
165+
const mockWallet = {} as unknown as IWallet;
166+
167+
await token
168+
.verifyTssTransaction({
169+
txParams: {},
170+
txPrebuild: mockTokenTransferData.txPrebuild as unknown as Parameters<
171+
typeof token.verifyTssTransaction
172+
>[0]['txPrebuild'],
173+
wallet: mockWallet,
174+
})
175+
.should.be.rejectedWith('missing txParams');
176+
});
177+
178+
it('should throw error when wallet is missing', async function () {
179+
const token = bitgo.coin('txdc:tmt') as XdcToken;
180+
181+
await token
182+
.verifyTssTransaction({
183+
txParams: {
184+
recipients: [
185+
{
186+
address: mockTokenTransferData.recipientAddress,
187+
amount: mockTokenTransferData.tokenAmount,
188+
},
189+
],
190+
},
191+
txPrebuild: mockTokenTransferData.txPrebuild as unknown as Parameters<
192+
typeof token.verifyTssTransaction
193+
>[0]['txPrebuild'],
194+
wallet: undefined as unknown as IWallet,
195+
})
196+
.should.be.rejectedWith('missing params');
197+
});
198+
199+
it('should throw error when txPrebuild is missing', async function () {
200+
const token = bitgo.coin('txdc:tmt') as XdcToken;
201+
const mockWallet = {} as unknown as IWallet;
202+
203+
await token
204+
.verifyTssTransaction({
205+
txParams: {
206+
recipients: [
207+
{
208+
address: mockTokenTransferData.recipientAddress,
209+
amount: mockTokenTransferData.tokenAmount,
210+
},
211+
],
212+
},
213+
txPrebuild: undefined as unknown as Parameters<typeof token.verifyTssTransaction>[0]['txPrebuild'],
214+
wallet: mockWallet,
215+
})
216+
.should.be.rejectedWith('missing params');
217+
});
218+
219+
it('should throw error for batch + hop transaction', async function () {
220+
const token = bitgo.coin('txdc:tmt') as XdcToken;
221+
const mockWallet = {} as unknown as IWallet;
222+
223+
await token
224+
.verifyTssTransaction({
225+
txParams: {
226+
hop: true,
227+
recipients: [
228+
{ address: '0x1111111111111111111111111111111111111111', amount: '1000' },
229+
{ address: '0x2222222222222222222222222222222222222222', amount: '2000' },
230+
],
231+
},
232+
txPrebuild: mockTokenTransferData.txPrebuild as unknown as Parameters<
233+
typeof token.verifyTssTransaction
234+
>[0]['txPrebuild'],
235+
wallet: mockWallet,
236+
})
237+
.should.be.rejectedWith('tx cannot be both a batch and hop transaction');
238+
});
239+
240+
it('should not throw EIP155 error when verifying token transaction', async function () {
241+
// This test ensures that verifyTssTransaction does NOT parse the txHex
242+
// which would fail with "Incompatible EIP155-based V" error
243+
const token = bitgo.coin('txdc:tmt') as XdcToken;
244+
const mockWallet = {} as unknown as IWallet;
245+
246+
// Use the signableHex (with v=51) which would fail if parsed
247+
const txPrebuildWithSignableHex = {
248+
...mockTokenTransferData.txPrebuild,
249+
txHex: mockTokenTransferData.signableHex,
250+
};
251+
252+
// This should NOT throw EIP155 error because verifyTssTransaction
253+
// does not parse the transaction
254+
const result = await token.verifyTssTransaction({
255+
txParams: {
256+
recipients: [
257+
{
258+
address: mockTokenTransferData.recipientAddress,
259+
amount: mockTokenTransferData.tokenAmount,
260+
},
261+
],
262+
},
263+
txPrebuild: txPrebuildWithSignableHex as unknown as Parameters<
264+
typeof token.verifyTssTransaction
265+
>[0]['txPrebuild'],
266+
wallet: mockWallet,
267+
});
268+
269+
result.should.equal(true);
270+
});
271+
});
30272
});

modules/statics/src/account.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2888,7 +2888,7 @@ export function xdcErc20(
28882888
decimalPlaces: number,
28892889
contractAddress: string,
28902890
asset: UnderlyingAsset,
2891-
features: CoinFeature[] = [...AccountCoin.DEFAULT_FEATURES, CoinFeature.EIP1559],
2891+
features: CoinFeature[] = AccountCoin.DEFAULT_FEATURES,
28922892
prefix = '',
28932893
suffix: string = name.toUpperCase(),
28942894
network: AccountNetwork = Networks.main.xdc,

0 commit comments

Comments
 (0)