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
38 changes: 33 additions & 5 deletions modules/sdk-coin-trx/src/trx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
isTssVerifyAddressOptions,
} from '@bitgo/sdk-core';
import { auditEcdsaPrivateKey } from '@bitgo/sdk-lib-mpc';
import { Interface, Utils, WrappedBuilder, KeyPair as TronKeyPair } from './lib';
import { Enum, Interface, Utils, WrappedBuilder, KeyPair as TronKeyPair } from './lib';
import { ValueFields, TransactionReceipt } from './lib/iface';
import { getBuilder } from './lib/builder';
import { isInteger, isUndefined } from 'lodash';
Expand Down Expand Up @@ -364,7 +364,7 @@ export class Trx extends BaseCoin {
}

async verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {
const { txParams, txPrebuild } = params;
const { txParams, txPrebuild, walletType } = params;

if (!txParams) {
throw new Error('missing txParams');
Expand All @@ -378,6 +378,28 @@ export class Trx extends BaseCoin {
throw new Error('missing txHex in txPrebuild');
}

if (walletType === 'tss') {
// For TSS wallets, txHex is the signableHex (raw_data_hex protobuf bytes),
// not a full transaction JSON. Decode it directly via protobuf.
// Note: decodeTransaction already validates exactly 1 contract exists.
const decodedTx = Utils.decodeTransaction(txPrebuild.txHex);

// decodedTx uses a numeric enum for contract type (from protobuf decoding),
// unlike the multisig path which checks the string 'TransferContract' from node JSON.
if (decodedTx.contractType === Enum.ContractType.Transfer) {
// For Transfer contracts, decoded contract is an array with one element.
// Addresses from decodeTransaction are already in base58 format (converted by decodeTransferContract).
if (!Array.isArray(decodedTx.contract) || decodedTx.contract.length !== 1) {
throw new Error('Invalid Transfer contract structure.');
}
return this.validateTransferContract(decodedTx.contract[0], txParams, true);
}

return true;
}

// On-chain multisig path: txHex is a full transaction JSON string (with txID, raw_data, raw_data_hex).
// The builder parses this JSON and addresses remain in hex format from the node response.
const rawTx = txPrebuild.txHex;
const txBuilder = getBuilder(this.getChain()).from(rawTx);
const tx = await txBuilder.build();
Expand All @@ -397,9 +419,15 @@ export class Trx extends BaseCoin {
}

/**
* Validate Transfer contract (native TRX transfer)
* Validate Transfer contract (native TRX transfer).
* Shared by both on-chain multisig and TSS wallet verification paths.
*
* @param contract The contract object from the transaction
* @param txParams The original transaction parameters
* @param addressesInBase58 When true, addresses are already in base58 format (TSS path via protobuf decoding).
* When false (default), addresses are in hex and need conversion (on-chain multisig path via builder JSON).
*/
private validateTransferContract(contract: any, txParams: any): boolean {
private validateTransferContract(contract: any, txParams: any, addressesInBase58 = false): boolean {
if (!('parameter' in contract) || !contract.parameter?.value) {
throw new Error('Invalid Transfer contract structure');
}
Expand All @@ -417,7 +445,7 @@ export class Trx extends BaseCoin {
const expectedAmount = recipient.amount.toString();
const expectedDestination = recipient.address;
const actualAmount = value.amount.toString();
const actualDestination = Utils.getBase58AddressFromHex(value.to_address);
const actualDestination = addressesInBase58 ? value.to_address : Utils.getBase58AddressFromHex(value.to_address);

if (expectedAmount !== actualAmount) {
throw new Error('transaction amount in txPrebuild does not match the value given by client');
Expand Down
160 changes: 159 additions & 1 deletion modules/sdk-coin-trx/test/unit/verifyTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { BitGoAPI } from '@bitgo/sdk-api';
import { TestBitGoAPI, TestBitGo } from '@bitgo/sdk-test';
import { Trx, Ttrx } from '../../src';
import { Utils } from '../../src/lib';
import { UnsignedBuildTransaction } from '../resources';
import { UnsignedBuildTransaction, UnsignedAccountPermissionUpdateContractTx } from '../resources';

describe('TRON Verify Transaction:', function () {
const bitgo: TestBitGoAPI = TestBitGo.decorate(BitGoAPI, { env: 'test' });
Expand Down Expand Up @@ -417,4 +417,162 @@ describe('TRON Verify Transaction:', function () {
});
});
});

describe('TSS Wallet Verification', () => {
/**
* Helper to build a raw_data_hex (protobuf) for a TransferContract.
* For TSS, txPrebuild.txHex is the raw_data_hex directly (not a JSON string).
*/
function buildTssTransferTxHex(params: {
ownerAddress: string;
toAddress: string;
amount: number;
timestamp?: number;
expiration?: number;
}): string {
const timestamp = params.timestamp || Date.now();
const transferContract = {
parameter: {
value: {
amount: params.amount,
owner_address: params.ownerAddress,
to_address: params.toAddress,
},
type_url: 'type.googleapis.com/protocol.TransferContract',
},
type: 'TransferContract',
};

const transformedRawData = {
contract: [transferContract] as any,
refBlockBytes: 'c8cf',
refBlockHash: '89177fd84c5d9196',
expiration: params.expiration || timestamp + 3600000,
timestamp: timestamp,
};

return Utils.generateRawDataHex(transformedRawData);
}

describe('TransferContract', () => {
it('should validate a valid TSS TransferContract', async function () {
const ownerHex = '4173a5993cd182ae152adad8203163f780c65a8aa5';
const toHex = '41d6cd6a2c0ff35a319e6abb5b9503ba0278679882';
const amount = 1000000;

const rawDataHex = buildTssTransferTxHex({ ownerAddress: ownerHex, toAddress: toHex, amount });

const params = {
txParams: {
recipients: [
{
// TSS path: recipients use base58 addresses
address: Utils.getBase58AddressFromHex(toHex),
amount: amount.toString(),
},
],
},
txPrebuild: {
// TSS path: txHex is the raw protobuf hex, not a JSON string
txHex: rawDataHex,
},
wallet: {},
walletType: 'tss',
};

const result = await basecoin.verifyTransaction(params);
assert.strictEqual(result, true);
});

it('should fail TSS verification when amount does not match', async function () {
const ownerHex = '4173a5993cd182ae152adad8203163f780c65a8aa5';
const toHex = '41d6cd6a2c0ff35a319e6abb5b9503ba0278679882';

const rawDataHex = buildTssTransferTxHex({ ownerAddress: ownerHex, toAddress: toHex, amount: 2000000 });

const params = {
txParams: {
recipients: [
{
address: Utils.getBase58AddressFromHex(toHex),
amount: '1000000', // mismatch: txHex has 2000000
},
],
},
txPrebuild: {
txHex: rawDataHex,
},
wallet: {},
walletType: 'tss',
};

await assert.rejects(basecoin.verifyTransaction(params), {
message: 'transaction amount in txPrebuild does not match the value given by client',
});
});

it('should fail TSS verification when destination address does not match', async function () {
const ownerHex = '4173a5993cd182ae152adad8203163f780c65a8aa5';
const toHex = '41d6cd6a2c0ff35a319e6abb5b9503ba0278679882';

const rawDataHex = buildTssTransferTxHex({ ownerAddress: ownerHex, toAddress: toHex, amount: 1000000 });

const params = {
txParams: {
recipients: [
{
// Different address than what's in the transaction
address: 'TTsGwnTLQ4eryFJpDvJSfuGQxPXRCjXvZz',
amount: '1000000',
},
],
},
txPrebuild: {
txHex: rawDataHex,
},
wallet: {},
walletType: 'tss',
};

await assert.rejects(basecoin.verifyTransaction(params), {
message: 'destination address does not match with the recipient address',
});
});
});

it('should return true for non-Transfer contract types in TSS', async function () {
// For non-Transfer contracts (e.g., AccountPermissionUpdate), TSS path returns true
// without detailed validation.
const rawDataHex = UnsignedAccountPermissionUpdateContractTx.raw_data_hex;

const params = {
txParams: {
recipients: [],
},
txPrebuild: {
txHex: rawDataHex,
},
wallet: {},
walletType: 'tss',
};

const result = await basecoin.verifyTransaction(params);
assert.strictEqual(result, true);
});

it('should throw error when txHex is missing for TSS wallet', async function () {
const params = {
txParams: {
recipients: [{ address: 'TQFxDSoXy2yXRE5HtKwAwrNRXGxYxkeSGk', amount: '1000000' }],
},
txPrebuild: {},
wallet: {},
walletType: 'tss',
};

await assert.rejects(basecoin.verifyTransaction(params), {
message: 'missing txHex in txPrebuild',
});
});
});
});
Loading