Skip to content

Commit a0464d4

Browse files
committed
feat(sdk): added the validation for verifyTxn for trx
COIN-3579 TICKET: COIN-3579
1 parent c0fbe9e commit a0464d4

File tree

4 files changed

+592
-5
lines changed

4 files changed

+592
-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: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ 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';
37+
import assert from 'assert';
3738

3839
export const MINIMUM_TRON_MSIG_TRANSACTION_FEE = 1e6;
3940
export const SAFE_TRON_TRANSACTION_FEE = 2.1 * 1e6; // TRON foundation recommends 2.1 TRX as fees for guaranteed transaction
@@ -250,6 +251,75 @@ export class Trx extends BaseCoin {
250251
}
251252

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

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)