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
5 changes: 5 additions & 0 deletions .changeset/shy-walls-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@swapkit/toolboxes": minor
---

Fix Tron transaction building and expiration extension logic
143 changes: 124 additions & 19 deletions packages/toolboxes/src/tron/__tests__/toolbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand Down Expand Up @@ -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) {
Expand All @@ -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,
Expand All @@ -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);

Expand All @@ -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);
Expand All @@ -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 },
);
});
77 changes: 35 additions & 42 deletions packages/toolboxes/src/tron/toolbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand All @@ -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 {
Expand Down Expand Up @@ -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 });
Expand All @@ -441,22 +428,35 @@ 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,
assetValue.getBaseValue("number"),
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);
Expand All @@ -474,23 +474,16 @@ 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,
parameter,
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:
Expand Down
4 changes: 3 additions & 1 deletion packages/toolboxes/src/tron/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<GenericCreateTransactionParams, "feeRate"> {
expiration?: number;
Expand Down