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
97 changes: 97 additions & 0 deletions modules/sdk-coin-trx/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,17 @@ export function decodeRawTransaction(hexString: string): {
};
}

/**
* Converts a base64 encoded string to hex
*
* @param base64 - The base64 encoded string to convert
* @returns {string} - The hex representation
*/
export function getHexFromBase64(base64: string): string {
const buffer = Buffer.from(base64, 'base64');
return buffer.toString('hex');
}

/**
* Indicates whether the passed string is a safe hex string for tron's purposes.
*
Expand Down Expand Up @@ -837,3 +848,89 @@ export function decodeDataParams(types: string[], data: string): any[] {
return obj;
}, []);
}

/**
* Generate raw_data_hex for a TRON transaction
*
* @param {Object} rawData - The transaction raw data object containing:
* @param {Array} rawData.contract - Array of contract objects
* @param {string} rawData.refBlockBytes - Reference block bytes
* @param {string} rawData.refBlockHash - Reference block hash
* @param {number} rawData.expiration - Transaction expiration timestamp
* @param {number} rawData.timestamp - Transaction creation timestamp
* @param {number} [rawData.feeLimit] - Optional fee limit for smart contracts
* @returns {string} The hex string representation of the encoded transaction data
*/
export function generateRawDataHex(
rawData: {
contract?: protocol.Transaction.Contract[];
refBlockBytes?: string;
refBlockHash?: string;
expiration?: number;
timestamp?: number;
feeLimit?: number;
} = {}
): string {
try {
// Process contracts to ensure proper protobuf encoding
let processedContracts = rawData.contract;
if (rawData.contract && rawData.contract.length > 0) {
processedContracts = rawData.contract.map((contract) => {
// Handle TransferContract specifically
if (contract.parameter?.type_url === 'type.googleapis.com/protocol.TransferContract') {
const contractValue = contract.parameter.value as any;

// Create the protobuf contract object
const transferContract: any = {};

// Handle owner_address (required field)
if (contractValue.owner_address) {
transferContract.ownerAddress = Buffer.from(contractValue.owner_address, 'hex');
}

// Handle to_address (required field)
if (contractValue.to_address) {
transferContract.toAddress = Buffer.from(contractValue.to_address, 'hex');
}

// Handle amount (required field)
if (contractValue.amount !== undefined) {
transferContract.amount = contractValue.amount;
}

// Encode the contract using protobuf
const encodedContract = protocol.TransferContract.encode(transferContract).finish();
const base64Value = Buffer.from(encodedContract).toString('base64');

return {
...contract,
parameter: {
...contract.parameter,
value: base64Value,
},
} as any;
}

return contract;
}) as protocol.Transaction.Contract[];
}

// Create raw transaction object matching protobuf schema
const rawTx: protocol.Transaction.Iraw = {
contract: processedContracts,
refBlockBytes: rawData.refBlockBytes ? Buffer.from(rawData.refBlockBytes, 'hex') : undefined,
refBlockHash: rawData.refBlockHash ? Buffer.from(rawData.refBlockHash, 'hex') : undefined,
expiration: rawData.expiration,
timestamp: rawData.timestamp,
feeLimit: rawData.feeLimit,
};

// Encode using protobuf and get final bytes
const encodedBytes = protocol.Transaction.raw.encode(rawTx).finish();

// Convert to hex string
return Buffer.from(encodedBytes).toString('hex');
} catch (e) {
throw new UtilsError('Failed to generate raw data hex: ' + e.message);
}
}
66 changes: 65 additions & 1 deletion modules/sdk-coin-trx/src/trx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ import {
AuditDecryptedKeyParams,
} from '@bitgo/sdk-core';
import { Interface, Utils, WrappedBuilder } from './lib';
import { ValueFields, TransactionReceipt } from './lib/iface';
import { getBuilder } from './lib/builder';
import { TransactionReceipt } from './lib/iface';
import { isInteger, isUndefined } from 'lodash';

export const MINIMUM_TRON_MSIG_TRANSACTION_FEE = 1e6;
Expand Down Expand Up @@ -250,6 +250,70 @@ export class Trx extends BaseCoin {
}

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

if (!txParams) {
throw new Error('missing txParams');
}

if (!txPrebuild) {
throw new Error('missing txPrebuild');
}

if (!txPrebuild.txHex) {
throw new Error('missing txHex in txPrebuild');
}

const rawTx = txPrebuild.txHex;
const txBuilder = getBuilder(this.getChain()).from(rawTx);
const tx = await txBuilder.build();
const txJson = tx.toJson();

if (!txJson.raw_data || !txJson.raw_data.contract || txJson.raw_data.contract.length !== 1) {
throw new Error('Number of contracts is greater than 1.');
}

const contract = txJson.raw_data.contract[0];

if (contract.type === 'TransferContract') {
return this.validateTransferContract(contract, txParams);
} else {
return true;
}
}

/**
* Validate Transfer contract (native TRX transfer)
*/
private validateTransferContract(contract: any, txParams: any): boolean {
if (!('parameter' in contract) || !contract.parameter?.value) {
throw new Error('Invalid Transfer contract structure');
}

const value = contract.parameter.value as ValueFields;

// Validate amount
if (!value.amount || value.amount < 0) {
throw new Error('Invalid transfer amount');
}

// If txParams has recipients, validate against expected values
if (txParams.recipients && txParams.recipients.length === 1) {
const recipient = txParams.recipients[0];
const expectedAmount = recipient.amount.toString();
const expectedDestination = recipient.address;
const actualAmount = value.amount.toString();
const actualDestination = Utils.getBase58AddressFromHex(value.to_address);

if (expectedAmount !== actualAmount) {
throw new Error('transaction amount in txPrebuild does not match the value given by client');
}

if (expectedDestination.toLowerCase() !== actualDestination.toLowerCase()) {
throw new Error('destination address does not match with the recipient address');
}
}

return true;
}

Expand Down
8 changes: 4 additions & 4 deletions modules/sdk-coin-trx/test/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ export const DELEGATE_RESOURCE_CONTRACT = [
value: {
resource: 'ENERGY',
balance: 1000000,
owner_address: '41d6cd6a2c0ff35a319e6abb5b9503ba0278679882',
receiver_address: '416ffedf93921506c3efdb510f7c4f256036c48a6a',
owner_address: '4173a5993cd182ae152adad8203163f780c65a8aa5',
receiver_address: '4173a5993cd182ae152adad8203163f780c65a8aa5',
},
type_url: 'type.googleapis.com/protocol.DelegateResourceContract',
},
Expand All @@ -190,8 +190,8 @@ export const UNDELEGATE_RESOURCE_CONTRACT = [
value: {
resource: 'ENERGY',
balance: 1000000,
owner_address: '41d6cd6a2c0ff35a319e6abb5b9503ba0278679882',
receiver_address: '416ffedf93921506c3efdb510f7c4f256036c48a6a',
owner_address: '4173a5993cd182ae152adad8203163f780c65a8aa5',
receiver_address: '4173a5993cd182ae152adad8203163f780c65a8aa5',
},
type_url: 'type.googleapis.com/protocol.UnDelegateResourceContract',
},
Expand Down
Loading