diff --git a/modules/sdk-coin-trx/src/lib/iface.ts b/modules/sdk-coin-trx/src/lib/iface.ts index e7ab0bda71..0056aa3b01 100644 --- a/modules/sdk-coin-trx/src/lib/iface.ts +++ b/modules/sdk-coin-trx/src/lib/iface.ts @@ -125,7 +125,7 @@ export interface AccountInfo { keys: [PermissionKey]; }; active_permission: [{ keys: [PermissionKey] }]; - trc20: [Record]; + trc20?: Record[]; } /** diff --git a/modules/sdk-coin-trx/src/trx.ts b/modules/sdk-coin-trx/src/trx.ts index 0a00781e88..1b9e2e782f 100644 --- a/modules/sdk-coin-trx/src/trx.ts +++ b/modules/sdk-coin-trx/src/trx.ts @@ -871,7 +871,7 @@ export class Trx extends BaseCoin { // check for possible token recovery, recover the token provide by user if (tokenContractAddr) { let rawTokenTxn: any | undefined; - for (const token of account.data[0].trc20) { + for (const token of account.data[0].trc20 ?? []) { if (token[tokenContractAddr]) { const amount = token[tokenContractAddr]; const tokenContractAddrHex = Utils.getHexAddressFromBase58Address(tokenContractAddr); @@ -1053,7 +1053,7 @@ export class Trx extends BaseCoin { let rawTokenTxn: any | undefined; let recoveryAmount = 0; - for (const token of account.data[0].trc20) { + for (const token of account.data[0].trc20 ?? []) { if (token[tokenContractAddr]) { const amount = token[tokenContractAddr]; const tokenContractAddrHex = Utils.getHexAddressFromBase58Address(tokenContractAddr); @@ -1165,8 +1165,9 @@ export class Trx extends BaseCoin { // check for possible token recovery, recover the token provide by user if (params.tokenContractAddress) { - if (accountInfo.data[0].balance > SAFE_TRON_TOKEN_TRANSACTION_FEE && accountInfo.data[0].trc20[0]) { - const tokenDataArray = accountInfo.data[0].trc20; + const trc20Data = accountInfo.data[0].trc20 ?? []; + if (accountInfo.data[0].balance > SAFE_TRON_TOKEN_TRANSACTION_FEE && trc20Data.length > 0) { + const tokenDataArray = trc20Data; for (const tokenData of tokenDataArray) { const contractAddress = Object.keys(tokenData) as Array; if (params.tokenContractAddress === contractAddress[0]) { diff --git a/modules/sdk-coin-trx/test/resources.ts b/modules/sdk-coin-trx/test/resources.ts index 11dd695711..e770293623 100644 --- a/modules/sdk-coin-trx/test/resources.ts +++ b/modules/sdk-coin-trx/test/resources.ts @@ -353,6 +353,96 @@ export function receiveAddressBalance(balance: number, address: string, trc20Bal }; } +export function receiveAddressBalanceWithoutTrc20Field(balance: number, address: string) { + return { + data: [ + { + owner_permission: { + keys: [ + { + address: address, + weight: 1, + }, + ], + threshold: 1, + permission_name: 'owner', + }, + active_permission: [ + { + operations: '7fff1fc0033ec30f000000000000000000000000000000000000000000000000', + keys: [ + { + address: address, + weight: 1, + }, + ], + threshold: 1, + id: 2, + type: 'Active', + permission_name: 'active', + }, + ], + balance: balance, + // trc20 field intentionally absent — real TRON node omits it for + // accounts that have never interacted with TRC20 tokens + }, + ], + }; +} + +export function baseAddressBalanceWithoutTrc20Field(trxBalance: number) { + return { + data: [ + { + owner_permission: { + keys: [ + { + address: 'TTgisRP7EJWMgpLXvbNHoHh5UotkjkBPoo', + weight: 1, + }, + { + address: 'TBDy8HAy8vvhoqKc5V1hHQatfjZHM1MhPb', + weight: 1, + }, + { + address: 'TPHPDfQ8Vs3Yp5UKDLHr5MjoVUrr5Y69m9', + weight: 1, + }, + ], + threshold: 2, + permission_name: 'owner', + }, + balance: trxBalance, + // trc20 field intentionally absent — real TRON node omits it for + // accounts that have never interacted with TRC20 tokens + active_permission: [ + { + operations: '7fff1fc0037e0000000000000000000000000000000000000000000000000000', + keys: [ + { + address: 'TTgisRP7EJWMgpLXvbNHoHh5UotkjkBPoo', + weight: 1, + }, + { + address: 'TBDy8HAy8vvhoqKc5V1hHQatfjZHM1MhPb', + weight: 1, + }, + { + address: 'TPHPDfQ8Vs3Yp5UKDLHr5MjoVUrr5Y69m9', + weight: 1, + }, + ], + threshold: 2, + id: 2, + type: 'Active', + permission_name: 'active0', + }, + ], + }, + ], + }; +} + export function creationTransaction(fromAddress: string, toAddress: string, amount: number) { return { visible: false, diff --git a/modules/sdk-coin-trx/test/unit/trx.ts b/modules/sdk-coin-trx/test/unit/trx.ts index a8cccb56f6..8c0a304f29 100644 --- a/modules/sdk-coin-trx/test/unit/trx.ts +++ b/modules/sdk-coin-trx/test/unit/trx.ts @@ -7,8 +7,10 @@ import { Trx, Ttrx, Utils } from '../../src'; import { signTxOptions, mockTx } from '../fixtures'; import { baseAddressBalance, + baseAddressBalanceWithoutTrc20Field, SampleRawTokenSendTxn, receiveAddressBalance, + receiveAddressBalanceWithoutTrc20Field, TestRecoverData, TssTestRecoverData, creationTransaction, @@ -442,6 +444,28 @@ describe('TRON:', function () { } ); }); + + it('should throw if trc20 field absent and token recovery requested (base address)', async function () { + mock.method(Trx.prototype as any, 'getAccountBalancesFromNode', (...args) => { + if (args.length > 0 && args[0] === TestRecoverData.baseAddress) { + return Promise.resolve(baseAddressBalanceWithoutTrc20Field(100000000)); + } + return undefined; + }); + + await assert.rejects( + basecoin.recover({ + userKey: TestRecoverData.userKey, + backupKey: TestRecoverData.backupKey, + bitgoKey: TestRecoverData.bitgoKey, + tokenContractAddress: 'TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs', + recoveryDestination: TestRecoverData.recoveryDestination, + }), + { + message: 'Not found token to recover, please check token balance', + } + ); + }); }); describe('Build Unsigned Consolidation Recoveries', () => { @@ -633,6 +657,34 @@ describe('TRON:', function () { assert.equal(Utils.getBase58AddressFromHex(value1.owner_address), TestRecoverData.firstReceiveAddress); assert.equal(Utils.getBase58AddressFromHex(value1.to_address), TestRecoverData.baseAddress); }); + + it('should skip token consolidation when trc20 field absent from account response', async () => { + mock.method(Trx.prototype as any, 'getAccountBalancesFromNode', (...args) => { + if (args.length > 0 && args[0] === TestRecoverData.firstReceiveAddress) { + return Promise.resolve( + receiveAddressBalanceWithoutTrc20Field(202100000, TestRecoverData.firstReceiveAddress) + ); + } + + if (args.length > 0 && args[0] === TestRecoverData.secondReceiveAddress) { + return Promise.resolve(receiveAddressBalanceWithoutTrc20Field(500, TestRecoverData.secondReceiveAddress)); + } + + return undefined; + }); + + const res = await basecoin.recoverConsolidations({ + userKey: TestRecoverData.userKey, + backupKey: TestRecoverData.backupKey, + bitgoKey: TestRecoverData.bitgoKey, + tokenContractAddress: 'TSdZwNqpHofzP6BsBKGQUWdBeJphLmF6id', + startingScanIndex: 1, + endingScanIndex: 3, + }); + + assert.ok(Object.prototype.hasOwnProperty.call(res, 'transactions')); + assert.equal(res.transactions.length, 0); + }); }); describe('TSS Recovery', () => { @@ -824,6 +876,26 @@ describe('TRON:', function () { } ); }); + + it('should throw if trc20 field absent for TSS wallet token recovery', async function () { + mock.method(Trx.prototype as any, 'getAccountBalancesFromNode', () => { + return Promise.resolve(baseAddressBalanceWithoutTrc20Field(100000000)); + }); + + await assert.rejects( + basecoin.recover({ + userKey: TssTestRecoverData.userKey, + backupKey: TssTestRecoverData.backupKey, + bitgoKey: TssTestRecoverData.bitgoKey, + tokenContractAddress: 'TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs', + recoveryDestination: TssTestRecoverData.recoveryDestination, + isTss: true, + }), + { + message: 'Not found token to recover, please check token balance', + } + ); + }); }); describe('isWalletAddress', () => {