diff --git a/packages/chain-adapters/src/tron/TronChainAdapter.ts b/packages/chain-adapters/src/tron/TronChainAdapter.ts index a21fb2a6545..5dc19b575ea 100644 --- a/packages/chain-adapters/src/tron/TronChainAdapter.ts +++ b/packages/chain-adapters/src/tron/TronChainAdapter.ts @@ -219,18 +219,24 @@ export class ChainAdapter implements IChainAdapter { 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 @@ -400,11 +406,9 @@ export class ChainAdapter implements IChainAdapter { } 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 @@ -426,7 +430,29 @@ export class ChainAdapter implements IChainAdapter { } } - 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)) diff --git a/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeQuote.ts index bb3f199ebbf..b8c5644d2d9 100644 --- a/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeQuote.ts @@ -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 } diff --git a/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeRate.ts b/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeRate.ts index a0a49537772..289c00fd88e 100644 --- a/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeRate.ts +++ b/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeRate.ts @@ -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 diff --git a/packages/swapper/src/swappers/RelaySwapper/utils/getTrade.ts b/packages/swapper/src/swappers/RelaySwapper/utils/getTrade.ts index ec1ac5ee1db..aae0e006d1d 100644 --- a/packages/swapper/src/swappers/RelaySwapper/utils/getTrade.ts +++ b/packages/swapper/src/swappers/RelaySwapper/utils/getTrade.ts @@ -39,6 +39,7 @@ import { isRelayError, isRelayQuoteEvmItemData, isRelayQuoteSolanaItemData, + isRelayQuoteTronItemData, isRelayQuoteUtxoItemData, } from '../utils/types' import { fetchRelayTrade } from './fetchRelayTrade' @@ -625,6 +626,17 @@ export async function getTrade({ } } + 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') })() diff --git a/packages/swapper/src/swappers/RelaySwapper/utils/types.ts b/packages/swapper/src/swappers/RelaySwapper/utils/types.ts index 6cb3f968d19..a35ca1a8942 100644 --- a/packages/swapper/src/swappers/RelaySwapper/utils/types.ts +++ b/packages/swapper/src/swappers/RelaySwapper/utils/types.ts @@ -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 = { @@ -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 diff --git a/packages/swapper/src/swappers/SunioSwapper/utils/getQuoteOrRate.ts b/packages/swapper/src/swappers/SunioSwapper/utils/getQuoteOrRate.ts index fe2fac23059..ccd079ddcec 100644 --- a/packages/swapper/src/swappers/SunioSwapper/utils/getQuoteOrRate.ts +++ b/packages/swapper/src/swappers/SunioSwapper/utils/getQuoteOrRate.ts @@ -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, @@ -44,7 +45,7 @@ export async function getQuoteOrRate( slippageTolerancePercentageDecimal, } = input - const { assertGetTronChainAdapter } = deps + const { assertGetTronChainAdapter: _assertGetTronChainAdapter } = deps if (!isSupportedChainId(sellAsset.chainId)) { return Err( @@ -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)