diff --git a/packages/chain-adapters/src/tron/TronChainAdapter.ts b/packages/chain-adapters/src/tron/TronChainAdapter.ts index 6de33396b9e..a21fb2a6545 100644 --- a/packages/chain-adapters/src/tron/TronChainAdapter.ts +++ b/packages/chain-adapters/src/tron/TronChainAdapter.ts @@ -361,27 +361,87 @@ export class ChainAdapter implements IChainAdapter { // This causes UI to show wrong fees and transactions to fail on-chain // See TRON_FEE_ESTIMATION_ISSUES.md for detailed analysis and fix async getFeeData( - _input: GetFeeDataInput, + input: GetFeeDataInput, ): Promise> { try { - // TODO: Use _input.chainSpecific.contractAddress to detect TRC20 - // TODO: Call estimateTRC20TransferFee() for TRC20 tokens - // TODO: Build actual transaction with memo to get accurate bandwidth - // TODO: Add 1 TRX memo fee if _input.chainSpecific.memo present - const { fast, average, slow, estimatedBandwidth } = - await this.providers.http.getPriorityFees() + const { to, value, chainSpecific: { from, contractAddress, memo } = {} } = input + + // Get live network prices from chain parameters + const tronWeb = new TronWeb({ fullHost: this.rpcUrl }) + const params = await tronWeb.trx.getChainParameters() + const bandwidthPrice = params.find(p => p.key === 'getTransactionFee')?.value ?? 1000 + const energyPrice = params.find(p => p.key === 'getEnergyFee')?.value ?? 100 + + let energyFee = 0 + let bandwidthFee = 0 + + if (contractAddress) { + // TRC20: Estimate energy using existing method + try { + // Use sender address if available, otherwise use recipient for estimation + const estimationFrom = from || to + const energyEstimate = await this.providers.http.estimateTRC20TransferFee({ + contractAddress, + from: estimationFrom, + to, + amount: value, + }) + energyFee = Number(energyEstimate) + + // Apply 1.5x safety margin for dynamic energy spikes + energyFee = Math.ceil(energyFee * 1.5) + } catch (err) { + // Fallback: Conservative estimate for new address (130k energy) + energyFee = 130000 * energyPrice + } + + // TRC20 transfers use ~276 bytes bandwidth + bandwidthFee = 276 * bandwidthPrice + } else { + // TRX transfer: Build actual transaction to get precise bandwidth + try { + const baseTx = await tronWeb.transactionBuilder.sendTrx( + to, + Number(value), + to, // Use recipient as sender for estimation + ) + + // Add memo if provided to get accurate size + const finalTx = memo + ? await tronWeb.transactionBuilder.addUpdateData(baseTx, memo, 'utf8') + : baseTx + + // Calculate bandwidth from actual transaction size + const rawDataBytes = finalTx.raw_data_hex ? finalTx.raw_data_hex.length / 2 : 133 + const signatureBytes = 65 + const totalBytes = rawDataBytes + signatureBytes + + bandwidthFee = totalBytes * bandwidthPrice + } catch (err) { + // Fallback bandwidth estimate: Base tx + memo bytes + const baseBytes = 198 + const memoBytes = memo ? Buffer.from(memo, 'utf8').length : 0 + const totalBytes = baseBytes + memoBytes + bandwidthFee = totalBytes * bandwidthPrice + } + } + + const totalFee = energyFee + bandwidthFee + + // Calculate bandwidth for display + const estimatedBandwidth = String(Math.ceil(bandwidthFee / bandwidthPrice)) return { fast: { - txFee: fast, + txFee: String(totalFee), chainSpecific: { bandwidth: estimatedBandwidth }, }, average: { - txFee: average, + txFee: String(totalFee), chainSpecific: { bandwidth: estimatedBandwidth }, }, slow: { - txFee: slow, + txFee: String(totalFee), chainSpecific: { bandwidth: estimatedBandwidth }, }, } diff --git a/packages/chain-adapters/src/tron/types.ts b/packages/chain-adapters/src/tron/types.ts index 5f65240452a..d8a6e49693e 100644 --- a/packages/chain-adapters/src/tron/types.ts +++ b/packages/chain-adapters/src/tron/types.ts @@ -21,6 +21,12 @@ export type BuildTxInput = { memo?: string } +export type GetFeeDataInput = { + from?: string + contractAddress?: string + memo?: string +} + export interface TronUnsignedTx { txID: string raw_data: { diff --git a/packages/chain-adapters/src/types.ts b/packages/chain-adapters/src/types.ts index 11ed60df8e4..cce86042649 100644 --- a/packages/chain-adapters/src/types.ts +++ b/packages/chain-adapters/src/types.ts @@ -320,6 +320,7 @@ type ChainSpecificGetFeeDataInput = ChainSpecific< [KnownChainIds.LitecoinMainnet]: utxo.GetFeeDataInput [KnownChainIds.SolanaMainnet]: solana.GetFeeDataInput [KnownChainIds.SuiMainnet]: sui.GetFeeDataInput + [KnownChainIds.TronMainnet]: tron.GetFeeDataInput } > export type GetFeeDataInput = { diff --git a/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeQuote.ts index 5c909d3192c..bb3f199ebbf 100644 --- a/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeQuote.ts @@ -222,6 +222,7 @@ export const getTradeQuote = async ( const feeData = await sellAdapter.getFeeData({ to: depositAddress, value: sellAmount, + chainSpecific: {}, }) return { networkFeeCryptoBaseUnit: feeData.fast.txFee } diff --git a/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeRate.ts b/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeRate.ts index ae61693a880..a0a49537772 100644 --- a/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeRate.ts +++ b/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeRate.ts @@ -227,6 +227,7 @@ export const getTradeRate = async ( const feeData = await sellAdapter.getFeeData({ to: depositAddress, value: sellAmount, + chainSpecific: {}, }) return feeData.fast.txFee diff --git a/packages/swapper/src/swappers/SunioSwapper/utils/getQuoteOrRate.ts b/packages/swapper/src/swappers/SunioSwapper/utils/getQuoteOrRate.ts index 42d9b53176b..fe2fac23059 100644 --- a/packages/swapper/src/swappers/SunioSwapper/utils/getQuoteOrRate.ts +++ b/packages/swapper/src/swappers/SunioSwapper/utils/getQuoteOrRate.ts @@ -1,5 +1,5 @@ import { tronChainId } from '@shapeshiftoss/caip' -import { bn } from '@shapeshiftoss/utils' +import { bn, contractAddressOrUndefined } from '@shapeshiftoss/utils' import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' @@ -118,9 +118,13 @@ export async function getQuoteOrRate( const adapter = assertGetTronChainAdapter(sellAsset.chainId) const feeData = await adapter.getFeeData({ - to: receiveAddress, + to: SUNIO_SMART_ROUTER_CONTRACT, value: '0', sendMax: false, + chainSpecific: { + from: receiveAddress, + contractAddress: contractAddressOrUndefined(sellAsset.assetId), + }, }) networkFeeCryptoBaseUnit = feeData.fast.txFee } diff --git a/packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts b/packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts index 52c0bc88683..8a430b3cc0d 100644 --- a/packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts +++ b/packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts @@ -5,6 +5,7 @@ import { assertUnreachable, bn, bnOrZero, + contractAddressOrUndefined, convertDecimalPercentageToBasisPoints, convertPrecision, fromBaseUnit, @@ -14,6 +15,7 @@ import { } from '@shapeshiftoss/utils' import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' +import { TronWeb } from 'tronweb' import { v4 as uuid } from 'uuid' import { getDefaultSlippageDecimalPercentageForSwapper } from '../index' @@ -41,6 +43,7 @@ import { getNativePrecision, getSwapSource, } from './index' +import * as tron from './tron' import type { ThorEvmTradeQuote, ThorEvmTradeRate, @@ -442,12 +445,65 @@ export const getL1RateOrQuote = async ( } case CHAIN_NAMESPACE.Tron: { const maybeRoutes = await Promise.allSettled( - perRouteValues.map((route): Promise => { + perRouteValues.map(async (route): Promise => { const memo = getMemo(route) - - // For rate quotes (no wallet), we can't calculate fees - // Actual fees will be calculated in getTronTransactionFees when executing - const networkFeeCryptoBaseUnit = undefined + let networkFeeCryptoBaseUnit: string | undefined = undefined + + // Calculate fees for rates when we have a receive address (wallet connected) + if (input.quoteOrRate === 'rate' && input.receiveAddress) { + try { + const { sellAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit } = input + const contractAddress = contractAddressOrUndefined(sellAsset.assetId) + + // Get vault address + const { vault } = await tron.getThorTxData({ sellAsset, config, swapperName }) + + // Estimate fees using the receive address for accurate energy calculation + const tronWeb = new TronWeb({ fullHost: deps.config.VITE_TRON_NODE_URL }) + const params = await tronWeb.trx.getChainParameters() + const bandwidthPrice = params.find(p => p.key === 'getTransactionFee')?.value ?? 1000 + const energyPrice = params.find(p => p.key === 'getEnergyFee')?.value ?? 100 + + let totalFee = 0 + + if (contractAddress) { + // TRC20: Estimate energy with actual recipient + try { + const result = await tronWeb.transactionBuilder.triggerConstantContract( + contractAddress, + 'transfer(address,uint256)', + {}, + [ + { type: 'address', value: vault }, // Use vault as recipient + { type: 'uint256', value: sellAmountIncludingProtocolFeesCryptoBaseUnit }, + ], + input.receiveAddress, // Use user's address as sender for estimation + ) + + const energyUsed = result.energy_used ?? 65000 + const energyFee = energyUsed * energyPrice * 1.5 // 1.5x safety margin + const bandwidthFee = 276 * bandwidthPrice // TRC20 bandwidth + totalFee = Math.ceil(energyFee + bandwidthFee) + } catch { + // Fallback: Conservative estimate + totalFee = 13_000_000 // 13 TRX worst case + } + } else { + // TRX transfer bandwidth: Base tx + memo bytes + const baseBytes = 198 + const memoBytes = route.quote.memo + ? Buffer.from(route.quote.memo, 'utf8').length + : 0 + const totalBandwidth = baseBytes + memoBytes + totalFee = totalBandwidth * bandwidthPrice + } + + networkFeeCryptoBaseUnit = String(totalFee) + } catch { + // Leave as undefined if estimation fails + } + } + // For quotes, fees will be calculated in getTronTransactionFees when executing return Promise.resolve( makeThorTradeRateOrQuote({ diff --git a/packages/unchained-client/src/tron/api.ts b/packages/unchained-client/src/tron/api.ts index 152d58e88bc..8f51b561b78 100644 --- a/packages/unchained-client/src/tron/api.ts +++ b/packages/unchained-client/src/tron/api.ts @@ -240,7 +240,8 @@ export class TronApi { return String(feeInSun) } catch (_err) { - return '31000000' + // Fallback: Worst case 130k energy at current 100 sun/energy + return '13000000' // 13 TRX (more realistic than 31 TRX) } } diff --git a/src/components/Modals/Send/hooks/useSendDetails/useSendDetails.tsx b/src/components/Modals/Send/hooks/useSendDetails/useSendDetails.tsx index 862364673b3..30536072fed 100644 --- a/src/components/Modals/Send/hooks/useSendDetails/useSendDetails.tsx +++ b/src/components/Modals/Send/hooks/useSendDetails/useSendDetails.tsx @@ -1,5 +1,5 @@ import type { ChainId } from '@shapeshiftoss/caip' -import { solAssetId } from '@shapeshiftoss/caip' +import { fromAccountId, solAssetId } from '@shapeshiftoss/caip' import type { FeeDataEstimate } from '@shapeshiftoss/chain-adapters' import { ChainAdapterError, solana } from '@shapeshiftoss/chain-adapters' import { contractAddressOrUndefined } from '@shapeshiftoss/utils' @@ -108,9 +108,12 @@ export const useSendDetails = (): UseSendDetailsReturnType => { if (!accountId) throw new Error('No accountId found') if (!wallet) throw new Error('No wallet connected') + const { account: from } = fromAccountId(accountId) + return estimateFees({ amountCryptoPrecision, assetId, + from, to, sendMax, accountId, @@ -193,8 +196,6 @@ export const useSendDetails = (): UseSendDetailsReturnType => { return estimatedFees } catch (e: unknown) { - console.debug(e) - if (e instanceof ChainAdapterError) { throw new Error(translate(e.metadata.translation, e.metadata.options)) } diff --git a/src/components/Modals/Send/utils.ts b/src/components/Modals/Send/utils.ts index 823bc9452bf..ee3f9729dbf 100644 --- a/src/components/Modals/Send/utils.ts +++ b/src/components/Modals/Send/utils.ts @@ -133,6 +133,11 @@ export const estimateFees = async ({ to, value, sendMax, + chainSpecific: { + from, + contractAddress, + memo, + }, } return adapter.getFeeData(getFeeDataInput) } diff --git a/src/hooks/queries/useApprovalFees.ts b/src/hooks/queries/useApprovalFees.ts index 606a6573b76..1ff4b49a243 100644 --- a/src/hooks/queries/useApprovalFees.ts +++ b/src/hooks/queries/useApprovalFees.ts @@ -69,9 +69,13 @@ export const useApprovalFees = ({ // Estimate fees for approval transaction const feeData = await adapter.getFeeData({ - to, + to: spender, value: '0', sendMax: false, + chainSpecific: { + from, + contractAddress: to, + }, }) return {