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