Updated on 2026-08-14
This commit is contained in:
parent
2e8d0bc03b
commit
298e7a5565
29 changed files with 687 additions and 43 deletions
|
|
@ -34,4 +34,5 @@ dependencies {
|
|||
testImplementation(deps.test.junit5)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.coroutine)
|
||||
}
|
||||
|
|
@ -38,11 +38,14 @@ fun SwapCurrencies.getGroupWithDirection(swapDirection: SwapDirection): SwapCurr
|
|||
* @param available list of available currencies to swap
|
||||
* @param available list of unavailable currencies to swap
|
||||
* @param isAfterSearch flag indicates whether user searched token
|
||||
* @param availableForSwap currencies that are unavailable for the current flow (e.g. Send with Swap), but
|
||||
* available in the regular Swap flow. Empty by default for backward compatibility (regular Swap doesn't fill it).
|
||||
*/
|
||||
data class SwapCurrenciesGroup(
|
||||
val available: List<SwapCryptoCurrency>,
|
||||
val unavailable: List<SwapCryptoCurrency>,
|
||||
val isAfterSearch: Boolean,
|
||||
val availableForSwap: List<SwapCryptoCurrency> = emptyList(),
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -24,11 +24,13 @@ class GetSwapSupportedPairsUseCase(
|
|||
filterProviderTypes: List<ExpressProviderType>,
|
||||
swapTxType: SwapTxType,
|
||||
) = Either.catch {
|
||||
// Request all provider types so the use case can tell apart currencies available for the current flow
|
||||
// from those available only in the regular Swap flow. The current-flow filter is applied below.
|
||||
val pairs = swapRepositoryV2.getSupportedPairs(
|
||||
userWallet = userWallet,
|
||||
initialCurrency = initialCurrency,
|
||||
cryptoCurrencyList = cryptoCurrencyList,
|
||||
filterProviderTypes = filterProviderTypes,
|
||||
filterProviderTypes = emptyList(),
|
||||
swapTxType = swapTxType,
|
||||
)
|
||||
|
||||
|
|
@ -42,12 +44,14 @@ class GetSwapSupportedPairsUseCase(
|
|||
filteringCurrency = { it.from },
|
||||
groupingCurrency = { it.to },
|
||||
cryptoCurrencyList = filteredOutInitial,
|
||||
allowedProviderTypes = filterProviderTypes,
|
||||
)
|
||||
val toGroup = pairs.groupPairs(
|
||||
initialCurrency = initialCurrency,
|
||||
filteringCurrency = { it.to },
|
||||
groupingCurrency = { it.from },
|
||||
cryptoCurrencyList = filteredOutInitial,
|
||||
allowedProviderTypes = filterProviderTypes,
|
||||
)
|
||||
|
||||
SwapCurrencies(
|
||||
|
|
@ -61,8 +65,9 @@ class GetSwapSupportedPairsUseCase(
|
|||
filteringCurrency: (SwapPairModel) -> CryptoCurrencyStatus,
|
||||
groupingCurrency: (SwapPairModel) -> CryptoCurrencyStatus,
|
||||
cryptoCurrencyList: List<CryptoCurrency>,
|
||||
allowedProviderTypes: List<ExpressProviderType>,
|
||||
): SwapCurrenciesGroup {
|
||||
val availableCryptoCurrencies = asSequence()
|
||||
val candidates = asSequence()
|
||||
.filter { pair ->
|
||||
filteringCurrency(pair).currency.id.rawCurrencyId == initialCurrency.id.rawCurrencyId
|
||||
}
|
||||
|
|
@ -75,18 +80,32 @@ class GetSwapSupportedPairsUseCase(
|
|||
.map { pair ->
|
||||
val toCurrency = groupingCurrency(pair)
|
||||
val isTxExtrasSupported = toCurrency.currency.network.transactionExtrasType.isTxExtrasSupported()
|
||||
val filteredProviders = if (isTxExtrasSupported) {
|
||||
pair.providers.filter { it.isExtraIdSupported }
|
||||
} else {
|
||||
pair.providers
|
||||
}
|
||||
SwapCryptoCurrency(toCurrency, filteredProviders)
|
||||
// Providers eligible for the current flow (e.g. Send with Swap): extra-id support + allowed type
|
||||
val eligibleProviders = pair.providers
|
||||
.filter { !isTxExtrasSupported || it.isExtraIdSupported }
|
||||
.filter { allowedProviderTypes.isEmpty() || it.type in allowedProviderTypes }
|
||||
// The pair has any provider => it can still be swapped in the regular Swap flow
|
||||
SwapCryptoCurrency(toCurrency, eligibleProviders) to pair.providers.isNotEmpty()
|
||||
}
|
||||
.filter { it.providers.isNotEmpty() }
|
||||
.toList()
|
||||
|
||||
val unavailableCryptoCurrencies =
|
||||
cryptoCurrencyList - availableCryptoCurrencies.map { it.currencyStatus.currency }.toSet()
|
||||
val availableCryptoCurrencies = candidates
|
||||
.filter { (currency, _) -> currency.providers.isNotEmpty() }
|
||||
.map { (currency, _) -> currency }
|
||||
val availableCurrencies = availableCryptoCurrencies.map { it.currencyStatus.currency }.toSet()
|
||||
|
||||
// Currencies with no providers for the current flow, but available in the regular Swap flow
|
||||
val availableForSwapCryptoCurrencies = candidates
|
||||
.filter { (currency, isAvailableForRegularSwap) ->
|
||||
currency.providers.isEmpty() && isAvailableForRegularSwap
|
||||
}
|
||||
.map { (currency, _) -> currency }
|
||||
.distinctBy { it.currencyStatus.currency }
|
||||
.filterNot { it.currencyStatus.currency in availableCurrencies }
|
||||
|
||||
val usedCurrencies = availableCurrencies +
|
||||
availableForSwapCryptoCurrencies.map { it.currencyStatus.currency }.toSet()
|
||||
val unavailableCryptoCurrencies = cryptoCurrencyList - usedCurrencies
|
||||
|
||||
return SwapCurrenciesGroup(
|
||||
available = availableCryptoCurrencies,
|
||||
|
|
@ -100,6 +119,7 @@ class GetSwapSupportedPairsUseCase(
|
|||
)
|
||||
},
|
||||
isAfterSearch = false,
|
||||
availableForSwap = availableForSwapCryptoCurrencies,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
package com.tangem.domain.swap.usecase
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.express.models.ExpressProvider
|
||||
import com.tangem.domain.express.models.ExpressProviderType
|
||||
import com.tangem.domain.express.models.ExpressRateType
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.swap.SwapErrorResolver
|
||||
import com.tangem.domain.swap.SwapRepositoryV2
|
||||
import com.tangem.domain.swap.models.SwapPairModel
|
||||
import com.tangem.domain.swap.models.SwapTxType
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class GetSwapSupportedPairsUseCaseTest {
|
||||
|
||||
private val swapRepositoryV2: SwapRepositoryV2 = mockk()
|
||||
private val swapErrorResolver: SwapErrorResolver = mockk()
|
||||
|
||||
private val useCase = GetSwapSupportedPairsUseCase(
|
||||
swapRepositoryV2 = swapRepositoryV2,
|
||||
swapErrorResolver = swapErrorResolver,
|
||||
)
|
||||
|
||||
private val initialCurrency = createCurrency("initial")
|
||||
private val cexCurrency = createCurrency("cex")
|
||||
private val dexOnlyCurrency = createCurrency("dex-only")
|
||||
private val unavailableCurrency = createCurrency("unavailable")
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
clearMocks(swapRepositoryV2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN cex, dex-only and missing currencies WHEN invoke THEN split into three buckets`() = runTest {
|
||||
// Arrange
|
||||
val pairs = listOf(
|
||||
createPair(from = initialCurrency, to = cexCurrency, providers = listOf(cexProvider)),
|
||||
createPair(from = initialCurrency, to = dexOnlyCurrency, providers = listOf(dexProvider)),
|
||||
// unavailableCurrency intentionally has no pair at all
|
||||
)
|
||||
coEvery {
|
||||
swapRepositoryV2.getSupportedPairs(any(), any(), any(), any(), any())
|
||||
} returns pairs
|
||||
|
||||
// Act
|
||||
val result = useCase.invoke(
|
||||
userWallet = userWallet,
|
||||
initialCurrency = initialCurrency,
|
||||
cryptoCurrencyList = listOf(initialCurrency, cexCurrency, dexOnlyCurrency, unavailableCurrency),
|
||||
filterProviderTypes = listOf(ExpressProviderType.CEX),
|
||||
swapTxType = SwapTxType.SendWithSwap,
|
||||
)
|
||||
|
||||
// Assert
|
||||
val fromGroup = result.getOrNull()!!.fromGroup
|
||||
assertThat(fromGroup.available.map { it.currencyStatus.currency }).containsExactly(cexCurrency)
|
||||
assertThat(fromGroup.availableForSwap.map { it.currencyStatus.currency }).containsExactly(dexOnlyCurrency)
|
||||
assertThat(fromGroup.unavailable.map { it.currencyStatus.currency }).containsExactly(unavailableCurrency)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN dex pair AND no type restriction WHEN invoke THEN currency is available not availableForSwap`() =
|
||||
runTest {
|
||||
// Arrange — allowedProviderTypes empty means any provider type is eligible for the current flow
|
||||
val pairs = listOf(
|
||||
createPair(from = initialCurrency, to = dexOnlyCurrency, providers = listOf(dexProvider)),
|
||||
)
|
||||
coEvery {
|
||||
swapRepositoryV2.getSupportedPairs(any(), any(), any(), any(), any())
|
||||
} returns pairs
|
||||
|
||||
// Act
|
||||
val result = useCase.invoke(
|
||||
userWallet = userWallet,
|
||||
initialCurrency = initialCurrency,
|
||||
cryptoCurrencyList = listOf(initialCurrency, dexOnlyCurrency),
|
||||
filterProviderTypes = emptyList(),
|
||||
swapTxType = SwapTxType.SendWithSwap,
|
||||
)
|
||||
|
||||
// Assert
|
||||
val fromGroup = result.getOrNull()!!.fromGroup
|
||||
assertThat(fromGroup.available.map { it.currencyStatus.currency }).containsExactly(dexOnlyCurrency)
|
||||
assertThat(fromGroup.availableForSwap).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN memo network with provider without extra-id WHEN invoke THEN currency is availableForSwap`() = runTest {
|
||||
// Arrange — to-network requires extra id, but the only CEX provider doesn't support it
|
||||
val memoCurrency = createCurrency(rawId = "memo", txExtras = Network.TransactionExtrasType.MEMO)
|
||||
val pairs = listOf(
|
||||
createPair(from = initialCurrency, to = memoCurrency, providers = listOf(cexProviderNoExtraId)),
|
||||
)
|
||||
coEvery {
|
||||
swapRepositoryV2.getSupportedPairs(any(), any(), any(), any(), any())
|
||||
} returns pairs
|
||||
|
||||
// Act
|
||||
val result = useCase.invoke(
|
||||
userWallet = userWallet,
|
||||
initialCurrency = initialCurrency,
|
||||
cryptoCurrencyList = listOf(initialCurrency, memoCurrency),
|
||||
filterProviderTypes = listOf(ExpressProviderType.CEX),
|
||||
swapTxType = SwapTxType.SendWithSwap,
|
||||
)
|
||||
|
||||
// Assert
|
||||
val fromGroup = result.getOrNull()!!.fromGroup
|
||||
assertThat(fromGroup.available).isEmpty()
|
||||
assertThat(fromGroup.availableForSwap.map { it.currencyStatus.currency }).containsExactly(memoCurrency)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN pair with both cex and dex providers WHEN invoke THEN currency is available not availableForSwap`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
val pairs = listOf(
|
||||
createPair(from = initialCurrency, to = cexCurrency, providers = listOf(cexProvider, dexProvider)),
|
||||
)
|
||||
coEvery {
|
||||
swapRepositoryV2.getSupportedPairs(any(), any(), any(), any(), any())
|
||||
} returns pairs
|
||||
|
||||
// Act
|
||||
val result = useCase.invoke(
|
||||
userWallet = userWallet,
|
||||
initialCurrency = initialCurrency,
|
||||
cryptoCurrencyList = listOf(initialCurrency, cexCurrency),
|
||||
filterProviderTypes = listOf(ExpressProviderType.CEX),
|
||||
swapTxType = SwapTxType.SendWithSwap,
|
||||
)
|
||||
|
||||
// Assert
|
||||
val fromGroup = result.getOrNull()!!.fromGroup
|
||||
assertThat(fromGroup.available.map { it.currencyStatus.currency }).containsExactly(cexCurrency)
|
||||
assertThat(fromGroup.availableForSwap).isEmpty()
|
||||
}
|
||||
|
||||
private fun createPair(from: CryptoCurrency, to: CryptoCurrency, providers: List<ExpressProvider>) = SwapPairModel(
|
||||
from = CryptoCurrencyStatus(currency = from, value = mockk(relaxed = true)),
|
||||
to = CryptoCurrencyStatus(currency = to, value = mockk(relaxed = true)),
|
||||
providers = providers,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
val userWallet: UserWallet = mockk(relaxed = true)
|
||||
|
||||
val cexProvider = createProvider("cex-1", ExpressProviderType.CEX, isExtraIdSupported = true)
|
||||
val cexProviderNoExtraId = createProvider("cex-2", ExpressProviderType.CEX, isExtraIdSupported = false)
|
||||
val dexProvider = createProvider("dex-1", ExpressProviderType.DEX, isExtraIdSupported = false)
|
||||
|
||||
fun createProvider(id: String, type: ExpressProviderType, isExtraIdSupported: Boolean) = ExpressProvider(
|
||||
providerId = id,
|
||||
rateTypes = listOf(ExpressRateType.Float),
|
||||
name = id,
|
||||
type = type,
|
||||
imageLarge = "",
|
||||
termsOfUse = null,
|
||||
privacyPolicy = null,
|
||||
slippage = null,
|
||||
isExtraIdSupported = isExtraIdSupported,
|
||||
)
|
||||
|
||||
fun createCurrency(
|
||||
rawId: String,
|
||||
txExtras: Network.TransactionExtrasType = Network.TransactionExtrasType.NONE,
|
||||
): CryptoCurrency {
|
||||
val id: CryptoCurrency.ID = mockk {
|
||||
every { rawCurrencyId } returns CryptoCurrency.RawID(rawId)
|
||||
every { rawNetworkId } returns rawId
|
||||
}
|
||||
return mockk<CryptoCurrency>(relaxed = true).also { currency ->
|
||||
every { currency.id } returns id
|
||||
every { currency.network.transactionExtrasType } returns txExtras
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue