diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index 8d32208885..6734b93464 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -9,6 +9,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -108,4 +109,12 @@ internal object ManageTokensDomainModule { fun provideCheckCurrencyUnsupportedUseCase(repository: ManageTokensRepository): CheckCurrencyUnsupportedUseCase { return CheckCurrencyUnsupportedUseCase(repository) } + + @Provides + @Singleton + fun provideDistinctManagedCurrenciesTokenUseCase( + coroutineDispatchersProvider: CoroutineDispatcherProvider, + ): GetDistinctManagedCurrenciesUseCase { + return GetDistinctManagedCurrenciesUseCase(coroutineDispatchersProvider) + } } \ No newline at end of file diff --git a/domain/manage-tokens/build.gradle.kts b/domain/manage-tokens/build.gradle.kts index 958503009f..da0b3d8595 100644 --- a/domain/manage-tokens/build.gradle.kts +++ b/domain/manage-tokens/build.gradle.kts @@ -22,4 +22,10 @@ dependencies { /* Core */ api(projects.core.pagination) + testImplementation(projects.core.pagination) + + /* Tests */ + testImplementation(deps.test.junit) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.truth) } \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetDistinctManagedCurrenciesUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetDistinctManagedCurrenciesUseCase.kt new file mode 100644 index 0000000000..605a11db0f --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetDistinctManagedCurrenciesUseCase.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.managetokens + +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.pagination.Batch +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +class GetDistinctManagedCurrenciesUseCase( + private val coroutineDispatchersProvider: CoroutineDispatcherProvider, +) { + + // FIXME: Add interception functionality to BatchFlow state and call this on domain + // [REDACTED_JIRA] + suspend operator fun invoke( + batches: List>>, + ): List>> = withContext(coroutineDispatchersProvider.default) { + val seenIds = mutableSetOf() + + batches.map { batch -> + val filtered = batch.data.filter { currency -> + seenIds.add(currency.id) + } + batch.copy(data = filtered) + } + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/test/kotlin/com/tangem/domain/managetokens/GetDistinctManagedCurrenciesUseCaseTest.kt b/domain/manage-tokens/src/test/kotlin/com/tangem/domain/managetokens/GetDistinctManagedCurrenciesUseCaseTest.kt new file mode 100644 index 0000000000..69c9a4370c --- /dev/null +++ b/domain/manage-tokens/src/test/kotlin/com/tangem/domain/managetokens/GetDistinctManagedCurrenciesUseCaseTest.kt @@ -0,0 +1,112 @@ +package com.tangem.domain.managetokens + +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import com.tangem.pagination.Batch +import org.junit.Test +import java.util.UUID +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest + +class GetDistinctManagedTokensUseCaseTest { + + private val useCase = GetDistinctManagedCurrenciesUseCase(TestingCoroutineDispatcherProvider()) + + @Test + fun `GIVEN generated batch with multiple duplicates WHEN invoke THEN batches with unique manage currencies`() = + runTest { + // GIVEN + val totalBatches = 10 + val batchSize = 20 + val duplicatePerBatch = 5 + val uniquePerBatch = batchSize - duplicatePerBatch + val globalTokens = mutableListOf() + val batches = mutableListOf>>() + + repeat(totalBatches) { batchIndex -> + val duplicates = if (batchIndex == 0) { + emptyList() + } else { + globalTokens.take(duplicatePerBatch) + } + + val newTokens = List(uniquePerBatch) { + val token = testToken(id = "token_${batchIndex}_$it") + globalTokens += token + token + } + + val batch = Batch(batchIndex, duplicates + newTokens) + batches += batch + } + + // WHEN + val result = useCase(batches) + + // THEN + val seenIds = mutableSetOf() + + result.forEach { batch -> + batch.data.forEach { token -> + assertThat(seenIds).doesNotContain(token.id) + seenIds.add(token.id) + } + } + + val totalTokens = result.sumOf { it.data.size } + assertThat(totalTokens).isEqualTo(totalBatches * (batchSize - duplicatePerBatch)) + } + + @Test + fun `GIVEN static batch with multiple duplicates WHEN invoke THEN batches with unique manage currencies`() = + runTest { + // GIVEN + val token1 = testToken(id = "A", name = "Delta") + val token2 = testToken(id = "B", name = "Delta") + val token3 = testToken(id = "C", name = "Delta") + val token4 = testToken(id = "D", name = "Delta") + val token5 = testToken(id = "E", name = "Delta") + val token6 = testToken(id = "F", name = "Delta") + val token7 = testToken(id = "G", name = "Delta") + val token8 = testToken(id = "H", name = "Delta") + val token9 = testToken(id = "I", name = "Delta") + val token10 = testToken(id = "J", name = "Delta") + + val batches = listOf( + Batch(0, listOf(token1, token2, token3, token4, token5)), + Batch(1, listOf(token1, token7, token8, token9, token6)), + Batch(2, listOf(token2, token8, token8, token6, token10)), + ) + + // WHEN + val result = useCase(batches) + + // THEN + val seenIds = mutableSetOf() + + result.forEach { batch -> + batch.data.forEach { token -> + assertThat(seenIds).doesNotContain(token.id) + seenIds.add(token.id) + } + } + + val totalTokens = result.sumOf { it.data.size } + assertThat(totalTokens).isEqualTo(10) + } +} + +fun testToken( + id: String = UUID.randomUUID().toString(), + name: String = "Test Token", + symbol: String = "TTK", +): ManagedCryptoCurrency { + return ManagedCryptoCurrency.Token( + id = ManagedCryptoCurrency.ID(id), + name = name, + symbol = symbol, + iconUrl = "null", + availableNetworks = emptyList(), + addedIn = emptySet(), + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt index 7dde531506..d9052b18b2 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -1,7 +1,5 @@ package com.tangem.features.managetokens.utils.list -import androidx.compose.ui.util.fastForEachIndexed -import androidx.compose.ui.util.fastMap import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped @@ -10,10 +8,7 @@ import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase -import com.tangem.domain.managetokens.CheckHasLinkedTokensUseCase -import com.tangem.domain.managetokens.GetManagedTokensUseCase -import com.tangem.domain.managetokens.RemoveCustomManagedCryptoCurrencyUseCase +import com.tangem.domain.managetokens.* import com.tangem.domain.managetokens.model.* import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId @@ -43,6 +38,7 @@ import javax.inject.Inject @ModelScoped internal class ManageTokensListManager @Inject constructor( private val getManagedTokensUseCase: GetManagedTokensUseCase, + private val getDistinctManagedTokensUseCase: GetDistinctManagedCurrenciesUseCase, private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase, private val removeCustomCurrencyUseCase: RemoveCustomManagedCryptoCurrencyUseCase, private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase, @@ -166,47 +162,27 @@ internal class ManageTokensListManager @Inject constructor( return } - state.update { state -> - val newBatches = distinctCurrencies(batchListState.data) - val currentBatches = state.currencyBatches + scope.launch { + state.update { state -> + val newBatches = getDistinctManagedTokensUseCase(batchListState.data) + val currentBatches = state.currencyBatches - // Distinct until changed - if (newBatches.size == currentBatches.size && - newBatches.map { it.key } == currentBatches.map { it.key } && - newBatches.flatMap { it.data } == currentBatches.flatMap { it.data } - ) { - return - } - - val canEditItems = userWalletId != null - state.copy( - userWalletId = userWalletId, - currencyBatches = newBatches, - uiBatches = uiManager.createOrUpdateUiBatches(newBatches, canEditItems), - canEditItems = canEditItems, - ) - } - } - - // FIXME: Add interception functionality to BatchFlow state and do this on domain - // [REDACTED_JIRA] - private fun distinctCurrencies( - batches: List>>, - ): List>> { - val allCurrenciesIds = mutableListOf() - - return batches.fastMap { batch -> - val batchCurrencies = batch.data.toMutableList() - - batch.data.fastForEachIndexed { index, currency -> - if (currency.id in allCurrenciesIds) { - batchCurrencies.removeAt(index) - } else { - allCurrenciesIds.add(currency.id) + // Distinct until changed + if (newBatches.size == currentBatches.size && + newBatches.map { it.key } == currentBatches.map { it.key } && + newBatches.flatMap { it.data } == currentBatches.flatMap { it.data } + ) { + return@launch } - } - batch.copy(data = batchCurrencies) + val canEditItems = userWalletId != null + state.copy( + userWalletId = userWalletId, + currencyBatches = newBatches, + uiBatches = uiManager.createOrUpdateUiBatches(newBatches, canEditItems), + canEditItems = canEditItems, + ) + } } }