Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-07 14:01:42 +03:00
commit fcfbe7ca92
6 changed files with 133 additions and 0 deletions

View file

@ -166,5 +166,9 @@
{
"name": "TWI_1637_CASHBACK_AND_REACTIVATION_CAMPAIGNS_ENABLED",
"version": "undefined"
},
{
"name": "AND_16080_TRON_DEX_SWAP_ENABLED",
"version": "undefined"
}
]

View file

@ -7,4 +7,5 @@ interface SwapFeatureToggles {
val isExpressShareButtonEnabled: Boolean
val isSwapBestDexRateEnabled: Boolean
val isHighFeeWarningEnabled: Boolean
val isTronDexSwapEnabled: Boolean
}

View file

@ -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

View file

@ -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<GetFeeError>.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.
*

View file

@ -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<TransactionData>()
coEvery {
getFeeUseCase.invoke(userWallet = any(), network = any(), transactionData = capture(capturedTxData))
} returns mockk<TransactionFee.Single>(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<TransactionFee.Single>(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",

View file

@ -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,
)
}