From 1127ca01131355ae5b53c806c1f0b44b79eb68cd Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 6 Jul 2026 00:02:31 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../configs/feature_toggles_config.json | 4 ++ .../features/swap/SwapFeatureToggles.kt | 1 + .../swap/domain/di/SwapDomainModule.kt | 3 + .../swap/domain/fee/DexSwapFeeCalculator.kt | 49 +++++++++++++ .../domain/fee/DexSwapFeeCalculatorTest.kt | 71 +++++++++++++++++++ .../feature/swap/DefaultSwapFeatureToggles.kt | 5 ++ 6 files changed, 133 insertions(+) diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index d538540a1f..ff1ce47b2b 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -166,5 +166,9 @@ { "name": "TWI_1637_CASHBACK_AND_REACTIVATION_CAMPAIGNS_ENABLED", "version": "undefined" + }, + { + "name": "AND_16080_TRON_DEX_SWAP_ENABLED", + "version": "undefined" } ] diff --git a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt index 0b58b199fe..5d442dbf20 100644 --- a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt +++ b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt @@ -7,4 +7,5 @@ interface SwapFeatureToggles { val isExpressShareButtonEnabled: Boolean val isSwapBestDexRateEnabled: Boolean val isHighFeeWarningEnabled: Boolean + val isTronDexSwapEnabled: Boolean } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 454592535e..da5ddfb5a5 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -18,6 +18,7 @@ import com.tangem.feature.swap.domain.fee.DexSwapFeeCalculator import com.tangem.feature.swap.domain.fee.PatchEthGasLimitForSwap import com.tangem.feature.swap.domain.transfer.SwapTransferInteractor import com.tangem.feature.swap.domain.transfer.SwapTransferInteractorImpl +import com.tangem.features.swap.SwapFeatureToggles import dagger.Binds import dagger.Module import dagger.Provides @@ -74,6 +75,7 @@ internal class SwapDomainModule { walletManagersFacade: WalletManagersFacade, @SwapDexGasLimit patchEthGasLimitForSwap: PatchEthGasLimitForSwap, wrapYieldSwapCallDataWithUpgradeUseCase: WrapYieldSwapCallDataWithUpgradeUseCase, + swapFeatureToggles: SwapFeatureToggles, ): DexSwapFeeCalculator = DexSwapFeeCalculator( getFeeUseCase = getFeeUseCase, getEthSpecificFeeUseCase = getEthSpecificFeeUseCase, @@ -82,6 +84,7 @@ internal class SwapDomainModule { walletManagersFacade = walletManagersFacade, patchEthGasLimitForSwap = patchEthGasLimitForSwap, wrapYieldSwapCallDataWithUpgradeUseCase = wrapYieldSwapCallDataWithUpgradeUseCase, + swapFeatureToggles = swapFeatureToggles, ) @Provides diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculator.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculator.kt index 0dd14bd531..b2ca111cd2 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculator.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculator.kt @@ -32,9 +32,11 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.yield.supply.usecase.WrapYieldSwapCallDataWithUpgradeUseCase import com.tangem.feature.swap.domain.models.domain.ExpressTransactionModel import com.tangem.feature.swap.domain.models.ui.PermissionDataState +import com.tangem.features.swap.SwapFeatureToggles import com.tangem.lib.crypto.BlockchainUtils.SOLANA_TRANSACTION_SIZE_THRESHOLD_BYTES import com.tangem.lib.crypto.BlockchainUtils.isBitcoin import com.tangem.lib.crypto.BlockchainUtils.isSolana +import com.tangem.lib.crypto.BlockchainUtils.isTron import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import java.math.BigInteger @@ -66,6 +68,7 @@ class DexSwapFeeCalculator( private val walletManagersFacade: WalletManagersFacade, private val patchEthGasLimitForSwap: PatchEthGasLimitForSwap, private val wrapYieldSwapCallDataWithUpgradeUseCase: WrapYieldSwapCallDataWithUpgradeUseCase, + private val swapFeatureToggles: SwapFeatureToggles, ) { suspend fun calculate( @@ -93,6 +96,11 @@ class DexSwapFeeCalculator( transaction = transaction, otherNativeFee = otherNativeFee, ) + isTron(networkRawId) && swapFeatureToggles.isTronDexSwapEnabled -> calculateTronFee( + fromSwapCurrencyStatus = fromSwapCurrencyStatus, + transaction = transaction, + otherNativeFee = otherNativeFee, + ) else -> calculateEvmFee( fromSwapCurrencyStatus = fromSwapCurrencyStatus, transaction = transaction, @@ -192,6 +200,47 @@ class DexSwapFeeCalculator( ) } + /** + * TRON DEX swap fee ([REDACTED_TASK_KEY], gated by [SwapFeatureToggles.isTronDexSwapEnabled]). + * + * The swap arrives in EVM format — a native-value contract call to the router [txTo] carrying + * raw [txData]. TRON has no EVM gas, so unlike [calculateEvmFee] there is no gas-limit bump and + * no eth-specific fallback: the fee (bandwidth + energy) is estimated directly for a + * [TransactionData.Uncompiled]. The `TronTransactionExtras` produced from [txData] is what makes + * the SDK build (and energy-estimate) the tx as a smart-contract call rather than a plain send. + */ + private suspend fun Raise.calculateTronFee( + fromSwapCurrencyStatus: SwapCurrencyStatus, + transaction: ExpressTransactionModel.DEX, + otherNativeFee: BigDecimal, + ): DexFeeResult { + val network = fromSwapCurrencyStatus.currency.network + val txValue = transaction.txValue ?: raise(GetFeeError.UnknownError) + val extras = createTransactionExtrasUseCase( + data = transaction.txData, + network = network, + ).getOrNull() ?: raise(GetFeeError.UnknownError) + + val transactionData = TransactionData.Uncompiled( + amount = createNativeAmountForDex(txValue, network), + destinationAddress = transaction.txTo, + fee = null, + sourceAddress = transaction.txFrom, + extras = extras, + ) + val fee = getFeeUseCase( + transactionData = transactionData, + network = network, + userWallet = fromSwapCurrencyStatus.userWallet, + ).getOrNull() ?: raise(GetFeeError.UnknownError) + + return DexFeeResult( + transactionFee = TransactionFeeResult.Loaded(fee), + otherNativeFee = otherNativeFee, + gas = null, + ) + } + /** * Yield-mode DEX fee path: routes the swap through the user's yield module proxy. * diff --git a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculatorTest.kt b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculatorTest.kt index 39bc1e1c00..8918f73d47 100644 --- a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculatorTest.kt +++ b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculatorTest.kt @@ -27,6 +27,7 @@ import com.tangem.feature.swap.domain.buildSwapCurrencyStatus import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.ExpressTransactionModel import com.tangem.feature.swap.domain.models.ui.PermissionDataState +import com.tangem.features.swap.SwapFeatureToggles import io.mockk.* import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach @@ -49,6 +50,7 @@ internal class DexSwapFeeCalculatorTest { private val ethNetwork = Blockchain.Ethereum.toNetworkId() private val solanaNetwork = Blockchain.Solana.toNetworkId() private val bitcoinNetwork = Blockchain.Bitcoin.toNetworkId() + private val tronNetwork = Blockchain.Tron.toNetworkId() private val getFeeUseCase: GetFeeUseCase = mockk(relaxed = true) private val getEthSpecificFeeUseCase: GetEthSpecificFeeUseCase = mockk(relaxed = true) @@ -56,6 +58,7 @@ internal class DexSwapFeeCalculatorTest { private val createTransactionExtrasUseCase: CreateTransactionDataExtrasUseCase = mockk(relaxed = true) private val walletManagersFacade: WalletManagersFacade = mockk(relaxed = true) private val wrapYieldSwapCallDataWithUpgradeUseCase: WrapYieldSwapCallDataWithUpgradeUseCase = mockk(relaxed = true) + private val swapFeatureToggles: SwapFeatureToggles = mockk(relaxed = true) private val dexBump = PatchEthGasLimitForSwap(percentage = PatchEthGasLimitForSwap.DEX_PERCENTAGE) @@ -68,6 +71,7 @@ internal class DexSwapFeeCalculatorTest { walletManagersFacade = walletManagersFacade, patchEthGasLimitForSwap = dexBump, wrapYieldSwapCallDataWithUpgradeUseCase = wrapYieldSwapCallDataWithUpgradeUseCase, + swapFeatureToggles = swapFeatureToggles, ) } @@ -841,6 +845,73 @@ internal class DexSwapFeeCalculatorTest { gasPrice = BigInteger.valueOf(20_000_000_000), ) + // ------------------------------------------------------------------------- + // TRON ([REDACTED_TASK_KEY]) — gated by SwapFeatureToggles.isTronDexSwapEnabled + // ------------------------------------------------------------------------- + + @Test + fun `GIVEN tron toggle ON WHEN calculate THEN tron fee path estimates fee with no evm gas`() = runTest { + // Arrange + every { swapFeatureToggles.isTronDexSwapEnabled } returns true + val fromStatus = buildSwapCurrencyStatus(networkRawId = tronNetwork, isCoin = true) + val transaction = buildDex( + txValue = "5000000", // 5 TRX + txTo = "TRouterAddress", + txFrom = "TSenderAddress", + txData = "a9059cbb", + ) + val capturedTxData = slot() + coEvery { + getFeeUseCase.invoke(userWallet = any(), network = any(), transactionData = capture(capturedTxData)) + } returns mockk(relaxed = true).right() + + // Act + val result = sut.calculate(fromStatus, transaction) + + // Assert + assertThat(result.isRight()).isTrue() + assertThat(capturedTxData.captured).isInstanceOf(TransactionData.Uncompiled::class.java) + result.onRight { dexFeeResult -> + // TRON path carries no EVM gas and applies no gas-limit bump. + assertThat(dexFeeResult.gas).isNull() + } + // No eth-specific fallback on the TRON path. + coVerify(exactly = 0) { + getEthSpecificFeeUseCase.invoke( + userWallet = any(), + cryptoCurrency = any(), + gasLimit = any(), + gasPrice = any(), + ) + } + } + + @Test + fun `GIVEN tron toggle OFF WHEN calculate THEN evm path is used and carries gas`() = runTest { + // Arrange + every { swapFeatureToggles.isTronDexSwapEnabled } returns false + val fromStatus = buildSwapCurrencyStatus(networkRawId = tronNetwork, isCoin = true) + val transaction = buildDex( + txValue = "5000000", + txTo = "TRouterAddress", + txFrom = "TSenderAddress", + txData = "a9059cbb", + ) + coEvery { + getFeeUseCase.invoke(userWallet = any(), network = any(), transactionData = any()) + } returns mockk(relaxed = true).right() + + // Act + val result = sut.calculate(fromStatus, transaction) + + // Assert + assertThat(result.isRight()).isTrue() + result.onRight { dexFeeResult -> + // Toggle off → TRON falls through to the EVM branch, which carries the tx gas. + assertThat(dexFeeResult.gas).isEqualTo(transaction.gas) + } + } + private fun buildDex( txData: String = "dGVzdA==", txValue: String? = "0", diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt index 636db910e3..5f4acc636d 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt @@ -38,4 +38,9 @@ internal class DefaultSwapFeatureToggles @Inject constructor( get() = featureTogglesManager.isFeatureEnabled( toggle = FeatureToggles.TWI_1367_HIGH_FEE_WARNING_ENABLED, ) + + override val isTronDexSwapEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled( + toggle = FeatureToggles.AND_16080_TRON_DEX_SWAP_ENABLED, + ) } \ No newline at end of file