Updated on 2026-08-14
This commit is contained in:
parent
50332186fa
commit
5f6767abaf
58 changed files with 85 additions and 717 deletions
|
|
@ -2,10 +2,6 @@ package com.tangem.features.swap
|
|||
|
||||
interface SwapFeatureToggles {
|
||||
val isYieldSwapEnabled: Boolean
|
||||
val isSwapSwitchToTransferEnabled: Boolean
|
||||
val isSwapIntegratedApproveEnabled: Boolean
|
||||
val isExpressShareButtonEnabled: Boolean
|
||||
val isSwapBestDexRateEnabled: Boolean
|
||||
val isHighFeeWarningEnabled: Boolean
|
||||
val isTronDexSwapEnabled: Boolean
|
||||
}
|
||||
|
|
@ -157,9 +157,7 @@ interface SwapInteractor {
|
|||
* `LIMITED`), loads its [com.tangem.blockchain.common.transaction.TransactionFee] and returns
|
||||
* both as [IntegratedApprovalData].
|
||||
*
|
||||
* Used by the integrated approve+swap flow when
|
||||
* `SwapFeatureToggles.isSwapIntegratedApproveEnabled` is ON and the quote requires an
|
||||
* allowance bump.
|
||||
* Used by the integrated approve+swap flow when the quote requires an allowance bump.
|
||||
*
|
||||
* @param approvalAmount LIMITED-mode swap amount (the user-input amount). Used when
|
||||
* [approveType] is `LIMITED`; ignored for `UNLIMITED`.
|
||||
|
|
|
|||
|
|
@ -404,8 +404,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
)
|
||||
}
|
||||
val isBalanceWithoutFeeEnough = isBalanceEnough(fromSwapCurrencyStatus, amount, null)
|
||||
val isIntegratedApproveActive = swapFeatureToggles.isSwapIntegratedApproveEnabled &&
|
||||
!hasIntegratedApprovalFallenBack(fromSwapCurrencyStatus, spenderAddress)
|
||||
val isIntegratedApproveActive = !hasIntegratedApprovalFallenBack(fromSwapCurrencyStatus, spenderAddress)
|
||||
|
||||
val isAllowanceSatisfied = if (isIntegratedApproveActive) {
|
||||
allowanceInfo !is AllowanceInfo.ResetNeeded
|
||||
|
|
@ -1973,7 +1972,6 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
val isYieldSwap = fromSwapCurrencyStatus.isYieldSwapActive &&
|
||||
fromSwapCurrencyStatus.currency is CryptoCurrency.Token
|
||||
val isIntegratedApprovalNeeded = !isYieldSwap &&
|
||||
swapFeatureToggles.isSwapIntegratedApproveEnabled &&
|
||||
allowanceInfo is AllowanceInfo.NotEnough &&
|
||||
!hasIntegratedApprovalFallenBack(fromSwapCurrencyStatus, spenderAddress)
|
||||
swapState.copy(
|
||||
|
|
@ -2120,7 +2118,6 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
).getOrNull() ?: return quotesLoadedState.copy(permissionState = PermissionDataState.Empty)
|
||||
|
||||
val isIntegratedApprovalNeeded = !isYieldSwap &&
|
||||
swapFeatureToggles.isSwapIntegratedApproveEnabled &&
|
||||
allowanceInfo is AllowanceInfo.NotEnough &&
|
||||
!hasIntegratedApprovalFallenBack(fromSwapCurrencyStatus, quoteModel.allowanceContract)
|
||||
return quotesLoadedState.copy(
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ import com.tangem.feature.swap.domain.models.SwapAmount
|
|||
import com.tangem.feature.swap.domain.models.ui.SwapState
|
||||
import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkFeeCoverage
|
||||
import com.tangem.features.swap.SwapFeatureToggles
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import kotlinx.coroutines.flow.first
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -54,7 +53,6 @@ import javax.inject.Inject
|
|||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
class SwapTransferInteractorImpl @Inject constructor(
|
||||
private val swapFeatureToggles: SwapFeatureToggles,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
|
|
@ -306,7 +304,6 @@ class SwapTransferInteractorImpl @Inject constructor(
|
|||
fromSwapCurrency: CryptoCurrency?,
|
||||
toSwapCurrency: CryptoCurrency?,
|
||||
): Boolean {
|
||||
if (swapFeatureToggles.isSwapSwitchToTransferEnabled.not()) return false
|
||||
val isSameCurrency = when {
|
||||
fromSwapCurrency is CryptoCurrency.Coin && toSwapCurrency is CryptoCurrency.Coin -> {
|
||||
fromSwapCurrency.network.rawId == toSwapCurrency.network.rawId
|
||||
|
|
|
|||
|
|
@ -1618,11 +1618,6 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase(
|
|||
private val spender = "0xDexRouter"
|
||||
private val tokenContract = "0xRegularToken"
|
||||
|
||||
@BeforeEach
|
||||
fun enableIntegrated() {
|
||||
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NotEnough allowance with integrated active proceeds to PermissionSettings`() = runTest {
|
||||
stubAllowanceForSpender(
|
||||
|
|
|
|||
|
|
@ -169,7 +169,6 @@ internal class SwapInteractorImplLoadDexSwapDataNoFeeTest : SwapInteractorImplTe
|
|||
|
||||
@Test
|
||||
fun `GIVEN NotEnough allowance AND integrated active THEN permissionState is PermissionSettings`() = runTest {
|
||||
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns true
|
||||
stubAllowance(AllowanceInfo.NotEnough(allowance = BigDecimal.ZERO, requiredAmount = BigDecimal.ONE))
|
||||
|
||||
val state = runFindBestQuoteForToken()
|
||||
|
|
@ -181,7 +180,6 @@ internal class SwapInteractorImplLoadDexSwapDataNoFeeTest : SwapInteractorImplTe
|
|||
|
||||
@Test
|
||||
fun `GIVEN Enough allowance THEN permissionState is Empty`() = runTest {
|
||||
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns true
|
||||
stubAllowance(AllowanceInfo.Enough(allowance = BigDecimal("100")))
|
||||
|
||||
val state = runFindBestQuoteForToken()
|
||||
|
|
@ -194,7 +192,6 @@ internal class SwapInteractorImplLoadDexSwapDataNoFeeTest : SwapInteractorImplTe
|
|||
runTest {
|
||||
// [REDACTED_TASK_KEY] / iOS parity: yield swaps must never use the integrated approve+swap path.
|
||||
// The yield-module proxy allowance is granted at enrollment, so no in-flow approval is shown.
|
||||
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns true
|
||||
every { swapFeatureToggles.isYieldSwapEnabled } returns true
|
||||
coEvery { yieldModuleAddressProvider.getOrFetch(any(), any()) } returns YIELD_PROXY
|
||||
coEvery { walletManagersFacade.isSwapSpenderAllowed(any(), any(), any()) } returns true
|
||||
|
|
@ -221,26 +218,6 @@ internal class SwapInteractorImplLoadDexSwapDataNoFeeTest : SwapInteractorImplTe
|
|||
assertThat(state.permissionState).isEqualTo(PermissionDataState.Empty)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN NotEnough allowance AND integrated toggle OFF THEN does not reach loadDexSwapDataNoFee`() = runTest {
|
||||
// With the integrated toggle off, NotEnough is not allowance-satisfied (requires Enough),
|
||||
// so manageDex does NOT enter loadDexSwapDataNoFee — getExchangeData is never called.
|
||||
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns false
|
||||
stubAllowance(AllowanceInfo.NotEnough(allowance = BigDecimal.ZERO, requiredAmount = BigDecimal.ONE))
|
||||
|
||||
runFindBestQuoteForTokenRaw()
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
repository.getExchangeData(
|
||||
userWallet = any(), fromContractAddress = any(), fromNetwork = any(),
|
||||
toContractAddress = any(), fromAddress = any(), toNetwork = any(),
|
||||
fromAmount = any(), fromDecimals = any(), toDecimals = any(),
|
||||
providerId = any(), rateType = any(), toAddress = any(),
|
||||
expressOperationType = any(), refundAddress = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
private fun stubAllowance(info: AllowanceInfo) {
|
||||
|
|
@ -261,11 +238,6 @@ internal class SwapInteractorImplLoadDexSwapDataNoFeeTest : SwapInteractorImplTe
|
|||
return result[dexProvider] as SwapState.QuotesLoadedState
|
||||
}
|
||||
|
||||
private suspend fun runFindBestQuoteForTokenRaw() {
|
||||
val dexProvider = stubDexQuoteAndExchangeData()
|
||||
invokeFindBestQuote(dexProvider)
|
||||
}
|
||||
|
||||
private fun stubDexQuoteAndExchangeData(): com.tangem.feature.swap.domain.models.domain.SwapProvider {
|
||||
val dexProvider = buildSwapProvider(ExchangeProviderType.DEX)
|
||||
val quoteModel = buildQuoteModel(allowanceContract = SPENDER)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ import com.tangem.feature.swap.domain.fee.TransactionFeeResult
|
|||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.ui.SwapState
|
||||
import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
|
||||
import com.tangem.features.swap.SwapFeatureToggles
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
|
@ -53,7 +52,6 @@ import java.math.BigDecimal
|
|||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class SwapTransferInteractorImplTest {
|
||||
|
||||
private val swapFeatureToggles: SwapFeatureToggles = mockk()
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase = mockk()
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase = mockk()
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase = mockk()
|
||||
|
|
@ -72,7 +70,6 @@ internal class SwapTransferInteractorImplTest {
|
|||
private val validateTransactionUseCase: ValidateTransactionUseCase = mockk()
|
||||
|
||||
private val sut = SwapTransferInteractorImpl(
|
||||
swapFeatureToggles = swapFeatureToggles,
|
||||
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
|
||||
getBalanceHidingSettingsUseCase = getBalanceHidingSettingsUseCase,
|
||||
isAccountsModeEnabledUseCase = isAccountsModeEnabledUseCase,
|
||||
|
|
@ -1314,23 +1311,8 @@ internal class SwapTransferInteractorImplTest {
|
|||
|
||||
// region shouldTransferInsteadOfSwap
|
||||
|
||||
@Test
|
||||
fun `GIVEN feature toggle disabled WHEN shouldTransferInsteadOfSwap THEN return false`() {
|
||||
every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns false
|
||||
|
||||
val result = sut.shouldTransferInsteadOfSwap(
|
||||
fromSwapCurrency = buildCoin(networkRawId = ETHEREUM),
|
||||
toSwapCurrency = buildCoin(networkRawId = ETHEREUM),
|
||||
)
|
||||
|
||||
assertThat(result).isFalse()
|
||||
verify { swapFeatureToggles.isSwapSwitchToTransferEnabled }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN both coins on the same network WHEN shouldTransferInsteadOfSwap THEN return true`() {
|
||||
every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns true
|
||||
|
||||
val result = sut.shouldTransferInsteadOfSwap(
|
||||
fromSwapCurrency = buildCoin(networkRawId = ETHEREUM),
|
||||
toSwapCurrency = buildCoin(networkRawId = ETHEREUM),
|
||||
|
|
@ -1341,8 +1323,6 @@ internal class SwapTransferInteractorImplTest {
|
|||
|
||||
@Test
|
||||
fun `GIVEN coins on different networks WHEN shouldTransferInsteadOfSwap THEN return false`() {
|
||||
every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns true
|
||||
|
||||
val result = sut.shouldTransferInsteadOfSwap(
|
||||
fromSwapCurrency = buildCoin(networkRawId = ETHEREUM),
|
||||
toSwapCurrency = buildCoin(networkRawId = POLYGON),
|
||||
|
|
@ -1353,8 +1333,6 @@ internal class SwapTransferInteractorImplTest {
|
|||
|
||||
@Test
|
||||
fun `GIVEN tokens with same network and same contract WHEN shouldTransferInsteadOfSwap THEN return true`() {
|
||||
every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns true
|
||||
|
||||
val result = sut.shouldTransferInsteadOfSwap(
|
||||
fromSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT),
|
||||
toSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT),
|
||||
|
|
@ -1365,8 +1343,6 @@ internal class SwapTransferInteractorImplTest {
|
|||
|
||||
@Test
|
||||
fun `GIVEN tokens with same network but different contract WHEN shouldTransferInsteadOfSwap THEN return false`() {
|
||||
every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns true
|
||||
|
||||
val result = sut.shouldTransferInsteadOfSwap(
|
||||
fromSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT),
|
||||
toSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDC_CONTRACT),
|
||||
|
|
@ -1377,8 +1353,6 @@ internal class SwapTransferInteractorImplTest {
|
|||
|
||||
@Test
|
||||
fun `GIVEN tokens with same contract but different network WHEN shouldTransferInsteadOfSwap THEN return false`() {
|
||||
every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns true
|
||||
|
||||
val result = sut.shouldTransferInsteadOfSwap(
|
||||
fromSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT),
|
||||
toSwapCurrency = buildToken(networkRawId = POLYGON, contractAddress = USDT_CONTRACT),
|
||||
|
|
@ -1389,8 +1363,6 @@ internal class SwapTransferInteractorImplTest {
|
|||
|
||||
@Test
|
||||
fun `GIVEN coin from and token to WHEN shouldTransferInsteadOfSwap THEN return false`() {
|
||||
every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns true
|
||||
|
||||
val result = sut.shouldTransferInsteadOfSwap(
|
||||
fromSwapCurrency = buildCoin(networkRawId = ETHEREUM),
|
||||
toSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT),
|
||||
|
|
@ -1401,8 +1373,6 @@ internal class SwapTransferInteractorImplTest {
|
|||
|
||||
@Test
|
||||
fun `GIVEN token from and coin to WHEN shouldTransferInsteadOfSwap THEN return false`() {
|
||||
every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns true
|
||||
|
||||
val result = sut.shouldTransferInsteadOfSwap(
|
||||
fromSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT),
|
||||
toSwapCurrency = buildCoin(networkRawId = ETHEREUM),
|
||||
|
|
|
|||
|
|
@ -14,26 +14,6 @@ internal class DefaultSwapFeatureToggles @Inject constructor(
|
|||
toggle = FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED,
|
||||
)
|
||||
|
||||
override val isSwapSwitchToTransferEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.AND_15207_SWAP_SWITCH_TO_TRANSFER_ENABLED,
|
||||
)
|
||||
|
||||
override val isSwapIntegratedApproveEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.AND_15120_SWAP_INTEGRATED_APPROVE,
|
||||
)
|
||||
|
||||
override val isExpressShareButtonEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.AND_15489_EXPRESS_SHARE_BUTTON_ENABLED,
|
||||
)
|
||||
|
||||
override val isSwapBestDexRateEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.AND_15715_SWAP_BEST_DEX_RATE_ENABLED,
|
||||
) && isSwapIntegratedApproveEnabled
|
||||
|
||||
override val isHighFeeWarningEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.TWI_1367_HIGH_FEE_WARNING_ENABLED,
|
||||
|
|
|
|||
|
|
@ -28,18 +28,11 @@ internal object SwapProviderResolver {
|
|||
/**
|
||||
* Picks the best provider among [states].
|
||||
*
|
||||
* When [isSwapBestDexRateEnabled] is on and at least one DEX/DEX_BRIDGE provider is present, the
|
||||
* best-rated DEX provider wins; otherwise the overall best-rated provider is returned (the best
|
||||
* CEX when no DEX is available). "Best rated" = lowest from/to fiat ratio (most output per unit
|
||||
* of input). Returns null when [states] is empty.
|
||||
*
|
||||
* @param isSwapBestDexRateEnabled whether the Best DEX Rate feature toggle is on.
|
||||
* When at least one DEX/DEX_BRIDGE provider is present, the best-rated DEX provider wins; otherwise
|
||||
* the overall best-rated provider is returned (the best CEX when no DEX is available). "Best rated" =
|
||||
* lowest from/to fiat ratio (most output per unit of input). Returns null when [states] is empty.
|
||||
*/
|
||||
fun findBest(
|
||||
states: Map<SwapProvider, SwapState.QuotesLoadedState>,
|
||||
isSwapBestDexRateEnabled: Boolean,
|
||||
): SwapProvider? {
|
||||
if (!isSwapBestDexRateEnabled) return findBestRated(states)
|
||||
fun findBest(states: Map<SwapProvider, SwapState.QuotesLoadedState>): SwapProvider? {
|
||||
val dexStates = states.filterKeys { it.type.isDex() }
|
||||
return if (dexStates.isNotEmpty()) {
|
||||
findBestRated(dexStates)
|
||||
|
|
@ -54,7 +47,7 @@ internal object SwapProviderResolver {
|
|||
* Priority: FCA restriction → permission required → recommended → best rate → none. A best-rate
|
||||
* badge is shown only when more than one provider is considered, FCA restrictions are not applied,
|
||||
* and this row's quote carries no price-impact warning. Which best-rate badge it is depends on the
|
||||
* provider mix (only relevant when [isSwapBestDexRateEnabled] is on):
|
||||
* provider mix:
|
||||
* - [AdditionalBadge.BestTrade] ("Best rate") — always on the overall best-rated provider,
|
||||
* regardless of its type.
|
||||
* - [AdditionalBadge.BestDexRate] ("Best DEX rate") — only when both CEX and DEX providers are
|
||||
|
|
@ -62,22 +55,17 @@ internal object SwapProviderResolver {
|
|||
* shown on the best-rated DEX. When a DEX already is the overall best, or the set is CEX-only /
|
||||
* DEX-only, no separate "Best DEX rate" badge is shown.
|
||||
*
|
||||
* When [isSwapBestDexRateEnabled] is off, only the overall best provider gets [AdditionalBadge.BestTrade]
|
||||
* (legacy behaviour) and [AdditionalBadge.BestDexRate] is never produced.
|
||||
*
|
||||
* @param states all loaded quotes — used to find the best providers and to count considered providers.
|
||||
* @param provider the provider this row represents.
|
||||
* @param needApplyFCARestrictions whether FCA restrictions apply to the current user.
|
||||
* @param state this provider's [SwapState]; price-impact and permission are read from it when it
|
||||
* is a [SwapState.QuotesLoadedState]. Null for error rows (which only resolve to FCA / recommended / none).
|
||||
* @param isSwapBestDexRateEnabled whether the Best DEX Rate feature toggle is on.
|
||||
*/
|
||||
fun resolveBadge(
|
||||
states: Map<SwapProvider, SwapState.QuotesLoadedState>,
|
||||
provider: SwapProvider,
|
||||
needApplyFCARestrictions: Boolean,
|
||||
state: SwapState? = null,
|
||||
isSwapBestDexRateEnabled: Boolean,
|
||||
): AdditionalBadge {
|
||||
val priceImpact = (state as? SwapState.QuotesLoadedState)?.priceImpact
|
||||
val permissionState = (state as? SwapState.QuotesLoadedState)?.permissionState
|
||||
|
|
@ -90,7 +78,7 @@ internal object SwapProviderResolver {
|
|||
needApplyFCARestrictions && provider.isFCARestricted() -> AdditionalBadge.FCAWarningList
|
||||
permissionState is PermissionDataState.PermissionRequired -> AdditionalBadge.PermissionRequired
|
||||
provider.isRecommended -> AdditionalBadge.Recommended
|
||||
isBestRateBadgeAllowed -> resolveBestRateBadge(states, provider, isSwapBestDexRateEnabled)
|
||||
isBestRateBadgeAllowed -> resolveBestRateBadge(states, provider)
|
||||
else -> AdditionalBadge.Empty
|
||||
}
|
||||
}
|
||||
|
|
@ -102,16 +90,10 @@ internal object SwapProviderResolver {
|
|||
private fun resolveBestRateBadge(
|
||||
states: Map<SwapProvider, SwapState.QuotesLoadedState>,
|
||||
provider: SwapProvider,
|
||||
isSwapBestDexRateEnabled: Boolean,
|
||||
): AdditionalBadge {
|
||||
val overallBest = findBestRated(states)
|
||||
val isOverallBest = provider.providerId == overallBest?.providerId
|
||||
|
||||
// Toggle off → legacy behaviour: only the overall best provider gets the "Best rate" badge.
|
||||
if (!isSwapBestDexRateEnabled) {
|
||||
return if (isOverallBest) AdditionalBadge.BestTrade else AdditionalBadge.Empty
|
||||
}
|
||||
|
||||
val dexStates = states.filterKeys { it.type.isDex() }
|
||||
val hasDex = dexStates.isNotEmpty()
|
||||
val hasCex = states.keys.any { !it.type.isDex() }
|
||||
|
|
|
|||
|
|
@ -281,7 +281,6 @@ internal class SwapModel @Inject constructor(
|
|||
get() {
|
||||
val permissionState = dataState.getCurrentLoadedSwapState()?.permissionState
|
||||
return permissionState == PermissionDataState.Empty ||
|
||||
swapFeatureToggles.isSwapIntegratedApproveEnabled &&
|
||||
permissionState is PermissionDataState.PermissionSettings
|
||||
}
|
||||
|
||||
|
|
@ -1175,7 +1174,6 @@ internal class SwapModel @Inject constructor(
|
|||
needApplyFCARestrictions = userCountry.needApplyFCARestrictions(),
|
||||
states = loadedStates,
|
||||
state = state,
|
||||
isSwapBestDexRateEnabled = swapFeatureToggles.isSwapBestDexRateEnabled,
|
||||
)
|
||||
val swapFee = getSelectedSwapFee()
|
||||
uiState = stateBuilder.createQuotesLoadedState(
|
||||
|
|
@ -1272,7 +1270,6 @@ internal class SwapModel @Inject constructor(
|
|||
needApplyFCARestrictions = userCountry.needApplyFCARestrictions(),
|
||||
states = loadedStates,
|
||||
state = state,
|
||||
isSwapBestDexRateEnabled = swapFeatureToggles.isSwapBestDexRateEnabled,
|
||||
)
|
||||
uiState = stateBuilder.createQuotesErrorState(
|
||||
uiStateHolder = uiState,
|
||||
|
|
@ -1327,10 +1324,7 @@ internal class SwapModel @Inject constructor(
|
|||
|
||||
return if (consideredProviders.isNotEmpty()) {
|
||||
val successLoadedData = consideredProviders.getLastLoadedSuccessStates()
|
||||
val bestQuotesProvider = SwapProviderResolver.findBest(
|
||||
states = successLoadedData,
|
||||
isSwapBestDexRateEnabled = swapFeatureToggles.isSwapBestDexRateEnabled,
|
||||
)
|
||||
val bestQuotesProvider = SwapProviderResolver.findBest(states = successLoadedData)
|
||||
val currentSelected = dataState.selectedProvider
|
||||
if (currentSelected != null && consideredProviders.keys.contains(currentSelected)) {
|
||||
// logic for always choose best if already selected provider
|
||||
|
|
@ -2111,7 +2105,6 @@ internal class SwapModel @Inject constructor(
|
|||
selectedProviderId = providerId,
|
||||
pricesLowerBest = pricesLowerBest,
|
||||
providersStates = dataState.lastLoadedSwapStates,
|
||||
isSwapBestDexRateEnabled = swapFeatureToggles.isSwapBestDexRateEnabled,
|
||||
needApplyFCARestrictions = userCountry.needApplyFCARestrictions(),
|
||||
) { uiState = stateBuilder.dismissBottomSheet(uiState) }
|
||||
},
|
||||
|
|
@ -2663,8 +2656,7 @@ internal class SwapModel @Inject constructor(
|
|||
val swapDataForCall = resolveDexSwapDataForFee(quoteState)
|
||||
.getOrElse { return Either.Left(it) }
|
||||
|
||||
val integratedSettings = (quoteState.permissionState as? PermissionDataState.PermissionSettings)
|
||||
?.takeIf { swapFeatureToggles.isSwapIntegratedApproveEnabled }
|
||||
val integratedSettings = quoteState.permissionState as? PermissionDataState.PermissionSettings
|
||||
|
||||
return swapInteractor.loadSwapFee(
|
||||
quotesLoadedState = quoteState,
|
||||
|
|
@ -2833,8 +2825,7 @@ internal class SwapModel @Inject constructor(
|
|||
|
||||
private fun isPermissionNotificationShown(): Boolean {
|
||||
val permissionState = dataState.getCurrentLoadedSwapState()?.permissionState
|
||||
val isApprovalIntegrated = swapFeatureToggles.isSwapIntegratedApproveEnabled &&
|
||||
permissionState is PermissionDataState.PermissionSettings
|
||||
val isApprovalIntegrated = permissionState is PermissionDataState.PermissionSettings
|
||||
return permissionState != null && permissionState !is PermissionDataState.Empty && !isApprovalIntegrated
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1220,7 +1220,6 @@ internal class StateBuilder(
|
|||
pricesLowerBest: Map<String, Float>,
|
||||
providersStates: Map<SwapProvider, SwapState>,
|
||||
needApplyFCARestrictions: Boolean,
|
||||
isSwapBestDexRateEnabled: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
): SwapStateHolder {
|
||||
val successStates = providersStates.getLastLoadedSuccessStates()
|
||||
|
|
@ -1231,7 +1230,6 @@ internal class StateBuilder(
|
|||
provider = entry.key,
|
||||
needApplyFCARestrictions = needApplyFCARestrictions,
|
||||
state = entry.value,
|
||||
isSwapBestDexRateEnabled = isSwapBestDexRateEnabled,
|
||||
)
|
||||
entry.convertToProviderBottomSheetState(
|
||||
pricesLowerBest = pricesLowerBest,
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ import java.math.BigDecimal
|
|||
* row-badge resolution ([SwapProviderResolver.resolveBadge]).
|
||||
*
|
||||
* Ranking metric: best provider == lowest `from/to` fiat ratio == highest `to` fiat output for the
|
||||
* same `from` input. "Best DEX Rate" prefers the best-rated DEX/DEX_BRIDGE provider when the feature
|
||||
* is on and any DEX is present.
|
||||
* same `from` input. "Best DEX Rate" prefers the best-rated DEX/DEX_BRIDGE provider when any DEX is
|
||||
* present.
|
||||
*/
|
||||
internal class SwapProviderResolverTest {
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ internal class SwapProviderResolverTest {
|
|||
dexBridge to quote(fromFiat = "100", toFiat = "105"),
|
||||
)
|
||||
|
||||
val best = SwapProviderResolver.findBest(states, isSwapBestDexRateEnabled = true)
|
||||
val best = SwapProviderResolver.findBest(states)
|
||||
|
||||
assertThat(best).isEqualTo(dex1)
|
||||
}
|
||||
|
|
@ -62,23 +62,11 @@ internal class SwapProviderResolverTest {
|
|||
cex2 to quote(fromFiat = "100", toFiat = "120"), // best CEX
|
||||
)
|
||||
|
||||
val best = SwapProviderResolver.findBest(states, isSwapBestDexRateEnabled = true)
|
||||
val best = SwapProviderResolver.findBest(states)
|
||||
|
||||
assertThat(best).isEqualTo(cex2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN best dex rate off WHEN findBest THEN best overall regardless of type`() {
|
||||
val states = mapOf(
|
||||
cex1 to quote(fromFiat = "100", toFiat = "120"), // best overall (a CEX)
|
||||
dex1 to quote(fromFiat = "100", toFiat = "110"),
|
||||
)
|
||||
|
||||
val best = SwapProviderResolver.findBest(states, isSwapBestDexRateEnabled = false)
|
||||
|
||||
assertThat(best).isEqualTo(cex1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN DEX_BRIDGE is the best DEX WHEN findBest with best dex rate on THEN DEX_BRIDGE selected`() {
|
||||
val states = mapOf(
|
||||
|
|
@ -87,7 +75,7 @@ internal class SwapProviderResolverTest {
|
|||
dexBridge to quote(fromFiat = "100", toFiat = "115"), // best among DEX-based
|
||||
)
|
||||
|
||||
val best = SwapProviderResolver.findBest(states, isSwapBestDexRateEnabled = true)
|
||||
val best = SwapProviderResolver.findBest(states)
|
||||
|
||||
assertThat(best).isEqualTo(dexBridge)
|
||||
}
|
||||
|
|
@ -110,7 +98,6 @@ internal class SwapProviderResolverTest {
|
|||
provider = dex1,
|
||||
needApplyFCARestrictions = false,
|
||||
state = states.getValue(dex1),
|
||||
isSwapBestDexRateEnabled = true,
|
||||
)
|
||||
|
||||
// Overall best is the DEX → it gets the single "Best rate" badge, NOT "Best DEX rate".
|
||||
|
|
@ -129,7 +116,6 @@ internal class SwapProviderResolverTest {
|
|||
provider = cex1,
|
||||
needApplyFCARestrictions = false,
|
||||
state = states.getValue(cex1),
|
||||
isSwapBestDexRateEnabled = true,
|
||||
)
|
||||
|
||||
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty)
|
||||
|
|
@ -150,7 +136,6 @@ internal class SwapProviderResolverTest {
|
|||
provider = cex1,
|
||||
needApplyFCARestrictions = false,
|
||||
state = states.getValue(cex1),
|
||||
isSwapBestDexRateEnabled = true,
|
||||
)
|
||||
|
||||
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.BestTrade)
|
||||
|
|
@ -169,7 +154,6 @@ internal class SwapProviderResolverTest {
|
|||
provider = dex1,
|
||||
needApplyFCARestrictions = false,
|
||||
state = states.getValue(dex1),
|
||||
isSwapBestDexRateEnabled = true,
|
||||
)
|
||||
|
||||
// CEX wins overall, so the best DEX additionally gets the "Best DEX rate" badge.
|
||||
|
|
@ -189,7 +173,6 @@ internal class SwapProviderResolverTest {
|
|||
provider = dexBridge,
|
||||
needApplyFCARestrictions = false,
|
||||
state = states.getValue(dexBridge),
|
||||
isSwapBestDexRateEnabled = true,
|
||||
)
|
||||
|
||||
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty)
|
||||
|
|
@ -209,7 +192,6 @@ internal class SwapProviderResolverTest {
|
|||
provider = dex1,
|
||||
needApplyFCARestrictions = false,
|
||||
state = states.getValue(dex1),
|
||||
isSwapBestDexRateEnabled = true,
|
||||
)
|
||||
|
||||
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.BestTrade)
|
||||
|
|
@ -227,7 +209,6 @@ internal class SwapProviderResolverTest {
|
|||
provider = dexBridge,
|
||||
needApplyFCARestrictions = false,
|
||||
state = states.getValue(dexBridge),
|
||||
isSwapBestDexRateEnabled = true,
|
||||
)
|
||||
|
||||
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty)
|
||||
|
|
@ -247,51 +228,11 @@ internal class SwapProviderResolverTest {
|
|||
provider = cex1,
|
||||
needApplyFCARestrictions = false,
|
||||
state = states.getValue(cex1),
|
||||
isSwapBestDexRateEnabled = true,
|
||||
)
|
||||
|
||||
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.BestTrade)
|
||||
}
|
||||
|
||||
// --- Toggle off → only the overall best gets BestTrade; "Best DEX rate" is never produced.
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle off AND both types present with CEX best WHEN resolveBadge for the CEX THEN BestTrade`() {
|
||||
val states = mapOf(
|
||||
cex1 to quote(fromFiat = "100", toFiat = "120"), // best overall
|
||||
dex1 to quote(fromFiat = "100", toFiat = "110"),
|
||||
)
|
||||
|
||||
val badge = SwapProviderResolver.resolveBadge(
|
||||
states = states,
|
||||
provider = cex1,
|
||||
needApplyFCARestrictions = false,
|
||||
state = states.getValue(cex1),
|
||||
isSwapBestDexRateEnabled = false,
|
||||
)
|
||||
|
||||
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.BestTrade)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle off AND both types present with CEX best WHEN resolveBadge for the DEX THEN Empty`() {
|
||||
val states = mapOf(
|
||||
cex1 to quote(fromFiat = "100", toFiat = "120"), // best overall
|
||||
dex1 to quote(fromFiat = "100", toFiat = "110"),
|
||||
)
|
||||
|
||||
val badge = SwapProviderResolver.resolveBadge(
|
||||
states = states,
|
||||
provider = dex1,
|
||||
needApplyFCARestrictions = false,
|
||||
state = states.getValue(dex1),
|
||||
isSwapBestDexRateEnabled = false,
|
||||
)
|
||||
|
||||
// Toggle off → no "Best DEX rate" badge even though a DEX is present and not the overall best.
|
||||
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN FCA restricted provider AND restrictions on WHEN resolveBadge THEN FCAWarningList`() {
|
||||
val restricted = provider(id = "changelly", type = ExchangeProviderType.CEX, isRecommended = true)
|
||||
|
|
@ -305,7 +246,6 @@ internal class SwapProviderResolverTest {
|
|||
provider = restricted,
|
||||
needApplyFCARestrictions = true,
|
||||
state = states.getValue(restricted),
|
||||
isSwapBestDexRateEnabled = true,
|
||||
)
|
||||
|
||||
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.FCAWarningList)
|
||||
|
|
@ -326,7 +266,6 @@ internal class SwapProviderResolverTest {
|
|||
provider = restricted,
|
||||
needApplyFCARestrictions = true,
|
||||
state = states.getValue(restricted),
|
||||
isSwapBestDexRateEnabled = true,
|
||||
)
|
||||
|
||||
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.FCAWarningList)
|
||||
|
|
@ -346,7 +285,6 @@ internal class SwapProviderResolverTest {
|
|||
provider = restricted,
|
||||
needApplyFCARestrictions = false,
|
||||
state = states.getValue(restricted),
|
||||
isSwapBestDexRateEnabled = false,
|
||||
)
|
||||
|
||||
// Restrictions are off → the restricted id is ignored and the normal best-rate badge wins.
|
||||
|
|
@ -365,7 +303,6 @@ internal class SwapProviderResolverTest {
|
|||
provider = cex1,
|
||||
needApplyFCARestrictions = true,
|
||||
state = states.getValue(cex1),
|
||||
isSwapBestDexRateEnabled = true,
|
||||
)
|
||||
|
||||
// FCA restrictions globally on suppress the best-rate badge even for non-restricted providers.
|
||||
|
|
@ -388,7 +325,6 @@ internal class SwapProviderResolverTest {
|
|||
provider = cex1,
|
||||
needApplyFCARestrictions = false,
|
||||
state = states.getValue(cex1),
|
||||
isSwapBestDexRateEnabled = true,
|
||||
)
|
||||
|
||||
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.PermissionRequired)
|
||||
|
|
@ -407,7 +343,6 @@ internal class SwapProviderResolverTest {
|
|||
provider = recommended,
|
||||
needApplyFCARestrictions = false,
|
||||
state = states.getValue(recommended),
|
||||
isSwapBestDexRateEnabled = true,
|
||||
)
|
||||
|
||||
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Recommended)
|
||||
|
|
@ -422,7 +357,6 @@ internal class SwapProviderResolverTest {
|
|||
provider = cex1,
|
||||
needApplyFCARestrictions = false,
|
||||
state = states.getValue(cex1),
|
||||
isSwapBestDexRateEnabled = true,
|
||||
)
|
||||
|
||||
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty)
|
||||
|
|
@ -440,7 +374,6 @@ internal class SwapProviderResolverTest {
|
|||
provider = cex2, // not the best
|
||||
needApplyFCARestrictions = false,
|
||||
state = states.getValue(cex2),
|
||||
isSwapBestDexRateEnabled = true,
|
||||
)
|
||||
|
||||
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty)
|
||||
|
|
@ -458,7 +391,6 @@ internal class SwapProviderResolverTest {
|
|||
provider = cex1,
|
||||
needApplyFCARestrictions = false,
|
||||
state = states.getValue(cex1),
|
||||
isSwapBestDexRateEnabled = true,
|
||||
)
|
||||
|
||||
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty)
|
||||
|
|
@ -476,7 +408,6 @@ internal class SwapProviderResolverTest {
|
|||
provider = cex1,
|
||||
needApplyFCARestrictions = false,
|
||||
state = null,
|
||||
isSwapBestDexRateEnabled = true,
|
||||
)
|
||||
|
||||
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.tangem.feature.swap.model
|
|||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
|
||||
import io.mockk.every
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
|
|
@ -11,7 +10,7 @@ import org.junit.jupiter.api.Test
|
|||
*
|
||||
* The getter is `true` when the current loaded swap state needs no approval:
|
||||
* - always when [PermissionDataState.Empty]
|
||||
* - additionally for [PermissionDataState.PermissionSettings] when the integrated-approve toggle is ON
|
||||
* - additionally for [PermissionDataState.PermissionSettings]
|
||||
*/
|
||||
internal class SwapModelIsPermissionNotNeededTest : SwapModelTestBase() {
|
||||
|
||||
|
|
@ -20,11 +19,7 @@ internal class SwapModelIsPermissionNotNeededTest : SwapModelTestBase() {
|
|||
setUpBase()
|
||||
}
|
||||
|
||||
private fun modelWithPermissionState(
|
||||
permissionState: PermissionDataState,
|
||||
isIntegratedApproveEnabled: Boolean,
|
||||
): SwapModel {
|
||||
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns isIntegratedApproveEnabled
|
||||
private fun modelWithPermissionState(permissionState: PermissionDataState): SwapModel {
|
||||
val provider = swapProvider()
|
||||
val model = createModel()
|
||||
model.dataState = model.dataState.copy(
|
||||
|
|
@ -35,48 +30,23 @@ internal class SwapModelIsPermissionNotNeededTest : SwapModelTestBase() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle OFF and Empty permission THEN permission is not needed`() {
|
||||
val model = modelWithPermissionState(PermissionDataState.Empty, isIntegratedApproveEnabled = false)
|
||||
fun `GIVEN Empty permission THEN permission is not needed`() {
|
||||
val model = modelWithPermissionState(PermissionDataState.Empty)
|
||||
|
||||
assertThat(model.isPermissionNotNeeded).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle OFF and PermissionSettings THEN permission is needed`() {
|
||||
val model = modelWithPermissionState(permissionSettings(), isIntegratedApproveEnabled = false)
|
||||
fun `GIVEN PermissionSettings THEN permission is not needed`() {
|
||||
val model = modelWithPermissionState(permissionSettings())
|
||||
|
||||
assertThat(model.isPermissionNotNeeded).isFalse()
|
||||
assertThat(model.isPermissionNotNeeded).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle OFF and PermissionRequired THEN permission is needed`() {
|
||||
fun `GIVEN PermissionRequired THEN permission is needed`() {
|
||||
val model = modelWithPermissionState(
|
||||
PermissionDataState.PermissionRequired(isResetApproval = false, spenderAddress = "0x"),
|
||||
isIntegratedApproveEnabled = false,
|
||||
)
|
||||
|
||||
assertThat(model.isPermissionNotNeeded).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle ON and Empty permission THEN permission is not needed`() {
|
||||
val model = modelWithPermissionState(PermissionDataState.Empty, isIntegratedApproveEnabled = true)
|
||||
|
||||
assertThat(model.isPermissionNotNeeded).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle ON and PermissionSettings THEN permission is not needed`() {
|
||||
val model = modelWithPermissionState(permissionSettings(), isIntegratedApproveEnabled = true)
|
||||
|
||||
assertThat(model.isPermissionNotNeeded).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle ON and PermissionRequired THEN permission is needed`() {
|
||||
val model = modelWithPermissionState(
|
||||
PermissionDataState.PermissionRequired(isResetApproval = false, spenderAddress = "0x"),
|
||||
isIntegratedApproveEnabled = true,
|
||||
)
|
||||
|
||||
assertThat(model.isPermissionNotNeeded).isFalse()
|
||||
|
|
@ -84,7 +54,6 @@ internal class SwapModelIsPermissionNotNeededTest : SwapModelTestBase() {
|
|||
|
||||
@Test
|
||||
fun `GIVEN no current loaded state THEN permission is needed`() {
|
||||
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns true
|
||||
val model = createModel()
|
||||
|
||||
assertThat(model.isPermissionNotNeeded).isFalse()
|
||||
|
|
|
|||
|
|
@ -130,7 +130,6 @@ internal abstract class SwapModelTestBase {
|
|||
coEvery { shouldShowStoriesUseCase.invokeSync(any()) } returns false
|
||||
coEvery { initialCurrenciesResolver.invoke(any(), any(), any(), any()) } returns (null to null)
|
||||
every { getSelectedAppCurrencyUseCase.invoke() } returns emptyFlow()
|
||||
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns true
|
||||
}
|
||||
|
||||
protected fun createParams(): SwapComponent.Params = SwapComponent.Params(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue