diff --git a/.changeset/shy-walls-accept.md b/.changeset/shy-walls-accept.md new file mode 100644 index 0000000000..cc04306d66 --- /dev/null +++ b/.changeset/shy-walls-accept.md @@ -0,0 +1,5 @@ +--- +"@swapkit/toolboxes": minor +--- + +Fix Tron transaction building and expiration extension logic diff --git a/packages/toolboxes/src/tron/__tests__/toolbox.test.ts b/packages/toolboxes/src/tron/__tests__/toolbox.test.ts index e765c99c2e..6e43a241ee 100644 --- a/packages/toolboxes/src/tron/__tests__/toolbox.test.ts +++ b/packages/toolboxes/src/tron/__tests__/toolbox.test.ts @@ -8,10 +8,7 @@ const context: { } = {} as any; beforeAll(async () => { - // Set up TRON mainnet configuration SKConfig.set({ rpcUrls: { [Chain.Tron]: ["https://api.trongrid.io"] } }); - - // Get the address validator context.validateAddress = await getTronAddressValidator(); }); @@ -39,15 +36,15 @@ describe("TRON Address Validation", () => { test("should reject invalid TRON addresses", () => { const invalidAddresses = [ - "", // Empty string - "invalid", // Random string - "0x742d35Cc6648C532F5e7c3d2a7a8E1e1e5b7c8D3", // Ethereum address - "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", // Bitcoin address - "cosmos1abc123", // Cosmos address - "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6", // Too short - "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6tt", // Too long - "XR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", // Wrong prefix - "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6O", // Invalid checksum + "", + "invalid", + "0x742d35Cc6648C532F5e7c3d2a7a8E1e1e5b7c8D3", + "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", + "cosmos1abc123", + "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6", + "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6tt", + "XR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", + "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6O", ]; for (const address of invalidAddresses) { @@ -59,13 +56,11 @@ describe("TRON Address Validation", () => { test("should create TRON transaction with valid addresses", async () => { const toolbox = context.toolbox; const fromAddress = "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE"; - const toAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"; // Valid TRON address + const toAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"; - // Both addresses should be valid expect(toolbox.validateAddress(fromAddress)).toBe(true); expect(toolbox.validateAddress(toAddress)).toBe(true); - // Create a transaction const transaction = await toolbox.createTransaction({ assetValue: AssetValue.from({ chain: Chain.Tron, @@ -82,9 +77,8 @@ describe("TRON Address Validation", () => { test("should create TRON.USDT transaction with valid addresses", async () => { const toolbox = context.toolbox; const fromAddress = "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE"; - const toAddress = "TT87ESmqUmH87hMx1MKCEqYrJKaQyNg9ao"; // Valid TRON address + const toAddress = "TT87ESmqUmH87hMx1MKCEqYrJKaQyNg9ao"; - // Both addresses should be valid expect(toolbox.validateAddress(fromAddress)).toBe(true); expect(toolbox.validateAddress(toAddress)).toBe(true); @@ -107,7 +101,6 @@ describe("TRON Address Validation", () => { const lowerCase = address.toLowerCase(); const upperCase = address.toUpperCase(); - // TRON addresses are case sensitive - only the original should be valid expect(context.validateAddress(address)).toBe(true); expect(context.validateAddress(lowerCase)).toBe(false); expect(context.validateAddress(upperCase)).toBe(false); @@ -117,9 +110,121 @@ describe("TRON Address Validation", () => { const edgeCases = [null, undefined, 123, {}, [], true, false]; for (const testCase of edgeCases) { - // Should not throw but return false for invalid inputs expect(context.validateAddress(testCase as any)).toBe(false); expect(context.toolbox.validateAddress(testCase as any)).toBe(false); } }); }); + +describe("TRON createTransaction with Extended Expiration", () => { + const baseExpiration = 60; // default is 60s + const extendedExpiration = 240; // Adding 240 for 5 minutes total + const fromAddress = "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE"; + const toAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"; + const buffer = 10000; + const memo = "Test transfer with memo"; + + test("should create native TRX transfer with extended expiration", async () => { + const toolbox = context.toolbox; + const beforeTimestamp = Date.now(); + + const transaction = await toolbox.createTransaction({ + assetValue: AssetValue.from({ + chain: Chain.Tron, + value: "1", // 1 TRX + }), + expiration: extendedExpiration, + recipient: toAddress, + sender: fromAddress, + }); + + expect(transaction.raw_data.expiration).toBeDefined(); + + const expectedExpiration = beforeTimestamp + (baseExpiration + extendedExpiration) * 1000; + const actualExpiration = transaction.raw_data.expiration; + + expect(actualExpiration).toBeGreaterThanOrEqual(expectedExpiration - buffer); + expect(actualExpiration).toBeLessThanOrEqual(expectedExpiration + buffer); + }); + + test("should create native TRX transfer with extended expiration and memo", async () => { + const toolbox = context.toolbox; + const beforeTimestamp = Date.now(); + + const transaction = await toolbox.createTransaction({ + assetValue: AssetValue.from({ + chain: Chain.Tron, + value: "1", // 1 TRX + }), + expiration: extendedExpiration, + memo, + recipient: toAddress, + sender: fromAddress, + }); + + expect(transaction.raw_data.expiration).toBeDefined(); + + const expectedExpiration = beforeTimestamp + (baseExpiration + extendedExpiration) * 1000; + const actualExpiration = transaction.raw_data.expiration; + + expect(actualExpiration).toBeGreaterThanOrEqual(expectedExpiration - buffer); + expect(actualExpiration).toBeLessThanOrEqual(expectedExpiration + buffer); + + expect(transaction.raw_data.data).toBeDefined(); + }); + + test( + "should create token transfer with extended expiration", + async () => { + const toolbox = context.toolbox; + const beforeTimestamp = Date.now(); + + const transaction = await toolbox.createTransaction({ + assetValue: AssetValue.from({ + asset: "TRON.USDT-TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", + value: "100", // 100 USDT + }), + expiration: extendedExpiration, + recipient: toAddress, + sender: fromAddress, + }); + + expect(transaction.raw_data.expiration).toBeDefined(); + + const expectedExpiration = beforeTimestamp + (baseExpiration + extendedExpiration) * 1000; + const actualExpiration = transaction.raw_data.expiration; + + // Allow 10 second tolerance for test execution time + expect(actualExpiration).toBeGreaterThanOrEqual(expectedExpiration - buffer); + expect(actualExpiration).toBeLessThanOrEqual(expectedExpiration + buffer); + }, + { retry: 3 }, + ); + + test( + "should create token transfer with extended expiration and memo", + async () => { + const toolbox = context.toolbox; + const beforeTimestamp = Date.now(); + + const transaction = await toolbox.createTransaction({ + assetValue: AssetValue.from({ + asset: "TRON.USDT-TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", + value: "100", // 100 USDT + }), + expiration: extendedExpiration, + memo, + recipient: toAddress, + sender: fromAddress, + }); + + const expectedExpiration = beforeTimestamp + (baseExpiration + extendedExpiration) * 1000; + const actualExpiration = transaction.raw_data.expiration; + + expect(actualExpiration).toBeGreaterThanOrEqual(expectedExpiration - buffer); + expect(actualExpiration).toBeLessThanOrEqual(expectedExpiration + buffer); + expect(transaction.raw_data).toMatchObject({ data: expect.any(String), expiration: expect.any(Number) }); + }, + { retry: 3 }, + ); +}); diff --git a/packages/toolboxes/src/tron/toolbox.ts b/packages/toolboxes/src/tron/toolbox.ts index 0abc3d514b..4d03e96230 100644 --- a/packages/toolboxes/src/tron/toolbox.ts +++ b/packages/toolboxes/src/tron/toolbox.ts @@ -258,7 +258,9 @@ export const createTronToolbox = async ( warnOnce({ condition: true, id: "tron_toolbox_get_token_metadata_failed", - warning: `Failed to get token metadata for ${contractAddress}: ${error instanceof Error ? error.message : error}`, + warning: `Failed to get token metadata for ${contractAddress}: ${ + error instanceof Error ? error.message : error + }`, }); return null; } @@ -333,29 +335,12 @@ export const createTronToolbox = async ( } }; - const transfer = async ({ recipient, assetValue, memo }: TronTransferParams) => { + const transfer = async ({ recipient, assetValue, memo, expiration }: TronTransferParams) => { if (!signer) throw new SwapKitError("toolbox_tron_no_signer"); const from = await getAddress(); tronWeb.setAddress(from); - const isNative = assetValue.isGasAsset; - - if (isNative) { - const transaction = await tronWeb.transactionBuilder.sendTrx(recipient, assetValue.getBaseValue("number"), from); - - if (memo) { - const transactionWithMemo = await tronWeb.transactionBuilder.addUpdateData(transaction, memo, "utf8"); - const signedTx = await signer.signTransaction(transactionWithMemo); - const { txid } = await tronWeb.trx.sendRawTransaction(signedTx); - return txid; - } - - const signedTx = await signer.signTransaction(transaction); - const { txid } = await tronWeb.trx.sendRawTransaction(signedTx); - return txid; - } - - const transaction = await createTransaction({ assetValue, memo, recipient, sender: from }); + const transaction = await createTransaction({ assetValue, expiration, memo, recipient, sender: from }); const signedTx = await signer.signTransaction(transaction); const { txid } = await tronWeb.trx.sendRawTransaction(signedTx); @@ -371,8 +356,8 @@ export const createTronToolbox = async ( assetValue, recipient, sender, - // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: TODO - }: TronTransferParams & { sender?: string }) => { + }: // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: TODO + TronTransferParams & { sender?: string }) => { const isNative = assetValue.isGasAsset; try { @@ -430,7 +415,9 @@ export const createTronToolbox = async ( warnOnce({ condition: true, id: "tron_toolbox_fee_estimation_failed", - warning: `Failed to calculate exact fee, using conservative estimate: ${error instanceof Error ? error.message : error}`, + warning: `Failed to calculate exact fee, using conservative estimate: ${ + error instanceof Error ? error.message : error + }`, }); throw new SwapKitError("toolbox_tron_fee_estimation_failed", { error }); @@ -441,6 +428,26 @@ export const createTronToolbox = async ( const { recipient, assetValue, memo, sender, expiration } = params; const isNative = assetValue.isGasAsset; + const addTxData = async ({ + transaction, + memo, + expiration, + }: { + transaction: TronTransaction; + memo?: string; + expiration?: number; + }) => { + const transactionWithMemo = memo + ? await tronWeb.transactionBuilder.addUpdateData(transaction, memo, "utf8") + : transaction; + + const transactionFinal = expiration + ? await tronWeb.transactionBuilder.extendExpiration(transactionWithMemo, expiration) + : transactionWithMemo; + + return transactionFinal; + }; + if (isNative) { const transaction = await tronWeb.transactionBuilder.sendTrx( recipient, @@ -448,15 +455,8 @@ export const createTronToolbox = async ( sender, ); - if (memo) { - return tronWeb.transactionBuilder.addUpdateData(transaction, memo, "utf8"); - } - - if (expiration) { - tronWeb.transactionBuilder.extendExpiration(transaction, expiration); - } - - return transaction; + const txWithData = addTxData({ expiration, memo, transaction }); + return txWithData; } tronWeb.setAddress(sender); @@ -474,7 +474,7 @@ export const createTronToolbox = async ( const options = { callValue: 0, feeLimit: calculateFeeLimit() }; - const result = await tronWeb.transactionBuilder.triggerSmartContract( + const { transaction } = await tronWeb.transactionBuilder.triggerSmartContract( contractAddress, functionSelector, options, @@ -482,15 +482,8 @@ export const createTronToolbox = async ( sender, ); - if (memo) { - return tronWeb.transactionBuilder.addUpdateData(result.transaction, memo, "utf8"); - } - - if (expiration) { - tronWeb.transactionBuilder.extendExpiration(result.transaction, expiration); - } - - return result.transaction; + const txWithData = addTxData({ expiration, memo, transaction }); + return txWithData; } catch (error) { throw new SwapKitError("toolbox_tron_transaction_creation_failed", { message: diff --git a/packages/toolboxes/src/tron/types.ts b/packages/toolboxes/src/tron/types.ts index 7c2a6a6aaf..c776171f9f 100644 --- a/packages/toolboxes/src/tron/types.ts +++ b/packages/toolboxes/src/tron/types.ts @@ -20,7 +20,9 @@ export type TronToolboxOptions = | { phrase?: string; derivationPath?: DerivationPathArray; index?: number } | {}; -export interface TronTransferParams extends GenericTransferParams {} +export interface TronTransferParams extends GenericTransferParams { + expiration?: number; +} export interface TronCreateTransactionParams extends Omit { expiration?: number;