diff --git a/data/networks/src/main/java/com/tangem/data/networks/di/NetworkDataModule.kt b/data/networks/src/main/java/com/tangem/data/networks/di/NetworkDataModule.kt index 6bf4e36673..e2f4c82b9c 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/di/NetworkDataModule.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/di/NetworkDataModule.kt @@ -8,6 +8,7 @@ import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.networks.repository.DefaultNetworksRepository import com.tangem.data.networks.store.DefaultNetworksStatusesStore import com.tangem.data.networks.store.NetworksStatusesStore +import com.tangem.data.networks.utils.DefaultNetworksCleaner import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.local.network.entity.NetworkStatusDM @@ -15,6 +16,7 @@ import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.mapWithStringKeyTypes import com.tangem.datasource.utils.setTypes import com.tangem.domain.networks.repository.NetworksRepository +import com.tangem.domain.networks.utils.NetworksCleaner import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -67,4 +69,18 @@ internal object NetworkDataModule { dispatchers = dispatchers, ) } + + @Provides + @Singleton + fun provideNetworksCleaner( + networksStatusesStore: NetworksStatusesStore, + walletManagersFacade: WalletManagersFacade, + dispatchers: CoroutineDispatcherProvider, + ): NetworksCleaner { + return DefaultNetworksCleaner( + networksStatusesStore = networksStatusesStore, + walletManagersFacade = walletManagersFacade, + dispatchers = dispatchers, + ) + } } \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt index ff25a5ad51..213a73be50 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt @@ -96,6 +96,20 @@ internal class DefaultNetworksStatusesStore( } } + override suspend fun clear(userWalletId: UserWalletId, networks: Set) { + persistenceDataStore.updateData { storedStatuses -> + storedStatuses.toMutableMap().apply { + val updatedValues = this[userWalletId.stringValue].orEmpty().filterNot { + networks.any { network -> + it.networkId.value == network.rawId && it.derivationPath.value == network.derivationPath.value + } + } + + this[userWalletId.stringValue] = updatedValues.toSet() + } + } + } + private suspend fun updateInRuntime( userWalletId: UserWalletId, networks: Set, diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStore.kt b/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStore.kt index 30246ff85c..680ee077ab 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStore.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStore.kt @@ -41,4 +41,7 @@ internal interface NetworksStatusesStore { * See complex methods in `NetworksStatusesStoreExt`. */ suspend fun store(userWalletId: UserWalletId, status: NetworkStatus) + + /** Clear statuses of [networks] by [userWalletId] */ + suspend fun clear(userWalletId: UserWalletId, networks: Set) } \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/utils/DefaultNetworksCleaner.kt b/data/networks/src/main/java/com/tangem/data/networks/utils/DefaultNetworksCleaner.kt new file mode 100644 index 0000000000..c4ef782bc9 --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/utils/DefaultNetworksCleaner.kt @@ -0,0 +1,73 @@ +package com.tangem.data.networks.utils + +import com.tangem.data.networks.store.NetworksStatusesStore +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.networks.utils.NetworksCleaner +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * Default implementation of [NetworksCleaner]. + * + * @property networksStatusesStore Store to manage network statuses. + * @property walletManagersFacade Facade to manage wallet managers. + * @property dispatchers Coroutine dispatchers provider. + * +[REDACTED_AUTHOR] + */ +internal class DefaultNetworksCleaner( + private val networksStatusesStore: NetworksStatusesStore, + private val walletManagersFacade: WalletManagersFacade, + private val dispatchers: CoroutineDispatcherProvider, +) : NetworksCleaner { + + override suspend fun invoke(userWalletId: UserWalletId, currencies: List) { + withContext(dispatchers.default) { + val (networks, tokens) = currencies.partitionByType() + + coroutineScope { + launch { cleanStore(userWalletId = userWalletId, networks = networks) } + launch { cleanWalletManager(userWalletId = userWalletId, networks = networks, tokens = tokens) } + } + } + } + + private suspend fun cleanStore(userWalletId: UserWalletId, networks: Set) { + if (networks.isNotEmpty()) { + networksStatusesStore.clear(userWalletId = userWalletId, networks = networks) + } + } + + private suspend fun cleanWalletManager( + userWalletId: UserWalletId, + networks: Set, + tokens: Set, + ) { + if (networks.isNotEmpty()) { + walletManagersFacade.remove(userWalletId = userWalletId, networks = networks) + } + + if (tokens.isNotEmpty()) { + walletManagersFacade.removeTokens(userWalletId = userWalletId, tokens = tokens) + } + } + + private fun List.partitionByType(): Pair, Set> { + val networks = mutableSetOf() + val tokens = mutableSetOf() + + for (currency in this) { + when (currency) { + is CryptoCurrency.Coin -> networks.add(currency.network) + is CryptoCurrency.Token -> tokens.add(currency) + } + } + + return Pair(networks, tokens) + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/utils/DefaultNetworksCleanerTest.kt b/data/networks/src/test/java/com/tangem/data/networks/utils/DefaultNetworksCleanerTest.kt new file mode 100644 index 0000000000..c2913c5f5a --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/utils/DefaultNetworksCleanerTest.kt @@ -0,0 +1,101 @@ +package com.tangem.data.networks.utils + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.data.networks.store.NetworksStatusesStore +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.clearMocks +import io.mockk.coVerifyOrder +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) +class DefaultNetworksCleanerTest { + + private val networksStatusesStore = mockk(relaxed = true) + private val walletManagersFacade = mockk(relaxed = true) + private val cleaner = DefaultNetworksCleaner( + networksStatusesStore = networksStatusesStore, + walletManagersFacade = walletManagersFacade, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + private val userWalletId = UserWalletId("011") + private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() + private val network = cryptoCurrencyFactory.ethereum.network + private val coin = cryptoCurrencyFactory.ethereum + private val token = cryptoCurrencyFactory.createToken(Blockchain.Ethereum) + + @BeforeEach + fun setUp() { + clearMocks(networksStatusesStore, walletManagersFacade) + } + + @Test + fun `should clear networks and remove managers and tokens when called`() = runTest { + // Arrange + val currencies = listOf(coin, token) + + // Act + cleaner(userWalletId = userWalletId, currencies = currencies) + + // Assert + coVerifyOrder { + networksStatusesStore.clear(userWalletId, setOf(network)) + walletManagersFacade.remove(userWalletId = userWalletId, networks = setOf(network)) + walletManagersFacade.removeTokens(userWalletId = userWalletId, tokens = setOf(token)) + } + } + + @Test + fun `should handle empty currencies`() = runTest { + // Act + cleaner(userWalletId = userWalletId, currencies = emptyList()) + + // Assert + coVerifyOrder(inverse = true) { + networksStatusesStore.clear(userWalletId = any(), networks = any()) + walletManagersFacade.remove(userWalletId = any(), networks = any()) + walletManagersFacade.removeTokens(userWalletId = any(), tokens = any()) + } + } + + @Test + fun `should clear only networks when there are no tokens`() = runTest { + val currencies = listOf(coin) + + cleaner(userWalletId = userWalletId, currencies = currencies) + + coVerifyOrder { + networksStatusesStore.clear(userWalletId, setOf(network)) + walletManagersFacade.remove(userWalletId = userWalletId, networks = setOf(network)) + } + + coVerifyOrder(inverse = true) { + walletManagersFacade.removeTokens(userWalletId = any(), tokens = any()) + } + } + + @Test + fun `should clear only tokens when there are no networks`() = runTest { + // Arrange + val currencies = listOf(token) + + // Act + cleaner(userWalletId = userWalletId, currencies = currencies) + + // Assert + coVerifyOrder { + walletManagersFacade.removeTokens(userWalletId = userWalletId, tokens = setOf(token)) + } + + coVerifyOrder(inverse = true) { + networksStatusesStore.clear(userWalletId = any(), networks = any()) + walletManagersFacade.remove(userWalletId = any(), networks = any()) + } + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index a5c5a41e2b..d96e876a73 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -10,6 +10,7 @@ import com.tangem.data.staking.DefaultStakingTransactionHashRepository import com.tangem.data.staking.converters.error.StakeKitErrorConverter import com.tangem.data.staking.store.YieldsBalancesStore import com.tangem.data.staking.toggles.DefaultStakingFeatureToggles +import com.tangem.data.staking.utils.DefaultStakingCleaner import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse import com.tangem.datasource.di.NetworkMoshi @@ -21,6 +22,7 @@ import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.repositories.StakingTransactionHashRepository import com.tangem.domain.staking.toggles.StakingFeatureToggles +import com.tangem.domain.staking.utils.StakingCleaner import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -102,4 +104,16 @@ internal object StakingDataModule { fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): StakingFeatureToggles { return DefaultStakingFeatureToggles(featureTogglesManager) } + + @Provides + @Singleton + fun provideStakingCleaner( + yieldsBalancesStore: YieldsBalancesStore, + dispatchers: CoroutineDispatcherProvider, + ): StakingCleaner { + return DefaultStakingCleaner( + yieldsBalancesStore = yieldsBalancesStore, + dispatchers = dispatchers, + ) + } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt b/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt index 40fbee1534..fa753503be 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt @@ -96,6 +96,16 @@ internal class DefaultYieldsBalancesStore( ) } + override suspend fun clear(userWalletId: UserWalletId, stakingIds: Set) { + persistenceStore.updateData { current -> + current.toMutableMap().apply { + this[userWalletId.stringValue] = this[userWalletId.stringValue].orEmpty() + .filterNot { it.getStakingId() in stakingIds } + .toSet() + } + } + } + private suspend fun storeInRuntime(userWalletId: UserWalletId, values: Set) { val newBalances = YieldBalanceConverter(isCached = false).convertSet(input = values) .filterNotNull() diff --git a/data/staking/src/main/java/com/tangem/data/staking/store/YieldsBalancesStore.kt b/data/staking/src/main/java/com/tangem/data/staking/store/YieldsBalancesStore.kt index 3ec260a07f..10bfa99242 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/store/YieldsBalancesStore.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/store/YieldsBalancesStore.kt @@ -33,4 +33,7 @@ interface YieldsBalancesStore { /** Store error by [userWalletId] and [stakingIds] */ suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set) + + /** Clear balances of [stakingIds] by [userWalletId] */ + suspend fun clear(userWalletId: UserWalletId, stakingIds: Set) } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/utils/DefaultStakingCleaner.kt b/data/staking/src/main/java/com/tangem/data/staking/utils/DefaultStakingCleaner.kt new file mode 100644 index 0000000000..1aa6753150 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/utils/DefaultStakingCleaner.kt @@ -0,0 +1,29 @@ +package com.tangem.data.staking.utils + +import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.staking.utils.StakingCleaner +import com.tangem.utils.coroutines.CoroutineDispatcherProvider + +/** + * Default implementation of [StakingCleaner]. + * + * @property yieldsBalancesStore Store to manage yields balances. + * @property dispatchers Coroutine dispatchers provider. + * +[REDACTED_AUTHOR] + */ +internal class DefaultStakingCleaner( + private val yieldsBalancesStore: YieldsBalancesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : StakingCleaner { + + override suspend fun invoke(userWalletId: UserWalletId, stakingIds: Set) { + if (stakingIds.isEmpty()) return + + with(dispatchers.default) { + yieldsBalancesStore.clear(userWalletId, stakingIds) + } + } +} \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/utils/DefaultStakingCleanerTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/utils/DefaultStakingCleanerTest.kt new file mode 100644 index 0000000000..08dc91f9d6 --- /dev/null +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/utils/DefaultStakingCleanerTest.kt @@ -0,0 +1,55 @@ +package com.tangem.data.staking.utils + +import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.staking.model.StakingIntegrationID +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.clearMocks +import io.mockk.coVerifyOrder +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) +class DefaultStakingCleanerTest { + + private val yieldsBalancesStore = mockk(relaxed = true) + private val cleaner = DefaultStakingCleaner( + yieldsBalancesStore = yieldsBalancesStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + private val userWalletId = UserWalletId("011") + private val stakingIds = setOf( + StakingID(integrationId = StakingIntegrationID.Coin.Cardano.value, address = "0x1"), + ) + + @BeforeEach + fun setUp() { + clearMocks(yieldsBalancesStore) + } + + @Test + fun `should clear yields balances when called`() = runTest { + // Act + cleaner(userWalletId = userWalletId, stakingIds = stakingIds) + + // Assert + coVerifyOrder { + yieldsBalancesStore.clear(userWalletId = userWalletId, stakingIds = stakingIds) + } + } + + @Test + fun `should handle empty stakingIds`() = runTest { + // Act + cleaner(userWalletId = userWalletId, stakingIds = emptySet()) + + // Assert + coVerifyOrder(inverse = true) { + yieldsBalancesStore.clear(userWalletId = any(), stakingIds = any()) + } + } +} \ No newline at end of file diff --git a/domain/networks/src/main/java/com/tangem/domain/networks/utils/NetworksCleaner.kt b/domain/networks/src/main/java/com/tangem/domain/networks/utils/NetworksCleaner.kt new file mode 100644 index 0000000000..5db343c0c4 --- /dev/null +++ b/domain/networks/src/main/java/com/tangem/domain/networks/utils/NetworksCleaner.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.networks.utils + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Cleans up network-related data for a specific user wallet and a list of cryptocurrencies. + * +[REDACTED_AUTHOR] + */ +interface NetworksCleaner { + + /** + * Cleans up network-related data for the given [userWalletId] and list of [currencies]. + * + * @param userWalletId The ID of the user wallet for which to clean up data. + * @param currencies The list of cryptocurrencies whose associated network data should be cleaned. + */ + suspend operator fun invoke(userWalletId: UserWalletId, currencies: List) +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/utils/StakingCleaner.kt b/domain/staking/src/main/java/com/tangem/domain/staking/utils/StakingCleaner.kt new file mode 100644 index 0000000000..84925b509b --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/utils/StakingCleaner.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.staking.utils + +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Cleans up staking-related data for a specific user wallet and a set of staking IDs. + * +[REDACTED_AUTHOR] + */ +interface StakingCleaner { + + /** + * Cleans up staking-related data for the given [userWalletId] and set of [stakingIds]. + * + * @param userWalletId The ID of the user wallet for which to clean up data. + * @param stakingIds The set of staking IDs whose associated data should be cleaned. + */ + suspend operator fun invoke(userWalletId: UserWalletId, stakingIds: Set) +} \ No newline at end of file