Skip to content
Closed
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
21 changes: 13 additions & 8 deletions modules/sdk-coin-trx/src/trxToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,10 @@ export class TrxToken extends Trx {

if (walletType === 'tss') {
// For TSS wallets, TRC20 token transfers are TriggerSmartContract transactions.
// Decode the transaction and validate destination address and amount against
// txParams.recipients before signing to ensure intent matches the prebuild.
// Always verify structure and ABI decodability. Intent validation (address + amount
// comparison) is performed only when recipients are present — absent recipients
// indicates a server-determined transfer (e.g. consolidation) where the server
// owns the intent.
const rawDataHex = this.extractRawDataHex(txPrebuild.txHex);
const decodedTx = Utils.decodeTransaction(rawDataHex);

Expand All @@ -110,7 +112,6 @@ export class TrxToken extends Trx {
`Expected TriggerSmartContract for TRC20 token transfer, got contract type: ${decodedTx.contractType}`
);
}

if (!Array.isArray(decodedTx.contract) || decodedTx.contract.length !== 1) {
throw new Error('Invalid TriggerSmartContract structure');
}
Expand All @@ -119,11 +120,6 @@ export class TrxToken extends Trx {
// data is base64-encoded from protobuf decoding; convert to hex for decodeDataParams
const contractData = Buffer.from(triggerContract.parameter.value.data, 'base64').toString('hex');

const recipients = txParams.recipients || (txPrebuild.txInfo as TronTxInfo).recipients;
if (!recipients || recipients.length !== 1) {
throw new Error('missing or invalid required property recipients');
}

let recipientHex: string;
let transferAmount: { toString(): string };
try {
Expand All @@ -135,6 +131,15 @@ export class TrxToken extends Trx {
throw new Error(`Failed to decode TRC20 transfer ABI data: ${e instanceof Error ? e.message : String(e)}`);
}

const recipients = txParams.recipients || (txPrebuild.txInfo as TronTxInfo | undefined)?.recipients;
if (!recipients || recipients.length === 0) {
// No recipients — server-determined transfer (e.g. consolidation); structural check above is sufficient.
return true;
}
if (recipients.length !== 1) {
throw new Error('invalid required property recipients');
}

// recipientHex has '41' hex prefix; convert to base58 for comparison
const actualDestination = Utils.getBase58AddressFromHex(recipientHex);
const actualAmount = transferAmount.toString();
Expand Down
32 changes: 23 additions & 9 deletions modules/sdk-coin-trx/test/unit/trxToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,29 @@ describe('TrxToken verifyTransaction:', function () {
);
});

it('should throw when recipients is empty', async function () {
await assert.rejects(
tokenCoin.verifyTransaction({
txPrebuild: { txHex: TRC20_RAW_DATA_HEX },
txParams: { recipients: [] },
walletType: 'tss',
} as any),
{ message: 'missing or invalid required property recipients' }
);
it('should return true when recipients is absent (consolidation path — undefined)', async function () {
// Consolidation: server determines intent; no recipients on txParams or txInfo.
// Verify passes because tx structure is valid TriggerSmartContract AND ABI decodes
// to a valid (address, uint256) pair — malformed ABI would still throw.
const result = await tokenCoin.verifyTransaction({
txPrebuild: { txHex: TRC20_RAW_DATA_HEX },
txParams: {},
walletType: 'tss',
} as any);

assert.strictEqual(result, true);
});

it('should return true when recipients is empty array (consolidation path — [])', async function () {
// Some callers pass recipients: [] rather than omitting the field entirely.
// Empty array must be treated the same as absent — consolidation, no intent match.
const result = await tokenCoin.verifyTransaction({
txPrebuild: { txHex: TRC20_RAW_DATA_HEX },
txParams: { recipients: [] },
walletType: 'tss',
} as any);

assert.strictEqual(result, true);
});

it('should throw when contract type is not TriggerSmartContract', async function () {
Expand Down
Loading