Skip to content

Instantly share code, notes, and snippets.

@sedhu-orbitx
Created October 10, 2025 09:06
Show Gist options
  • Select an option

  • Save sedhu-orbitx/0dab8307e2d18776760be182bb27dcf6 to your computer and use it in GitHub Desktop.

Select an option

Save sedhu-orbitx/0dab8307e2d18776760be182bb27dcf6 to your computer and use it in GitHub Desktop.
import { Options } from "@layerzerolabs/lz-v2-utilities";
import { ethers, parseUnits, zeroPadValue } from "ethers";
import { TronWeb } from "tronweb";
import {
MessagingFee,
OFT_ABI,
SendParam
} from "./abis";
import {
LAYERZERO_EIDS,
RPC_URLS,
USDT0_CONTRACTS,
USDT_CONTRACTS,
USDT_DECIMALS
} from "./constants";
// Helper function to calculate minimum amount with slippage
function calculateMinAmount(amountLD: bigint, slippageBps: number): bigint {
if (slippageBps >= 10000) {
throw new Error("Slippage cannot be 100% or more");
}
const slippageAmount = (amountLD * BigInt(slippageBps)) / 10000n;
return amountLD - slippageAmount;
}
// Helper function to initialize TronWeb with debug logging
function initializeTronWeb(privateKey: string, tronRpcUrl?: string): TronWeb {
console.log("πŸ”§ Initializing TronWeb...");
const tronWeb = new TronWeb({
fullHost: tronRpcUrl || RPC_URLS.TRON,
privateKey: privateKey
});
// Enable debug logging if available
// if (tronWeb.setDebug) {
// tronWeb.setDebug(true);
// console.log(" Debug logging enabled for TronWeb");
// }
console.log(` TronWeb initialized with RPC: ${tronWeb.fullNode.host}`);
console.log(` Wallet address: ${tronWeb.address.fromPrivateKey(privateKey)}`);
return tronWeb;
}
// Helper function to encode compose message for multi-hop transfer
function encodeComposeMessage(hopSendParams: SendParam): string {
return ethers.AbiCoder.defaultAbiCoder().encode(
['tuple(uint32,bytes32,uint256,uint256,bytes,bytes,bytes)'],
[[
hopSendParams.dstEid,
hopSendParams.to,
hopSendParams.amountLD,
hopSendParams.minAmountLD,
hopSendParams.extraOptions,
hopSendParams.composeMsg,
hopSendParams.oftCmd
]]
);
}
// Helper function to build LayerZero transfer options
function buildTransferOptions(
recipient: string,
hopChainFee: MessagingFee,
options?: {
nativeDropAmount?: string;
composeGasLimit?: number;
lzReceiveGasLimit?: number;
}
): string {
console.log("πŸ”§ Building LayerZero transfer options...");
try {
const builder = Options.newOptions();
// Add lzReceive option with gas limit
const lzReceiveGasLimit = options?.lzReceiveGasLimit || 200_000;
builder.addExecutorLzReceiveOption(lzReceiveGasLimit, 0);
console.log(` Added lzReceive option: ${lzReceiveGasLimit} gas limit`);
// Add compose option for multi-hop with hop chain fee
const composeGasLimit = options?.composeGasLimit || 500_000;
const composeValue = Number(hopChainFee.nativeFee * 11n / 10n); // 10% buffer
builder.addExecutorComposeOption(0, composeGasLimit, composeValue);
console.log(` Added compose option: ${composeGasLimit} gas, ${composeValue} value`);
// Add native drop option if specified
if (options?.nativeDropAmount && options.nativeDropAmount !== "0") {
const nativeDropWei = parseUnits(options.nativeDropAmount, 18);
const recipientBytes32 = zeroPadValue(recipient, 32);
builder.addExecutorNativeDropOption(nativeDropWei, recipientBytes32);
console.log(` Added native drop: ${options.nativeDropAmount} ETH to ${recipient}`);
}
const optionsHex = builder.toHex();
console.log(` Final options: ${optionsHex}`);
return optionsHex;
} catch (error) {
console.warn("Failed to create LayerZero options with v2 utilities, using fallback:", error);
return "0x";
}
}
async function getHopChainQuote(recipient: string, amount: bigint): Promise<MessagingFee> {
console.log("πŸ“Š Getting hop chain quote (Arbitrum β†’ Polygon)...");
try {
const arbitrumProvider = new ethers.JsonRpcProvider(RPC_URLS.ARBITRUM);
// arbitrumProvider.on('debug', (message: string) => {
// console.log(JSON.stringify(message, null, 2));
// });
const arbitrumUsdt0Contract = new ethers.Contract(
USDT0_CONTRACTS.ARBITRUM.UsdtOFT,
OFT_ABI, // Use standard OFT ABI for Arbitrum USDT0 contract
arbitrumProvider
);
// Build hop send parameters
const hopSendParams: SendParam = {
dstEid: LAYERZERO_EIDS.POLYGON,
to: zeroPadValue(recipient, 32), // Placeholder
amountLD: amount, // Will be updated with actual amount
minAmountLD: 0n,
extraOptions: "0x",
composeMsg: "0x",
oftCmd: "0x"
};
const quoteData = await arbitrumUsdt0Contract.quoteSend(hopSendParams, false);
const hopFee: MessagingFee = {
nativeFee: quoteData.nativeFee,
lzTokenFee: quoteData.lzTokenFee
};
console.log(` Hop chain fee: ${ethers.formatEther(hopFee.nativeFee)} ETH`);
return hopFee;
} catch (error) {
console.warn("Hop chain quote error, using fallback fee:", error);
// Fallback fee: 0.0001 ETH
const fallbackFee: MessagingFee = {
nativeFee: BigInt(1e14), // 0.0001 ETH
lzTokenFee: 0n
};
console.log(` Using fallback fee: ${ethers.formatEther(fallbackFee.nativeFee)} ETH`);
return fallbackFee;
}
}
// Helper function to check USDT balance on Tron
async function getTronUSDTBalance(tronWeb: TronWeb, walletAddress: string): Promise<bigint> {
console.log("πŸ’° Checking USDT balance on Tron...");
try {
const contract = await tronWeb.contract().at(USDT_CONTRACTS.TRON);
const balance = await contract.balanceOf(walletAddress).call();
const balanceBigInt = BigInt(balance.toString());
console.log(` USDT Balance: ${ethers.formatUnits(balanceBigInt, USDT_DECIMALS)} USDT`);
return balanceBigInt;
} catch (error) {
throw new Error(`Failed to get USDT balance on Tron: ${error}`);
}
}
// Helper function to check USDT allowance
async function checkTronUSDTAllowance(tronWeb: TronWeb, walletAddress: string): Promise<bigint> {
console.log("πŸ” Checking USDT allowance...");
try {
const contract = await tronWeb.contract().at(USDT_CONTRACTS.TRON);
const allowance = await contract.allowance(
walletAddress,
USDT0_CONTRACTS.TRON.UsdtOFT
).call();
const allowanceBigInt = BigInt(allowance.toString());
console.log(` USDT Allowance: ${ethers.formatUnits(allowanceBigInt, USDT_DECIMALS)} USDT`);
return allowanceBigInt;
} catch (error) {
throw new Error(`Failed to check USDT allowance on Tron: ${error}`);
}
}
// Helper function to approve USDT spending
async function approveTronUSDT(tronWeb: TronWeb, amount: bigint): Promise<any> {
console.log(`πŸ” Approving ${ethers.formatUnits(amount, USDT_DECIMALS)} USDT for Legacy Mesh contract...`);
try {
const contract = await tronWeb.contract().at(USDT_CONTRACTS.TRON);
const tx = await contract.approve(
USDT0_CONTRACTS.TRON.UsdtOFT,
amount.toString()
).send({
feeLimit: 100_000_000, // 100 TRX fee limit
callValue: 0,
shouldPollResponse: true
});
console.log(` Approval transaction hash: ${tx.txid}`);
console.log(` Energy used: ${tx.energy_used}`);
console.log(` Bandwidth used: ${tx.bandwidth_used}`);
console.log(" USDT approval confirmed on Tron");
return tx;
} catch (error) {
throw new Error(`Failed to approve USDT on Tron: ${error}`);
}
}
/**
* Send USDT from Tron to Polygon via USDT0 Legacy Mesh
*
* @param privateKey - Private key for Tron wallet
* @param amount - Amount to transfer (in USDT, e.g., "10.5")
* @param recipient - Recipient address on Polygon
* @param options - Optional transfer configuration
* @returns Promise with transaction result and monitoring links
*/
async function sendUSDTFromTronToPolygon(
privateKey: string,
amount: string,
recipient: string,
options?: {
slippageBps?: number; // Default: 50 (0.5%)
nativeDropAmount?: string; // Default: undefined (disabled)
composeGasLimit?: number; // Default: 500_000
lzReceiveGasLimit?: number; // Default: 200_000
tronRpcUrl?: string; // Default: RPC_URLS.TRON
}
) {
console.log(`πŸš€ Starting Tron to Polygon USDT transfer via Legacy Mesh`);
console.log(` Amount: ${amount} USDT`);
console.log(` Recipient: ${recipient}`);
console.log(` Architecture: Tron (USDT) β†’ Arbitrum (USDT0 Hub) β†’ Polygon (USDT0)\n`);
// Initialize TronWeb
const tronWeb = initializeTronWeb(privateKey, options?.tronRpcUrl);
const walletAddress = tronWeb.address.fromPrivateKey(privateKey);
if (!walletAddress) {
throw new Error("Invalid private key");
}
// Convert amount to wei
const amountWei = parseUnits(amount, USDT_DECIMALS);
const slippageBps = options?.slippageBps || 50; // Default 0.5%
// Check USDT balance
const usdtBalance = await getTronUSDTBalance(tronWeb, walletAddress);
if (usdtBalance < amountWei) {
throw new Error(`Insufficient USDT balance. Required: ${amount} USDT, Available: ${ethers.formatUnits(usdtBalance, USDT_DECIMALS)} USDT`);
}
// Check and approve USDT allowance
const allowance = await checkTronUSDTAllowance(tronWeb, walletAddress);
if (allowance < amountWei) {
console.log(" Insufficient USDT allowance, requesting approval...");
await approveTronUSDT(tronWeb, amountWei);
} else {
console.log(" Sufficient USDT allowance already exists");
}
// Get hop chain quote (Arbitrum β†’ Polygon)
const hopChainFee = await getHopChainQuote(recipient, amountWei);
// Build hop chain send parameters
const hopSendParams: SendParam = {
dstEid: LAYERZERO_EIDS.POLYGON,
to: zeroPadValue(recipient, 32),
amountLD: amountWei,
minAmountLD: 0n, // Will be updated after quote
extraOptions: "0x",
composeMsg: "0x",
oftCmd: "0x"
};
// Encode compose message
const composeMsg = encodeComposeMessage(hopSendParams);
console.log(`πŸ“¦ Composed message encoded: ${composeMsg}`);
// Build transfer options
const extraOptions = buildTransferOptions(recipient, hopChainFee, {
nativeDropAmount: options?.nativeDropAmount,
composeGasLimit: options?.composeGasLimit,
lzReceiveGasLimit: options?.lzReceiveGasLimit
});
const tronUsdt0ContractAddress = 'TFG4wBaDQ8sHWWP1ACeSGnoNR6RRzevLPt';
// Initialize Tron contract
const tronUsdt0Contract = await tronWeb.contract().at(tronUsdt0ContractAddress);
// Step 1: Create initial sendParam for quote
const initialSendParam = [
LAYERZERO_EIDS.ARBITRUM, // Destination: Arbitrum (hub)
zeroPadValue(USDT0_CONTRACTS.ARBITRUM.MultiHopComposer, 32), // Send to MultiHopComposer
amountWei,
0, // minAmountLD = 0 for initial quote
extraOptions,
composeMsg,
"0x" // oftCmd = "0x"
];
console.log("πŸ“Š Getting initial transfer quote...");
console.log(` Destination EID: ${LAYERZERO_EIDS.ARBITRUM} (Arbitrum)`);
console.log(` MultiHopComposer: ${USDT0_CONTRACTS.ARBITRUM.MultiHopComposer}`);
// Step 2: Call quoteOFT to get oftReceipt
const [, , oftReceipt] = await tronUsdt0Contract.quoteOFT(initialSendParam).call();
// Step 3: Calculate slippage-adjusted minAmountLD
const minAmountLD = calculateMinAmount(oftReceipt[1], slippageBps);
console.log(`πŸ“Š Transfer Details:`);
console.log(` Amount Sent: ${amount} USDT (${amountWei.toString()} wei)`);
console.log(` Amount Received: ${ethers.formatUnits(oftReceipt[1], USDT_DECIMALS)} USDT0`);
console.log(` Min Amount: ${ethers.formatUnits(minAmountLD, USDT_DECIMALS)} USDT0 (${minAmountLD.toString()} wei)`);
console.log(` Slippage: ${slippageBps / 100}%`);
console.log(` Native Drop: ${options?.nativeDropAmount || 'disabled'}`);
// Step 4: Update sendParam with minAmountLD
const finalSendParam = [
LAYERZERO_EIDS.ARBITRUM,
zeroPadValue(USDT0_CONTRACTS.ARBITRUM.MultiHopComposer, 32),
amountWei,
minAmountLD,
extraOptions,
composeMsg,
"0x"
];
// Step 5: Call quoteSend to get final msgFee
console.log("πŸ’° Getting final messaging fee...");
const msgFeeResult = await tronUsdt0Contract.quoteSend(finalSendParam, false).call();
// Simplified fee handling - TronWeb returns arrays directly
const msgFee = [
msgFeeResult[0].toString(), // nativeFee
msgFeeResult[1].toString() // lzTokenFee
];
console.log(` Native Fee: ${ethers.formatUnits(msgFee[0], 6)} TRX`); // TRX has 6 decimals
console.log(` LZ Token Fee: ${ethers.formatUnits(msgFee[1], 6)} LZ`);
// Warning about insufficient message value
console.log(`⚠️ IMPORTANT: Ensure sufficient TRX balance for native fee payment`);
console.log(` If transaction gets stuck on Arbitrum, use handleStuckTransaction() for retry/refund`);
// Step 6: Execute send() transaction
console.log(`πŸš€ Executing composed transfer...`);
const tx = await tronUsdt0Contract.send(
finalSendParam,
msgFee,
walletAddress // refund address
).send({
feeLimit: 200_000_000, // 200 TRX fee limit for complex transaction
callValue: msgFee[0], // Add native fee payment
shouldPollResponse: true
});
console.log(`βœ… Transfer initiated!`);
console.log(` Transaction Hash: ${tx.txid}`);
console.log(` Block Number: ${tx.blockNumber}`);
console.log(` Energy Used: ${tx.energy_used}`);
console.log(` Bandwidth Used: ${tx.bandwidth_used}`);
// Generate monitoring links
const monitoring = {
tronExplorer: `https://tronscan.org/#/transaction/${tx.txid}`,
layerZeroExplorer: `https://layerzeroscan.com/tx/${tx.txid}`,
polygonExplorer: `https://polygonscan.com/address/${recipient}`,
arbitrumExplorer: `https://arbiscan.io/address/${USDT0_CONTRACTS.ARBITRUM.MultiHopComposer}`
};
console.log(`πŸ” Monitoring Links:`);
console.log(` Tron Explorer: ${monitoring.tronExplorer}`);
console.log(` LayerZero Explorer: ${monitoring.layerZeroExplorer}`);
console.log(` Polygon Explorer: ${monitoring.polygonExplorer}`);
console.log(` Arbitrum MultiHopComposer: ${monitoring.arbitrumExplorer}`);
return {
tronTx: tx,
monitoring,
transferDetails: {
amountSent: amount,
amountReceived: ethers.formatUnits(oftReceipt[1], USDT_DECIMALS),
minAmount: ethers.formatUnits(minAmountLD, USDT_DECIMALS),
slippageBps,
nativeFee: ethers.formatUnits(msgFee[0], 6), // TRX has 6 decimals
lzTokenFee: ethers.formatUnits(msgFee[1], 6) // LZ token also has 6 decimals
}
};
}
// Example usage function
async function main() {
try {
if (!process.env.TRON_PRIVATE_KEY) {
throw new Error("TRON_PRIVATE_KEY must be set in environment variables");
}
// Transfer USDT from Tron to Polygon with configurable options
await sendUSDTFromTronToPolygon(
process.env.TRON_PRIVATE_KEY,
"0.1", // Amount: 0.1 USDT
"0x3C351B147B578E65078626eCfa4606dDC6990318", // Recipient on Polygon
{
slippageBps: 50, // 0.5% slippage protection
nativeDropAmount: "0", // Drop 0.00001 ETH to recipient on Polygon
lzReceiveGasLimit: 200_000, // Gas for lzReceive
composeGasLimit: 500_000, // Gas for compose
tronRpcUrl: 'https://api.trongrid.io' // Use default Tron RPC
}
);
} catch (error) {
console.error("Transfer failed:", error);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment