Updated on 2026-08-14
This commit is contained in:
parent
a2c8e7e95a
commit
1127ca0113
6 changed files with 133 additions and 0 deletions
|
|
@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue