Created
October 10, 2025 09:06
-
-
Save sedhu-orbitx/0dab8307e2d18776760be182bb27dcf6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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