Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-06 00:02:31 +03:00
parent a2c8e7e95a
commit 1127ca0113
6 changed files with 133 additions and 0 deletions

View file

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

View file

@ -7,4 +7,5 @@ interface SwapFeatureToggles {
val isExpressShareButtonEnabled: Boolean val isExpressShareButtonEnabled: Boolean
val isSwapBestDexRateEnabled: Boolean val isSwapBestDexRateEnabled: Boolean
val isHighFeeWarningEnabled: 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.fee.PatchEthGasLimitForSwap
import com.tangem.feature.swap.domain.transfer.SwapTransferInteractor import com.tangem.feature.swap.domain.transfer.SwapTransferInteractor
import com.tangem.feature.swap.domain.transfer.SwapTransferInteractorImpl import com.tangem.feature.swap.domain.transfer.SwapTransferInteractorImpl
import com.tangem.features.swap.SwapFeatureToggles
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -74,6 +75,7 @@ internal class SwapDomainModule {
walletManagersFacade: WalletManagersFacade, walletManagersFacade: WalletManagersFacade,
@SwapDexGasLimit patchEthGasLimitForSwap: PatchEthGasLimitForSwap, @SwapDexGasLimit patchEthGasLimitForSwap: PatchEthGasLimitForSwap,
wrapYieldSwapCallDataWithUpgradeUseCase: WrapYieldSwapCallDataWithUpgradeUseCase, wrapYieldSwapCallDataWithUpgradeUseCase: WrapYieldSwapCallDataWithUpgradeUseCase,
swapFeatureToggles: SwapFeatureToggles,
): DexSwapFeeCalculator = DexSwapFeeCalculator( ): DexSwapFeeCalculator = DexSwapFeeCalculator(
getFeeUseCase = getFeeUseCase, getFeeUseCase = getFeeUseCase,
getEthSpecificFeeUseCase = getEthSpecificFeeUseCase, getEthSpecificFeeUseCase = getEthSpecificFeeUseCase,
@ -82,6 +84,7 @@ internal class SwapDomainModule {
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
patchEthGasLimitForSwap = patchEthGasLimitForSwap, patchEthGasLimitForSwap = patchEthGasLimitForSwap,
wrapYieldSwapCallDataWithUpgradeUseCase = wrapYieldSwapCallDataWithUpgradeUseCase, wrapYieldSwapCallDataWithUpgradeUseCase = wrapYieldSwapCallDataWithUpgradeUseCase,
swapFeatureToggles = swapFeatureToggles,
) )
@Provides @Provides

View file

@ -32,9 +32,11 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.yield.supply.usecase.WrapYieldSwapCallDataWithUpgradeUseCase import com.tangem.domain.yield.supply.usecase.WrapYieldSwapCallDataWithUpgradeUseCase
import com.tangem.feature.swap.domain.models.domain.ExpressTransactionModel import com.tangem.feature.swap.domain.models.domain.ExpressTransactionModel
import com.tangem.feature.swap.domain.models.ui.PermissionDataState 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.SOLANA_TRANSACTION_SIZE_THRESHOLD_BYTES
import com.tangem.lib.crypto.BlockchainUtils.isBitcoin import com.tangem.lib.crypto.BlockchainUtils.isBitcoin
import com.tangem.lib.crypto.BlockchainUtils.isSolana import com.tangem.lib.crypto.BlockchainUtils.isSolana
import com.tangem.lib.crypto.BlockchainUtils.isTron
import com.tangem.utils.logging.TangemLogger import com.tangem.utils.logging.TangemLogger
import java.math.BigDecimal import java.math.BigDecimal
import java.math.BigInteger import java.math.BigInteger
@ -66,6 +68,7 @@ class DexSwapFeeCalculator(
private val walletManagersFacade: WalletManagersFacade, private val walletManagersFacade: WalletManagersFacade,
private val patchEthGasLimitForSwap: PatchEthGasLimitForSwap, private val patchEthGasLimitForSwap: PatchEthGasLimitForSwap,
private val wrapYieldSwapCallDataWithUpgradeUseCase: WrapYieldSwapCallDataWithUpgradeUseCase, private val wrapYieldSwapCallDataWithUpgradeUseCase: WrapYieldSwapCallDataWithUpgradeUseCase,
private val swapFeatureToggles: SwapFeatureToggles,
) { ) {
suspend fun calculate( suspend fun calculate(
@ -93,6 +96,11 @@ class DexSwapFeeCalculator(
transaction = transaction, transaction = transaction,
otherNativeFee = otherNativeFee, otherNativeFee = otherNativeFee,
) )
isTron(networkRawId) && swapFeatureToggles.isTronDexSwapEnabled -> calculateTronFee(
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
transaction = transaction,
otherNativeFee = otherNativeFee,
)
else -> calculateEvmFee( else -> calculateEvmFee(
fromSwapCurrencyStatus = fromSwapCurrencyStatus, fromSwapCurrencyStatus = fromSwapCurrencyStatus,
transaction = transaction, 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. * 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.SwapAmount
import com.tangem.feature.swap.domain.models.domain.ExpressTransactionModel import com.tangem.feature.swap.domain.models.domain.ExpressTransactionModel
import com.tangem.feature.swap.domain.models.ui.PermissionDataState import com.tangem.feature.swap.domain.models.ui.PermissionDataState
import com.tangem.features.swap.SwapFeatureToggles
import io.mockk.* import io.mockk.*
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
@ -49,6 +50,7 @@ internal class DexSwapFeeCalculatorTest {
private val ethNetwork = Blockchain.Ethereum.toNetworkId() private val ethNetwork = Blockchain.Ethereum.toNetworkId()
private val solanaNetwork = Blockchain.Solana.toNetworkId() private val solanaNetwork = Blockchain.Solana.toNetworkId()
private val bitcoinNetwork = Blockchain.Bitcoin.toNetworkId() private val bitcoinNetwork = Blockchain.Bitcoin.toNetworkId()
private val tronNetwork = Blockchain.Tron.toNetworkId()
private val getFeeUseCase: GetFeeUseCase = mockk(relaxed = true) private val getFeeUseCase: GetFeeUseCase = mockk(relaxed = true)
private val getEthSpecificFeeUseCase: GetEthSpecificFeeUseCase = 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 createTransactionExtrasUseCase: CreateTransactionDataExtrasUseCase = mockk(relaxed = true)
private val walletManagersFacade: WalletManagersFacade = mockk(relaxed = true) private val walletManagersFacade: WalletManagersFacade = mockk(relaxed = true)
private val wrapYieldSwapCallDataWithUpgradeUseCase: WrapYieldSwapCallDataWithUpgradeUseCase = 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) private val dexBump = PatchEthGasLimitForSwap(percentage = PatchEthGasLimitForSwap.DEX_PERCENTAGE)
@ -68,6 +71,7 @@ internal class DexSwapFeeCalculatorTest {
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
patchEthGasLimitForSwap = dexBump, patchEthGasLimitForSwap = dexBump,
wrapYieldSwapCallDataWithUpgradeUseCase = wrapYieldSwapCallDataWithUpgradeUseCase, wrapYieldSwapCallDataWithUpgradeUseCase = wrapYieldSwapCallDataWithUpgradeUseCase,
swapFeatureToggles = swapFeatureToggles,
) )
} }
@ -841,6 +845,73 @@ internal class DexSwapFeeCalculatorTest {
gasPrice = BigInteger.valueOf(20_000_000_000), 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( private fun buildDex(
txData: String = "dGVzdA==", txData: String = "dGVzdA==",
txValue: String? = "0", txValue: String? = "0",

View file

@ -38,4 +38,9 @@ internal class DefaultSwapFeatureToggles @Inject constructor(
get() = featureTogglesManager.isFeatureEnabled( get() = featureTogglesManager.isFeatureEnabled(
toggle = FeatureToggles.TWI_1367_HIGH_FEE_WARNING_ENABLED, toggle = FeatureToggles.TWI_1367_HIGH_FEE_WARNING_ENABLED,
) )
override val isTronDexSwapEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(
toggle = FeatureToggles.AND_16080_TRON_DEX_SWAP_ENABLED,
)
} }