diff --git a/packages/chain-adapters/src/tron/TronChainAdapter.ts b/packages/chain-adapters/src/tron/TronChainAdapter.ts index 8790d6dd92f..56e85f8a4df 100644 --- a/packages/chain-adapters/src/tron/TronChainAdapter.ts +++ b/packages/chain-adapters/src/tron/TronChainAdapter.ts @@ -289,6 +289,80 @@ export class ChainAdapter implements IChainAdapter { } } + async buildCustomApiTx(input: { + from: string + to: string + accountNumber: number + data: string + value: string + method?: string + args?: { type: string; value: unknown }[] + }): Promise { + try { + const { from, to, accountNumber, data, value } = input + + // Always use raw data field instead of method/args to ensure correct method selector + // TronWeb's triggerSmartContract computes method selectors differently than expected + const callData = data.startsWith('0x') ? data.slice(2) : data + let txData: TronUnsignedTx + + const requestBody = { + owner_address: from, + contract_address: to, + data: callData, + fee_limit: 100_000_000, + call_value: Number(value) || 0, + visible: true, + } + + const response = await this.requestQueue.add(() => + fetch(`${this.rpcUrl}/wallet/triggersmartcontract`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestBody), + }), + ) + + const result = await response.json() + + if (result.Error || !result.transaction) { + throw new Error(`TronGrid API error: ${result.Error || 'No transaction returned'}`) + } + + txData = result.transaction + + if (!txData.raw_data_hex) { + throw new Error('Failed to create transaction') + } + + const rawDataHexValue = txData.raw_data_hex + const rawDataHex = + typeof rawDataHexValue === 'string' + ? rawDataHexValue + : Buffer.isBuffer(rawDataHexValue) + ? (rawDataHexValue as Buffer).toString('hex') + : Array.isArray(rawDataHexValue) + ? Buffer.from(rawDataHexValue).toString('hex') + : (() => { + throw new Error(`Unexpected raw_data_hex type: ${typeof rawDataHexValue}`) + })() + + if (!/^[0-9a-fA-F]+$/.test(rawDataHex)) { + throw new Error(`Invalid raw_data_hex format: ${rawDataHex.slice(0, 100)}`) + } + + return { + addressNList: toAddressNList(this.getBip44Params({ accountNumber })), + rawDataHex, + transaction: txData, + } + } catch (err) { + return ErrorHandler(err, { + translation: 'chainAdapters.errors.buildTransaction', + }) + } + } + async buildSendTransaction(input: BuildSendTxInput): Promise<{ txToSign: TronSignTx }> { diff --git a/packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeQuote.ts index e50abed5251..79499414f9e 100644 --- a/packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeQuote.ts @@ -63,6 +63,8 @@ export const getTradeQuote = async ( ) } + // TODO: Debug why same-chain Tron swaps revert (swapAndCall method works on EVM but 0 successful on Tron) + // Yes, this is supposed to be supported as per checks above, but currently, Butter doesn't yield any quotes for BTC sells if (sellAsset.assetId === btcAssetId) { return Err( @@ -259,13 +261,15 @@ export const getTradeQuote = async ( buyAsset, sellAsset, accountNumber, - allowanceContract: route.contract ?? '0x0', + allowanceContract: sellAsset.chainId === tronChainId ? buildTx.to : route.contract ?? '0x0', estimatedExecutionTimeMs: route.timeEstimated * 1000, butterSwapTransactionMetadata: { to: buildTx.to, data: buildTx.data, value: buildTx.value, gasLimit: bnOrZero(route.gasEstimatedTarget).toFixed(), + method: buildTx.method, + args: buildTx.args, }, ...(solanaTransactionMetadata && { solanaTransactionMetadata, diff --git a/packages/swapper/src/tron-utils/getUnsignedTronTransaction.ts b/packages/swapper/src/tron-utils/getUnsignedTronTransaction.ts index 70a0112718b..ad51f0cb7eb 100644 --- a/packages/swapper/src/tron-utils/getUnsignedTronTransaction.ts +++ b/packages/swapper/src/tron-utils/getUnsignedTronTransaction.ts @@ -1,3 +1,4 @@ +import { tronAssetId } from '@shapeshiftoss/caip' import { contractAddressOrUndefined } from '@shapeshiftoss/utils' import type { GetUnsignedTronTransactionArgs } from '../types' @@ -23,15 +24,33 @@ export const getUnsignedTronTransaction = ({ const adapter = assertGetTronChainAdapter(sellAsset.chainId) - const to = - relayTransactionMetadata?.to ?? - nearIntentsSpecific?.depositAddress ?? - butterSwapTransactionMetadata?.to + if (butterSwapTransactionMetadata) { + const { to, data, method, args, value: butterValue } = butterSwapTransactionMetadata + + if (!to) throw new Error('Missing Butter swap contract address') + if (!data) throw new Error('Missing Butter swap transaction data') + + // Use Butter's value field which includes swap fees for same-chain swaps + // For native TRX sells, also include the sell amount + const isNativeTron = sellAsset.assetId === tronAssetId + const value = isNativeTron ? step.sellAmountIncludingProtocolFeesCryptoBaseUnit : butterValue + + return adapter.buildCustomApiTx({ + from, + to, + accountNumber, + data, + value, + method, + args, + }) + } + + const to = relayTransactionMetadata?.to ?? nearIntentsSpecific?.depositAddress if (!to) throw new Error('Missing transaction destination address') const value = step.sellAmountIncludingProtocolFeesCryptoBaseUnit - // Extract contract address for TRC20 tokens const contractAddress = contractAddressOrUndefined(sellAsset.assetId) return adapter.buildSendApiTransaction({ diff --git a/packages/swapper/src/types.ts b/packages/swapper/src/types.ts index 755a6eb218e..fbc6f04bb30 100644 --- a/packages/swapper/src/types.ts +++ b/packages/swapper/src/types.ts @@ -434,6 +434,8 @@ export type TradeQuoteStep = { data: string value: Hex gasLimit: string + method?: string + args?: { type: string; value: unknown }[] } sunioTransactionMetadata?: { route: {