Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 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
646b480
feat: add TRON gas estimates for Near Intents swapper
gomesalexandre Dec 4, 2025
d2b888b
fix: remove SVG module script MIME type error in splashscreen
gomesalexandre Dec 4, 2025
a516022
debug: add comprehensive TRON transaction logging
gomesalexandre Dec 4, 2025
f002da8
debug: add TRON account balance check before transaction
gomesalexandre Dec 4, 2025
99a75fe
debug: JSON.stringify all console.log objects
gomesalexandre Dec 4, 2025
bb3be02
fix: use actual sender address for TRON bandwidth estimation
gomesalexandre Dec 4, 2025
0af2671
debug: check if recipient address needs activation (1 TRX cost)
gomesalexandre Dec 4, 2025
63ecbea
fix: include 1 TRX account activation fee in TRON gas estimates
gomesalexandre Dec 4, 2025
4f59e9f
feat: add TRON gas estimation for Sun.io swapper
gomesalexandre Dec 4, 2025
1138892
feat: add TRON support to Relay swapper quote validation
gomesalexandre Dec 4, 2025
7d32639
fix: properly estimate Sun.io TRX swap gas costs
gomesalexandre Dec 4, 2025
618fc0b
fix: use 2k energy estimate for Sun.io TRX swaps
gomesalexandre Dec 4, 2025
a8c0090
fix: use 2k energy estimate for Sun.io TRC-20 swaps
gomesalexandre Dec 4, 2025
b5f8142
fix: increase Sun.io bandwidth estimate to 1100 bytes
gomesalexandre Dec 4, 2025
a8568e2
chore: remove debug logging from TRON implementations
gomesalexandre Dec 4, 2025
0b52f51
Revert "fix: remove SVG module script MIME type error in splashscreen"
gomesalexandre Dec 4, 2025
bdf495c
chore: merge develop into feat_tron_all_swapper_estimates
gomesalexandre Dec 5, 2025
6d24430
fix: account activation fee should apply to all Sun.io swaps
gomesalexandre Dec 5, 2025
791ab22
Merge branch 'develop' into feat_tron_all_swapper_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
50 changes: 38 additions & 12 deletions packages/chain-adapters/src/tron/TronChainAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,18 +219,24 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.TronMainnet> {

txData = txData.transaction
} else {
const requestBody = {
owner_address: from,
to_address: to,
amount: Number(value),
visible: true,
}

const response = await fetch(`${this.rpcUrl}/wallet/createtransaction`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
owner_address: from,
to_address: to,
amount: Number(value),
visible: true,
}),
body: JSON.stringify(requestBody),
})

txData = await response.json()

if (txData.Error) {
throw new Error(`TronGrid API error: ${txData.Error}`)
}
}

// Add memo if provided
Expand Down Expand Up @@ -400,11 +406,9 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.TronMainnet> {
} 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
)
// Use actual sender if available, otherwise use recipient for estimation
const estimationFrom = from || to
const baseTx = await tronWeb.transactionBuilder.sendTrx(to, Number(value), estimationFrom)

// Add memo if provided to get accurate size
const finalTx = memo
Expand All @@ -426,7 +430,29 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.TronMainnet> {
}
}

const totalFee = energyFee + bandwidthFee
// Check if recipient address needs activation (1 TRX cost)
let accountActivationFee = 0
try {
const recipientInfoResponse = await fetch(`${this.rpcUrl}/wallet/getaccount`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
address: to,
visible: true,
}),
})
const recipientInfo = await recipientInfoResponse.json()
const recipientExists = recipientInfo && Object.keys(recipientInfo).length > 1

// If recipient doesn't exist, add 1 TRX activation fee
if (!recipientExists && !contractAddress) {
accountActivationFee = 1_000_000 // 1 TRX = 1,000,000 sun
}
} catch (err) {
// Don't fail on this check - continue with 0 activation fee
}

const totalFee = energyFee + bandwidthFee + accountActivationFee

// Calculate bandwidth for display
const estimatedBandwidth = String(Math.ceil(bandwidthFee / bandwidthPrice))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,14 @@ export const getTradeQuote = async (

case CHAIN_NAMESPACE.Tron: {
const sellAdapter = deps.assertGetTronChainAdapter(sellAsset.chainId)
const contractAddress = contractAddressOrUndefined(sellAsset.assetId)
const feeData = await sellAdapter.getFeeData({
to: depositAddress,
value: sellAmount,
chainSpecific: {},
chainSpecific: {
from: sendAddress,
contractAddress,
},
})

return { networkFeeCryptoBaseUnit: feeData.fast.txFee }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,14 @@ export const getTradeRate = async (
case CHAIN_NAMESPACE.Tron: {
try {
const sellAdapter = deps.assertGetTronChainAdapter(sellAsset.chainId)
const contractAddress = contractAddressOrUndefined(sellAsset.assetId)
const feeData = await sellAdapter.getFeeData({
to: depositAddress,
value: sellAmount,
chainSpecific: {},
chainSpecific: {
from: sendAddress,
contractAddress,
},
})

return feeData.fast.txFee
Expand Down
12 changes: 12 additions & 0 deletions packages/swapper/src/swappers/RelaySwapper/utils/getTrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
isRelayError,
isRelayQuoteEvmItemData,
isRelayQuoteSolanaItemData,
isRelayQuoteTronItemData,
isRelayQuoteUtxoItemData,
} from '../utils/types'
import { fetchRelayTrade } from './fetchRelayTrade'
Expand Down Expand Up @@ -625,6 +626,17 @@ export async function getTrade<T extends 'quote' | 'rate'>({
}
}

if (isRelayQuoteTronItemData(selectedItem.data)) {
return {
allowanceContract: '',
solanaTransactionMetadata: undefined,
relayTransactionMetadata: {
relayId: quote.steps[0].requestId,
to: selectedItem.data?.parameter?.contract_address,
},
}
}

throw new Error('Relay quote step contains no data')
})()

Expand Down
43 changes: 39 additions & 4 deletions packages/swapper/src/swappers/RelaySwapper/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,21 @@ export type RelayQuoteSolanaItemData = {
addressLookupTableAddresses: string[]
}

export type RelayQuoteTronItemData = {
type?: string
parameter?: {
owner_address?: string
contract_address?: string
data?: string
}
}

export type RelayQuoteItem = {
data?: RelayQuoteEvmItemData | RelayQuoteUtxoItemData | RelayQuoteSolanaItemData
data?:
| RelayQuoteEvmItemData
| RelayQuoteUtxoItemData
| RelayQuoteSolanaItemData
| RelayQuoteTronItemData
}

export type RelayQuoteStep = {
Expand All @@ -140,23 +153,45 @@ export type RelayQuote = {
}

export const isRelayQuoteUtxoItemData = (
item: RelayQuoteUtxoItemData | RelayQuoteEvmItemData | RelayQuoteSolanaItemData,
item:
| RelayQuoteUtxoItemData
| RelayQuoteEvmItemData
| RelayQuoteSolanaItemData
| RelayQuoteTronItemData,
): item is RelayQuoteUtxoItemData => {
return 'psbt' in item
}

export const isRelayQuoteEvmItemData = (
item: RelayQuoteUtxoItemData | RelayQuoteEvmItemData | RelayQuoteSolanaItemData,
item:
| RelayQuoteUtxoItemData
| RelayQuoteEvmItemData
| RelayQuoteSolanaItemData
| RelayQuoteTronItemData,
): item is RelayQuoteEvmItemData => {
return 'to' in item && 'data' in item && 'value' in item
}

export const isRelayQuoteSolanaItemData = (
item: RelayQuoteUtxoItemData | RelayQuoteEvmItemData | RelayQuoteSolanaItemData,
item:
| RelayQuoteUtxoItemData
| RelayQuoteEvmItemData
| RelayQuoteSolanaItemData
| RelayQuoteTronItemData,
): item is RelayQuoteSolanaItemData => {
return 'instructions' in item
}

export const isRelayQuoteTronItemData = (
item:
| RelayQuoteUtxoItemData
| RelayQuoteEvmItemData
| RelayQuoteSolanaItemData
| RelayQuoteTronItemData,
): item is RelayQuoteTronItemData => {
return 'type' in item && 'parameter' in item
}

export type RelaySolanaInstruction = {
keys: {
pubkey: string
Expand Down
113 changes: 91 additions & 22 deletions packages/swapper/src/swappers/SunioSwapper/utils/getQuoteOrRate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { tronChainId } from '@shapeshiftoss/caip'
import { bn, contractAddressOrUndefined } from '@shapeshiftoss/utils'
import type { Result } from '@sniptt/monads'
import { Err, Ok } from '@sniptt/monads'
import { TronWeb } from 'tronweb'

import type {
CommonTradeQuoteInput,
Expand Down Expand Up @@ -44,7 +45,7 @@ export async function getQuoteOrRate(
slippageTolerancePercentageDecimal,
} = input

const { assertGetTronChainAdapter } = deps
const { assertGetTronChainAdapter: _assertGetTronChainAdapter } = deps

if (!isSupportedChainId(sellAsset.chainId)) {
return Err(
Expand Down Expand Up @@ -103,30 +104,98 @@ export async function getQuoteOrRate(

const isQuote = input.quoteOrRate === 'quote'

// Fetch network fees only for quotes
// For quotes, receiveAddress is required
if (isQuote && !receiveAddress) {
return Err(
makeSwapErrorRight({
message: '[Sun.io] receiveAddress is required for quotes',
code: TradeQuoteError.InternalError,
}),
)
}

// Fetch network fees for both quotes and rates (when wallet connected)
let networkFeeCryptoBaseUnit: string | undefined = undefined

if (isQuote) {
if (!receiveAddress) {
return Err(
makeSwapErrorRight({
message: '[Sun.io] receiveAddress is required for quotes',
code: TradeQuoteError.InternalError,
}),
)
}
// Estimate fees when we have an address to estimate from
if (receiveAddress) {
try {
const contractAddress = contractAddressOrUndefined(sellAsset.assetId)
const isSellingNativeTrx = !contractAddress

const adapter = assertGetTronChainAdapter(sellAsset.chainId)
const feeData = await adapter.getFeeData({
to: SUNIO_SMART_ROUTER_CONTRACT,
value: '0',
sendMax: false,
chainSpecific: {
from: receiveAddress,
contractAddress: contractAddressOrUndefined(sellAsset.assetId),
},
})
networkFeeCryptoBaseUnit = feeData.fast.txFee
const tronWeb = new TronWeb({ fullHost: deps.config.VITE_TRON_NODE_URL })

// Get chain parameters for pricing
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

// Check if recipient needs activation (applies to all swaps)
let accountActivationFee = 0
try {
const recipientInfoResponse = await fetch(
`${deps.config.VITE_TRON_NODE_URL}/wallet/getaccount`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address: receiveAddress, visible: true }),
},
)
const recipientInfo = await recipientInfoResponse.json()
const recipientExists = recipientInfo && Object.keys(recipientInfo).length > 1
if (!recipientExists) {
accountActivationFee = 1_000_000 // 1 TRX
}
} catch {
// Ignore activation check errors
}

// For native TRX swaps, Sun.io uses a contract call with value
// We need to estimate energy for the swap contract, not just bandwidth
if (isSellingNativeTrx) {
try {
// Sun.io contract owner provides most energy (~117k), users only pay ~2k
// Use fixed 2k energy estimate instead of querying (which returns total 120k)
const energyUsed = 2000 // User pays ~2k energy, contract covers the rest
const energyFee = energyUsed * energyPrice // No multiplier - contract provides energy

// Estimate bandwidth for contract call (much larger than simple transfer)
const bandwidthFee = 1100 * bandwidthPrice // ~1100 bytes for contract call (with safety buffer)

networkFeeCryptoBaseUnit = bn(energyFee)
.plus(bandwidthFee)
.plus(accountActivationFee)
.toFixed(0)
} catch (estimationError) {
// Fallback estimate: ~2k energy + ~1100 bytes bandwidth + activation fee
const fallbackEnergyFee = 2000 * energyPrice
const fallbackBandwidthFee = 1100 * bandwidthPrice
networkFeeCryptoBaseUnit = bn(fallbackEnergyFee)
.plus(fallbackBandwidthFee)
.plus(accountActivationFee)
.toFixed(0)
}
} else {
// For TRC-20 swaps through Sun.io router
// Same as TRX: contract owner provides most energy, user pays ~2k
// Sun.io provides ~217k energy, user pays ~2k
const energyFee = 2000 * energyPrice
const bandwidthFee = 1100 * bandwidthPrice

networkFeeCryptoBaseUnit = bn(energyFee)
.plus(bandwidthFee)
.plus(accountActivationFee)
.toFixed(0)
}
} catch (error) {
// For rates, fall back to '0' on estimation failure
// For quotes, let it error (required for accurate swap)
if (!isQuote) {
networkFeeCryptoBaseUnit = '0'
} else {
throw error
}
}
}

const buyAmountCryptoBaseUnit = bn(bestRoute.amountOut)
Expand Down