Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-13 10:04:27 +02:00
parent 2091e5eecd
commit 16b069cf7a
2 changed files with 170 additions and 9 deletions

View file

@ -532,16 +532,21 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
private fun List<ExpressProvider>.filterYieldSupplyProvider(
cryptoCurrencyStatus: CryptoCurrencyStatus?,
): List<ExpressProvider> {
val isYieldSupplyActive = cryptoCurrencyStatus?.value?.yieldSupplyStatus?.isActive == true
if (!isYieldSupplyActive) return this
return if (featureTogglesManager.isFeatureEnabled(FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED)) {
this
} else {
filter { provider ->
if (cryptoCurrencyStatus?.value?.yieldSupplyStatus?.isActive == true) {
provider.type == ExpressProviderType.CEX
} else {
true
when (provider.type) {
ExpressProviderType.CEX -> true
ExpressProviderType.DEX,
ExpressProviderType.DEX_BRIDGE,
-> provider.providerId in YIELD_ALLOWED_DEX_PROVIDER_IDS
ExpressProviderType.ONRAMP -> false
}
}
} else {
filter { it.type == ExpressProviderType.CEX }
}
}
}
@ -552,6 +557,17 @@ private val MEMO_RESTRICTED_NETWORKS = setOf(
Blockchain.Algorand.toNetworkId(),
)
/**
* DEX providers allowed for token swaps in yield mode.
* Mirrors iOS ExpressConstants.yieldModuleDEXProviderIds (PR #4998).
*/
private val YIELD_ALLOWED_DEX_PROVIDER_IDS = setOf(
"1inch",
"li-fi",
"okx-cross-chain",
"okx-on-chain",
)
private suspend fun ExpressRepository.getFilteredProviders(
userWallet: UserWallet,
filterProviderTypes: List<ExpressProviderType>,

View file

@ -539,7 +539,7 @@ internal class DefaultSwapRepositoryV2Test {
}
@Test
fun `getPairs keeps DEX providers when yield supply is active and flag is on`() = runTest {
fun `GIVEN yield active and flag on WHEN getPairs THEN non-allowlisted DEX filtered out`() = runTest {
// Arrange
every { featureTogglesManager.isFeatureEnabled(FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED) } returns true
val primaryStatus = createCryptoCurrencyStatusWithActiveYield(primaryCoin)
@ -580,10 +580,155 @@ internal class DefaultSwapRepositoryV2Test {
swapTxType = SwapTxType.Swap,
)
// Assert — both providers should remain
// Assert — non-allowlisted DEX (dex-provider-1) dropped, only CEX remains
assertThat(result).hasSize(2)
val providers = result.first().providers
assertThat(providers).hasSize(2)
assertThat(providers).hasSize(1)
assertThat(providers.first().type).isEqualTo(ExpressProviderType.CEX)
}
@Test
fun `GIVEN yield active and flag on WHEN getPairs THEN allowlisted DEX kept with CEX`() = runTest {
// Arrange
every { featureTogglesManager.isFeatureEnabled(FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED) } returns true
val primaryStatus = createCryptoCurrencyStatusWithActiveYield(primaryCoin)
val secondaryStatus = createCryptoCurrencyStatus(secondaryCoin)
val primarySwapCurrencyStatus = SwapCurrencyStatus(userWallet = userWallet, status = primaryStatus, account = mockk())
val secondarySwapCurrencyStatus = SwapCurrencyStatus(userWallet = userWallet, status = secondaryStatus, account = mockk())
val allowedDexProvider = dexProvider.copy(providerId = "1inch")
val swapPair = SwapPair(
from = LeastTokenInfo(contractAddress = "0", network = ETH_BACKEND_ID),
to = LeastTokenInfo(contractAddress = "0", network = BTC_BACKEND_ID),
providers = listOf(
SwapPairProvider(providerId = "1inch", rateTypes = listOf(RateType.FLOAT)),
SwapPairProvider(providerId = CEX_PROVIDER_ID, rateTypes = listOf(RateType.FLOAT)),
),
)
coEvery { tangemExpressApi.getPairs(any(), any(), any()) } returns ApiResponse.Success(listOf(swapPair))
coEvery { expressRepository.getProviders(any(), any()) } returns listOf(allowedDexProvider, cexProvider)
// Act
val result = repository.getPairs(
primarySwapCurrencyStatus = primarySwapCurrencyStatus,
secondarySwapCurrencyStatus = secondarySwapCurrencyStatus,
filterProviderTypes = emptyList(),
swapTxType = SwapTxType.Swap,
)
// Assert — allow-listed DEX (1inch) + CEX both kept
assertThat(result).hasSize(2)
val providers = result.first().providers
assertThat(providers.map { it.providerId }).containsExactly("1inch", CEX_PROVIDER_ID)
}
@Test
fun `GIVEN yield active and flag on WHEN getPairs THEN allowlisted DEX_BRIDGE kept with CEX`() = runTest {
// Arrange
every { featureTogglesManager.isFeatureEnabled(FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED) } returns true
val primaryStatus = createCryptoCurrencyStatusWithActiveYield(primaryCoin)
val secondaryStatus = createCryptoCurrencyStatus(secondaryCoin)
val primarySwapCurrencyStatus = SwapCurrencyStatus(userWallet = userWallet, status = primaryStatus, account = mockk())
val secondarySwapCurrencyStatus = SwapCurrencyStatus(userWallet = userWallet, status = secondaryStatus, account = mockk())
val lifiProvider = dexProvider.copy(providerId = "li-fi", type = ExpressProviderType.DEX_BRIDGE)
val swapPair = SwapPair(
from = LeastTokenInfo(contractAddress = "0", network = ETH_BACKEND_ID),
to = LeastTokenInfo(contractAddress = "0", network = BTC_BACKEND_ID),
providers = listOf(
SwapPairProvider(providerId = "li-fi", rateTypes = listOf(RateType.FLOAT)),
SwapPairProvider(providerId = CEX_PROVIDER_ID, rateTypes = listOf(RateType.FLOAT)),
),
)
coEvery { tangemExpressApi.getPairs(any(), any(), any()) } returns ApiResponse.Success(listOf(swapPair))
coEvery { expressRepository.getProviders(any(), any()) } returns listOf(lifiProvider, cexProvider)
// Act
val result = repository.getPairs(
primarySwapCurrencyStatus = primarySwapCurrencyStatus,
secondarySwapCurrencyStatus = secondarySwapCurrencyStatus,
filterProviderTypes = emptyList(),
swapTxType = SwapTxType.Swap,
)
// Assert — allow-listed DEX_BRIDGE (li-fi) + CEX both kept
assertThat(result).hasSize(2)
val providers = result.first().providers
assertThat(providers.map { it.providerId }).containsExactly("li-fi", CEX_PROVIDER_ID)
}
@Test
fun `GIVEN yield active and flag on WHEN getPairs THEN ONRAMP provider filtered out`() = runTest {
// Arrange
every { featureTogglesManager.isFeatureEnabled(FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED) } returns true
val primaryStatus = createCryptoCurrencyStatusWithActiveYield(primaryCoin)
val secondaryStatus = createCryptoCurrencyStatus(secondaryCoin)
val primarySwapCurrencyStatus = SwapCurrencyStatus(userWallet = userWallet, status = primaryStatus, account = mockk())
val secondarySwapCurrencyStatus = SwapCurrencyStatus(userWallet = userWallet, status = secondaryStatus, account = mockk())
val onrampProvider = dexProvider.copy(providerId = "onramp-provider", type = ExpressProviderType.ONRAMP)
val swapPair = SwapPair(
from = LeastTokenInfo(contractAddress = "0", network = ETH_BACKEND_ID),
to = LeastTokenInfo(contractAddress = "0", network = BTC_BACKEND_ID),
providers = listOf(
SwapPairProvider(providerId = "onramp-provider", rateTypes = listOf(RateType.FLOAT)),
SwapPairProvider(providerId = CEX_PROVIDER_ID, rateTypes = listOf(RateType.FLOAT)),
),
)
coEvery { tangemExpressApi.getPairs(any(), any(), any()) } returns ApiResponse.Success(listOf(swapPair))
coEvery { expressRepository.getProviders(any(), any()) } returns listOf(onrampProvider, cexProvider)
// Act
val result = repository.getPairs(
primarySwapCurrencyStatus = primarySwapCurrencyStatus,
secondarySwapCurrencyStatus = secondarySwapCurrencyStatus,
filterProviderTypes = emptyList(),
swapTxType = SwapTxType.Swap,
)
// Assert — ONRAMP dropped, only CEX remains
assertThat(result).hasSize(2)
val providers = result.first().providers
assertThat(providers).hasSize(1)
assertThat(providers.first().type).isEqualTo(ExpressProviderType.CEX)
}
@Test
fun `GIVEN yield inactive and flag on WHEN getPairs THEN all providers kept`() = runTest {
// Arrange
every { featureTogglesManager.isFeatureEnabled(FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED) } returns true
val primaryStatus = createCryptoCurrencyStatus(primaryCoin)
val secondaryStatus = createCryptoCurrencyStatus(secondaryCoin)
val primarySwapCurrencyStatus = SwapCurrencyStatus(userWallet = userWallet, status = primaryStatus, account = mockk())
val secondarySwapCurrencyStatus = SwapCurrencyStatus(userWallet = userWallet, status = secondaryStatus, account = mockk())
val swapPair = SwapPair(
from = LeastTokenInfo(contractAddress = "0", network = ETH_BACKEND_ID),
to = LeastTokenInfo(contractAddress = "0", network = BTC_BACKEND_ID),
providers = listOf(
SwapPairProvider(providerId = PROVIDER_ID, rateTypes = listOf(RateType.FLOAT)),
SwapPairProvider(providerId = CEX_PROVIDER_ID, rateTypes = listOf(RateType.FLOAT)),
),
)
coEvery { tangemExpressApi.getPairs(any(), any(), any()) } returns ApiResponse.Success(listOf(swapPair))
coEvery { expressRepository.getProviders(any(), any()) } returns listOf(dexProvider, cexProvider)
// Act
val result = repository.getPairs(
primarySwapCurrencyStatus = primarySwapCurrencyStatus,
secondarySwapCurrencyStatus = secondarySwapCurrencyStatus,
filterProviderTypes = emptyList(),
swapTxType = SwapTxType.Swap,
)
// Assert — yield inactive → non-allow-listed DEX (dex-provider-1) survives alongside CEX
assertThat(result).hasSize(2)
val providers = result.first().providers
assertThat(providers.map { it.providerId }).containsExactly(PROVIDER_ID, CEX_PROVIDER_ID)
}
// endregion