Skip to content

Commit f386a91

Browse files
authored
Merge pull request #7065 from BitGo/COIN-3579
feat(sdk): added the validation for verifyTxn for trx
2 parents 26eb453 + b5faa7c commit f386a91

File tree

4 files changed

+586
-5
lines changed

4 files changed

+586
-5
lines changed

modules/sdk-coin-trx/src/lib/utils.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,17 @@ export function decodeRawTransaction(hexString: string): {
283283
};
284284
}
285285

286+
/**
287+
* Converts a base64 encoded string to hex
288+
*
289+
* @param base64 - The base64 encoded string to convert
290+
* @returns {string} - The hex representation
291+
*/
292+
export function getHexFromBase64(base64: string): string {
293+
const buffer = Buffer.from(base64, 'base64');
294+
return buffer.toString('hex');
295+
}
296+
286297
/**
287298
* Indicates whether the passed string is a safe hex string for tron's purposes.
288299
*
@@ -837,3 +848,89 @@ export function decodeDataParams(types: string[], data: string): any[] {
837848
return obj;
838849
}, []);
839850
}
851+
852+
/**
853+
* Generate raw_data_hex for a TRON transaction
854+
*
855+
* @param {Object} rawData - The transaction raw data object containing:
856+
* @param {Array} rawData.contract - Array of contract objects
857+
* @param {string} rawData.refBlockBytes - Reference block bytes
858+
* @param {string} rawData.refBlockHash - Reference block hash
859+
* @param {number} rawData.expiration - Transaction expiration timestamp
860+
* @param {number} rawData.timestamp - Transaction creation timestamp
861+
* @param {number} [rawData.feeLimit] - Optional fee limit for smart contracts
862+
* @returns {string} The hex string representation of the encoded transaction data
863+
*/
864+
export function generateRawDataHex(
865+
rawData: {
866+
contract?: protocol.Transaction.Contract[];
867+
refBlockBytes?: string;
868+
refBlockHash?: string;
869+
expiration?: number;
870+
timestamp?: number;
871+
feeLimit?: number;
872+
} = {}
873+
): string {
874+
try {
875+
// Process contracts to ensure proper protobuf encoding
876+
let processedContracts = rawData.contract;
877+
if (rawData.contract && rawData.contract.length > 0) {
878+
processedContracts = rawData.contract.map((contract) => {
879+
// Handle TransferContract specifically
880+
if (contract.parameter?.type_url === 'type.googleapis.com/protocol.TransferContract') {
881+
const contractValue = contract.parameter.value as any;
882+
883+
// Create the protobuf contract object
884+
const transferContract: any = {};
885+
886+
// Handle owner_address (required field)
887+
if (contractValue.owner_address) {
888+
transferContract.ownerAddress = Buffer.from(contractValue.owner_address, 'hex');
889+
}
890+
891+
// Handle to_address (required field)
892+
if (contractValue.to_address) {
893+
transferContract.toAddress = Buffer.from(contractValue.to_address, 'hex');
894+
}
895+
896+
// Handle amount (required field)
897+
if (contractValue.amount !== undefined) {
898+
transferContract.amount = contractValue.amount;
899+
}
900+
901+
// Encode the contract using protobuf
902+
const encodedContract = protocol.TransferContract.encode(transferContract).finish();
903+
const base64Value = Buffer.from(encodedContract).toString('base64');
904+
905+
return {
906+
...contract,
907+
parameter: {
908+
...contract.parameter,
909+
value: base64Value,
910+
},
911+
} as any;
912+
}
913+
914+
return contract;
915+
}) as protocol.Transaction.Contract[];
916+
}
917+
918+
// Create raw transaction object matching protobuf schema
919+
const rawTx: protocol.Transaction.Iraw = {
920+
contract: processedContracts,
921+
refBlockBytes: rawData.refBlockBytes ? Buffer.from(rawData.refBlockBytes, 'hex') : undefined,
922+
refBlockHash: rawData.refBlockHash ? Buffer.from(rawData.refBlockHash, 'hex') : undefined,
923+
expiration: rawData.expiration,
924+
timestamp: rawData.timestamp,
925+
feeLimit: rawData.feeLimit,
926+
};
927+
928+
// Encode using protobuf and get final bytes
929+
const encodedBytes = protocol.Transaction.raw.encode(rawTx).finish();
930+
931+
// Convert to hex string
932+
return Buffer.from(encodedBytes).toString('hex');
933+
} catch (e) {
934+
throw new UtilsError('Failed to generate raw data hex: ' + e.message);
935+
}
936+
}

modules/sdk-coin-trx/src/trx.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ import {
3131
AuditDecryptedKeyParams,
3232
} from '@bitgo/sdk-core';
3333
import { Interface, Utils, WrappedBuilder } from './lib';
34+
import { ValueFields, TransactionReceipt } from './lib/iface';
3435
import { getBuilder } from './lib/builder';
35-
import { TransactionReceipt } from './lib/iface';
3636
import { isInteger, isUndefined } from 'lodash';
3737

3838
export const MINIMUM_TRON_MSIG_TRANSACTION_FEE = 1e6;
@@ -250,6 +250,70 @@ export class Trx extends BaseCoin {
250250
}
251251

252252
async verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {
253+
const { txParams, txPrebuild } = params;
254+
255+
if (!txParams) {
256+
throw new Error('missing txParams');
257+
}
258+
259+
if (!txPrebuild) {
260+
throw new Error('missing txPrebuild');
261+
}
262+
263+
if (!txPrebuild.txHex) {
264+
throw new Error('missing txHex in txPrebuild');
265+
}
266+
267+
const rawTx = txPrebuild.txHex;
268+
const txBuilder = getBuilder(this.getChain()).from(rawTx);
269+
const tx = await txBuilder.build();
270+
const txJson = tx.toJson();
271+
272+
if (!txJson.raw_data || !txJson.raw_data.contract || txJson.raw_data.contract.length !== 1) {
273+
throw new Error('Number of contracts is greater than 1.');
274+
}
275+
276+
const contract = txJson.raw_data.contract[0];
277+
278+
if (contract.type === 'TransferContract') {
279+
return this.validateTransferContract(contract, txParams);
280+
} else {
281+
return true;
282+
}
283+
}
284+
285+
/**
286+
* Validate Transfer contract (native TRX transfer)
287+
*/
288+
private validateTransferContract(contract: any, txParams: any): boolean {
289+
if (!('parameter' in contract) || !contract.parameter?.value) {
290+
throw new Error('Invalid Transfer contract structure');
291+
}
292+
293+
const value = contract.parameter.value as ValueFields;
294+
295+
// Validate amount
296+
if (!value.amount || value.amount < 0) {
297+
throw new Error('Invalid transfer amount');
298+
}
299+
300+
// If txParams has recipients, validate against expected values
301+
if (txParams.recipients && txParams.recipients.length === 1) {
302+
const recipient = txParams.recipients[0];
303+
const expectedAmount = recipient.amount.toString();
304+
const expectedDestination = recipient.address;
305+
const actualAmount = value.amount.toString();
306+
const actualDestination = Utils.getBase58AddressFromHex(value.to_address);
307+
308+
if (expectedAmount !== actualAmount) {
309+
throw new Error('transaction amount in txPrebuild does not match the value given by client');
310+
}
311+
312+
if (expectedDestination.toLowerCase() !== actualDestination.toLowerCase()) {
313+
throw new Error('destination address does not match with the recipient address');
314+
}
315+
}
316+
253317
return true;
254318
}
255319

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,8 @@ export const DELEGATE_RESOURCE_CONTRACT = [
175175
value: {
176176
resource: 'ENERGY',
177177
balance: 1000000,
178-
owner_address: '41d6cd6a2c0ff35a319e6abb5b9503ba0278679882',
179-
receiver_address: '416ffedf93921506c3efdb510f7c4f256036c48a6a',
178+
owner_address: '4173a5993cd182ae152adad8203163f780c65a8aa5',
179+
receiver_address: '4173a5993cd182ae152adad8203163f780c65a8aa5',
180180
},
181181
type_url: 'type.googleapis.com/protocol.DelegateResourceContract',
182182
},
@@ -190,8 +190,8 @@ export const UNDELEGATE_RESOURCE_CONTRACT = [
190190
value: {
191191
resource: 'ENERGY',
192192
balance: 1000000,
193-
owner_address: '41d6cd6a2c0ff35a319e6abb5b9503ba0278679882',
194-
receiver_address: '416ffedf93921506c3efdb510f7c4f256036c48a6a',
193+
owner_address: '4173a5993cd182ae152adad8203163f780c65a8aa5',
194+
receiver_address: '4173a5993cd182ae152adad8203163f780c65a8aa5',
195195
},
196196
type_url: 'type.googleapis.com/protocol.UnDelegateResourceContract',
197197
},

0 commit comments

Comments
 (0)