Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8b78bb7
feat: thorchain tron support
gomesalexandre Dec 2, 2025
9aca0d9
fix: add executeTronTransaction and improve fee estimation
gomesalexandre Dec 2, 2025
19320c9
fix: simplify TronChainAdapter memo handling
gomesalexandre Dec 2, 2025
a7dc51d
fix: increase feeLimit for TRC20 with memo and use consistent contrac…
gomesalexandre Dec 2, 2025
ef93c07
fix: preserve fee_limit in TRC20 transactions with memo using txLocal
gomesalexandre Dec 2, 2025
cbc6d39
fix: remove txLocal from addUpdateData - may be causing validation is…
gomesalexandre Dec 2, 2025
c69fd5e
fix: revert to standard 100 TRX feeLimit matching SwapKit and Thorchain
gomesalexandre Dec 2, 2025
3fc2255
docs: document TRON fee estimation issues and add TODOs
gomesalexandre Dec 2, 2025
ac80cfb
Merge branch 'develop' into feat_thor_tron
gomesalexandre Dec 2, 2025
5870e6f
docs: add comprehensive Thorchain TRON integration documentation
gomesalexandre Dec 2, 2025
9947ff9
fix: use actual sender address for TRON fee estimation
gomesalexandre Dec 3, 2025
d1f14a2
Merge remote-tracking branch 'origin/develop' into feat_thor_tron
gomesalexandre Dec 3, 2025
857d335
fix: revert Number() removal - TronWeb TypeScript requires number type
gomesalexandre Dec 3, 2025
f37fd68
refactor: use bnOrZero().toNumber() instead of Number() cast
gomesalexandre Dec 3, 2025
81561ee
fix: accurate TRON fee estimation for TRC20 transfers
gomesalexandre Dec 3, 2025
45d3653
fix: use sender address for accurate TRON TRC20 energy estimation
gomesalexandre Dec 3, 2025
3b73ebb
docs: comprehensive TRON fees explainer
gomesalexandre Dec 3, 2025
49dfa23
chore: cleanup TRON fee fix - remove docs and console.warn
gomesalexandre Dec 3, 2025
9d0d9c5
chore: remove console logs from useSendDetails
gomesalexandre Dec 3, 2025
c63f0b0
Merge branch 'develop' into feat_thor_tron
NeOMakinG Dec 4, 2025
ebab87e
Merge branch 'feat_thor_tron' into fix_tron_estimates
gomesalexandre Dec 4, 2025
6b1e3ba
Merge branch 'develop' into fix_tron_estimates
gomesalexandre Dec 4, 2025
ae4f809
fix: Add chainSpecific to Tron getFeeData calls and fix memo bandwidt…
gomesalexandre Dec 4, 2025
ad6b328
Merge branch 'develop' into fix_tron_estimates
NeOMakinG Dec 5, 2025
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
80 changes: 70 additions & 10 deletions packages/chain-adapters/src/tron/TronChainAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,27 +361,87 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.TronMainnet> {
// 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<KnownChainIds.TronMainnet>,
input: GetFeeDataInput<KnownChainIds.TronMainnet>,
): Promise<FeeDataEstimate<KnownChainIds.TronMainnet>> {
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 },
},
}
Expand Down
6 changes: 6 additions & 0 deletions packages/chain-adapters/src/tron/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
1 change: 1 addition & 0 deletions packages/chain-adapters/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ type ChainSpecificGetFeeDataInput<T> = ChainSpecific<
[KnownChainIds.LitecoinMainnet]: utxo.GetFeeDataInput
[KnownChainIds.SolanaMainnet]: solana.GetFeeDataInput
[KnownChainIds.SuiMainnet]: sui.GetFeeDataInput
[KnownChainIds.TronMainnet]: tron.GetFeeDataInput
}
>
export type GetFeeDataInput<T extends ChainId> = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ export const getTradeQuote = async (
const feeData = await sellAdapter.getFeeData({
to: depositAddress,
value: sellAmount,
chainSpecific: {},
})

return { networkFeeCryptoBaseUnit: feeData.fast.txFee }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ export const getTradeRate = async (
const feeData = await sellAdapter.getFeeData({
to: depositAddress,
value: sellAmount,
chainSpecific: {},
})

return feeData.fast.txFee
Expand Down
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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
}
Expand Down
66 changes: 61 additions & 5 deletions packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
assertUnreachable,
bn,
bnOrZero,
contractAddressOrUndefined,
convertDecimalPercentageToBasisPoints,
convertPrecision,
fromBaseUnit,
Expand All @@ -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'
Expand Down Expand Up @@ -41,6 +43,7 @@ import {
getNativePrecision,
getSwapSource,
} from './index'
import * as tron from './tron'
import type {
ThorEvmTradeQuote,
ThorEvmTradeRate,
Expand Down Expand Up @@ -442,12 +445,65 @@ export const getL1RateOrQuote = async <T extends ThorTradeRateOrQuote>(
}
case CHAIN_NAMESPACE.Tron: {
const maybeRoutes = await Promise.allSettled(
perRouteValues.map((route): Promise<T> => {
perRouteValues.map(async (route): Promise<T> => {
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<ThorUtxoOrCosmosTradeRateOrQuote>({
Expand Down
3 changes: 2 additions & 1 deletion packages/unchained-client/src/tron/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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))
}
Expand Down
5 changes: 5 additions & 0 deletions src/components/Modals/Send/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ export const estimateFees = async ({
to,
value,
sendMax,
chainSpecific: {
from,
contractAddress,
memo,
},
}
return adapter.getFeeData(getFeeDataInput)
}
Expand Down
6 changes: 5 additions & 1 deletion src/hooks/queries/useApprovalFees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down