From ae759af1c7632b1cbb14ca8c83f0c73262793358 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Dec 2025 10:15:05 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../tangem/tap/di/domain/NFTDomainModule.kt | 16 --- .../networks/utils/DefaultNetworksCleaner.kt | 42 ++++-- .../utils/DefaultNetworksCleanerTest.kt | 31 ++++- .../data/staking/di/StakingDataModule.kt | 3 + .../staking/utils/DefaultStakingCleaner.kt | 46 ++++++- .../utils/DefaultStakingCleanerTest.kt | 127 +++++++++++++++--- .../status/di/AccountStatusUseCaseModule.kt | 26 +++- .../usecase/ManageCryptoCurrenciesUseCase.kt | 27 +--- .../utils/CryptoCurrencyMetadataCleaner.kt | 69 ++++++++++ .../status/utils/CryptoCurrencyCleanerTest.kt | 74 ++++++++++ .../domain/networks/utils/NetworksCleaner.kt | 10 ++ .../ObserveAndClearNFTCacheIfNeedUseCase.kt | 51 ------- .../domain/staking/utils/StakingCleaner.kt | 29 ++++ .../wallet/child/wallet/model/WalletModel.kt | 11 -- 14 files changed, 422 insertions(+), 140 deletions(-) create mode 100644 domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyMetadataCleaner.kt create mode 100644 domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/CryptoCurrencyCleanerTest.kt delete mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/ObserveAndClearNFTCacheIfNeedUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index 2fbc11170b..cf1a7707c6 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -142,22 +142,6 @@ internal object NFTDomainModule { return GetWalletNFTEnabledUseCase(walletsRepository) } - @Provides - @Singleton - fun provideClearNFTCacheUseCase( - nftCleaner: NFTCleaner, - currenciesRepository: CurrenciesRepository, - accountsFeatureToggles: AccountsFeatureToggles, - singleAccountListSupplier: SingleAccountListSupplier, - ): ObserveAndClearNFTCacheIfNeedUseCase { - return ObserveAndClearNFTCacheIfNeedUseCase( - nftCleaner = nftCleaner, - currenciesRepository = currenciesRepository, - accountsFeatureToggles = accountsFeatureToggles, - singleAccountListSupplier = singleAccountListSupplier, - ) - } - @Provides @Singleton fun provideGetNftCurrencyUseCase(nftRepository: NFTRepository): GetNFTCurrencyUseCase { 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 index c4ef782bc9..b395990e65 100644 --- 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 @@ -7,9 +7,11 @@ 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 com.tangem.utils.coroutines.runSuspendCatching +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext +import timber.log.Timber /** * Default implementation of [NetworksCleaner]. @@ -27,33 +29,51 @@ internal class DefaultNetworksCleaner( ) : NetworksCleaner { override suspend fun invoke(userWalletId: UserWalletId, currencies: List) { + if (currencies.isEmpty()) { + Timber.d("No currencies to clear for wallet: $userWalletId") + return + } + withContext(dispatchers.default) { val (networks, tokens) = currencies.partitionByType() - coroutineScope { - launch { cleanStore(userWalletId = userWalletId, networks = networks) } - launch { cleanWalletManager(userWalletId = userWalletId, networks = networks, tokens = tokens) } - } + awaitAll( + async { clearStatusesStore(userWalletId = userWalletId, networks = networks) }, + async { clearBlockchainSDK(userWalletId = userWalletId, networks = networks, tokens = tokens) }, + ) } } - private suspend fun cleanStore(userWalletId: UserWalletId, networks: Set) { + private suspend fun clearStatusesStore(userWalletId: UserWalletId, networks: Set) { if (networks.isNotEmpty()) { - networksStatusesStore.clear(userWalletId = userWalletId, networks = networks) + runSuspendCatching { + networksStatusesStore.clear(userWalletId = userWalletId, networks = networks) + } + .onFailure { Timber.e(it, "Failed to clear network statuses for wallet: $userWalletId") } } } - private suspend fun cleanWalletManager( + private suspend fun clearBlockchainSDK( userWalletId: UserWalletId, networks: Set, tokens: Set, ) { if (networks.isNotEmpty()) { - walletManagersFacade.remove(userWalletId = userWalletId, networks = networks) + runSuspendCatching { + walletManagersFacade.remove(userWalletId = userWalletId, networks = networks) + } + .onFailure { + Timber.e(it, "Failed to remove networks from Blockchain SDK for wallet: $userWalletId") + } } if (tokens.isNotEmpty()) { - walletManagersFacade.removeTokens(userWalletId = userWalletId, tokens = tokens) + runSuspendCatching { + walletManagersFacade.removeTokens(userWalletId = userWalletId, tokens = tokens) + } + .onFailure { + Timber.e(it, "Failed to remove tokens from Blockchain SDK for wallet: $userWalletId") + } } } 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 index c2913c5f5a..5e7d1d6083 100644 --- 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 @@ -7,6 +7,7 @@ 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.coEvery import io.mockk.coVerifyOrder import io.mockk.mockk import kotlinx.coroutines.test.runTest @@ -45,7 +46,7 @@ class DefaultNetworksCleanerTest { // Assert coVerifyOrder { - networksStatusesStore.clear(userWalletId, setOf(network)) + networksStatusesStore.clear(userWalletId = userWalletId, networks = setOf(network)) walletManagersFacade.remove(userWalletId = userWalletId, networks = setOf(network)) walletManagersFacade.removeTokens(userWalletId = userWalletId, tokens = setOf(token)) } @@ -66,12 +67,15 @@ class DefaultNetworksCleanerTest { @Test fun `should clear only networks when there are no tokens`() = runTest { + // Arrange val currencies = listOf(coin) + // Act cleaner(userWalletId = userWalletId, currencies = currencies) + // Assert coVerifyOrder { - networksStatusesStore.clear(userWalletId, setOf(network)) + networksStatusesStore.clear(userWalletId = userWalletId, networks = setOf(network)) walletManagersFacade.remove(userWalletId = userWalletId, networks = setOf(network)) } @@ -98,4 +102,27 @@ class DefaultNetworksCleanerTest { walletManagersFacade.remove(userWalletId = any(), networks = any()) } } + + @Test + fun `should handle exception during cleaning`() = runTest { + // Arrange + val currencies = listOf(coin, token) + + val exception = Exception("Test exception") + coEvery { networksStatusesStore.clear(userWalletId, setOf(network)) } throws exception + coEvery { walletManagersFacade.remove(userWalletId = userWalletId, networks = setOf(network)) } throws exception + coEvery { + walletManagersFacade.removeTokens(userWalletId = userWalletId, tokens = setOf(token)) + } throws exception + + // Act + cleaner(userWalletId = userWalletId, currencies = currencies) + + // Assert + coVerifyOrder { + networksStatusesStore.clear(userWalletId = userWalletId, networks = setOf(network)) + walletManagersFacade.remove(userWalletId = userWalletId, networks = setOf(network)) + walletManagersFacade.removeTokens(userWalletId = userWalletId, tokens = setOf(token)) + } + } } \ 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 2f4f5b754a..220bdc1198 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 @@ -16,6 +16,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.P2PEthPoolVaultsStore import com.tangem.datasource.local.token.StakingActionsStore import com.tangem.datasource.local.token.StakingYieldsStore +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.repositories.* import com.tangem.domain.staking.toggles.StakingFeatureToggles import com.tangem.domain.staking.utils.StakingCleaner @@ -136,10 +137,12 @@ internal object StakingDataModule { @Provides @Singleton fun provideStakingCleaner( + stakingIdFactory: StakingIdFactory, stakingBalancesStore: StakingBalancesStore, dispatchers: CoroutineDispatcherProvider, ): StakingCleaner { return DefaultStakingCleaner( + stakingIdFactory = stakingIdFactory, stakingBalancesStore = stakingBalancesStore, dispatchers = dispatchers, ) 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 index c55bf2154e..396099e6ff 100644 --- 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 @@ -1,10 +1,17 @@ package com.tangem.data.staking.utils +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.data.staking.store.StakingBalancesStore import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.utils.StakingCleaner import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runSuspendCatching +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext +import timber.log.Timber /** * Default implementation of [StakingCleaner]. @@ -15,15 +22,46 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider [REDACTED_AUTHOR] */ internal class DefaultStakingCleaner( + private val stakingIdFactory: StakingIdFactory, private val stakingBalancesStore: StakingBalancesStore, private val dispatchers: CoroutineDispatcherProvider, ) : StakingCleaner { - override suspend fun invoke(userWalletId: UserWalletId, stakingIds: Set) { - if (stakingIds.isEmpty()) return + override suspend fun invoke(userWalletId: UserWalletId, currencies: List) { + if (currencies.isEmpty()) { + Timber.d("No currencies to clear for wallet: $userWalletId") + return + } - with(dispatchers.default) { - stakingBalancesStore.clear(userWalletId, stakingIds) + val stakingIds = currencies.mapNotNullTo(hashSetOf()) { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() + } + + if (stakingIds.isEmpty()) { + Timber.d("All currencies have no stakingIds to clear for wallet: $userWalletId") + return + } + + invoke(userWalletId = userWalletId, stakingIds = stakingIds) + } + + override suspend fun invoke(userWalletId: UserWalletId, stakingIds: Set) { + if (stakingIds.isEmpty()) { + Timber.d("No stakingIds to clear for wallet: $userWalletId") + return + } + + withContext(dispatchers.default) { + awaitAll( + async { clearStatusesStore(userWalletId = userWalletId, stakingIds = stakingIds) }, + ) } } + + private suspend fun clearStatusesStore(userWalletId: UserWalletId, stakingIds: Set) { + runSuspendCatching { + stakingBalancesStore.clear(userWalletId, stakingIds) + } + .onFailure { Timber.e(it, "Failed to clear yield balance statuses for wallet: $userWalletId") } + } } \ 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 index 630865f7ba..823197d89f 100644 --- 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 @@ -1,55 +1,146 @@ package com.tangem.data.staking.utils +import arrow.core.right +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.data.staking.store.StakingBalancesStore import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.clearMocks +import io.mockk.coEvery import io.mockk.coVerifyOrder import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DefaultStakingCleanerTest { + private val stakingIdFactory = mockk(relaxed = true) private val stakingBalancesStore = mockk(relaxed = true) private val cleaner = DefaultStakingCleaner( + stakingIdFactory = stakingIdFactory, stakingBalancesStore = stakingBalancesStore, dispatchers = TestingCoroutineDispatcherProvider(), ) + private val userWalletId = UserWalletId("011") - private val stakingIds = setOf( - StakingID(integrationId = StakingIntegrationID.StakeKit.Coin.Cardano.value, address = "0x1"), - ) @BeforeEach fun setUp() { - clearMocks(stakingBalancesStore) + clearMocks(stakingIdFactory, stakingBalancesStore) } - @Test - fun `should clear yields balances when called`() = runTest { - // Act - cleaner(userWalletId = userWalletId, stakingIds = stakingIds) + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ClearByStakingIds { - // Assert - coVerifyOrder { - stakingBalancesStore.clear(userWalletId = userWalletId, stakingIds = stakingIds) + private val stakingIds = setOf( + StakingID(integrationId = StakingIntegrationID.StakeKit.Coin.Cardano.value, address = "0x1"), + ) + + @Test + fun `should clear yields balances when called`() = runTest { + // Act + cleaner(userWalletId = userWalletId, stakingIds = stakingIds) + + // Assert + coVerifyOrder { + stakingBalancesStore.clear(userWalletId = userWalletId, stakingIds = stakingIds) + } + } + + @Test + fun `should handle empty stakingIds`() = runTest { + // Act + cleaner(userWalletId = userWalletId, stakingIds = emptySet()) + + // Assert + coVerifyOrder(inverse = true) { + stakingBalancesStore.clear(userWalletId = any(), stakingIds = any()) + } + } + + @Test + fun `should catch exception from stakingBalancesStore and not throw`() = runTest { + // Arrange + coEvery { stakingBalancesStore.clear(userWalletId, stakingIds) } throws Exception() + + // Act + cleaner(userWalletId = userWalletId, stakingIds = stakingIds) + + // Assert + coVerifyOrder { + stakingBalancesStore.clear(userWalletId = userWalletId, stakingIds = stakingIds) + } } } - @Test - fun `should handle empty stakingIds`() = runTest { - // Act - cleaner(userWalletId = userWalletId, stakingIds = emptySet()) + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ClearByCurrencies { - // Assert - coVerifyOrder(inverse = true) { - stakingBalancesStore.clear(userWalletId = any(), stakingIds = any()) + private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() + private val coin = cryptoCurrencyFactory.ethereum + + @Test + fun `should clear yields balances when called with single currency`() = runTest { + // Arrange + val stakingId = StakingID( + integrationId = "stake_kit_coin_eth", + address = "0xabc", + ) + + coEvery { stakingIdFactory.create(userWalletId, coin) } returns stakingId.right() + + // Act + cleaner(userWalletId = userWalletId, currency = coin) + + // Assert + coVerifyOrder { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = coin) + stakingBalancesStore.clear(userWalletId = userWalletId, stakingIds = setOf(stakingId)) + } + } + + @Test + fun `should handle empty list of currencies`() = runTest { + // Act + cleaner(userWalletId = userWalletId, currencies = emptyList()) + + // Assert + coVerifyOrder(inverse = true) { + stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) + stakingBalancesStore.clear(userWalletId = any(), stakingIds = any()) + } + } + + @Test + fun `should catch exception from stakingBalancesStore and not throw`() = runTest { + // Arrange + val stakingId = StakingID( + integrationId = "stake_kit_coin_eth", + address = "0xabc", + ) + + coEvery { stakingIdFactory.create(userWalletId, coin) } returns stakingId.right() + coEvery { + stakingBalancesStore.clear(userWalletId = userWalletId, stakingIds = setOf(stakingId)) + } throws Exception() + + // Act + cleaner(userWalletId = userWalletId, currency = coin) + + // Assert + coVerifyOrder { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = coin) + stakingBalancesStore.clear(userWalletId = userWalletId, stakingIds = setOf(stakingId)) + } } } } \ No newline at end of file diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt index c2d429c650..fced65957e 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt @@ -5,11 +5,13 @@ import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.usecase.* import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher +import com.tangem.domain.account.status.utils.CryptoCurrencyMetadataCleaner import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.utils.NetworksCleaner +import com.tangem.domain.nft.utils.NFTCleaner import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher @@ -98,9 +100,7 @@ internal object AccountStatusUseCaseModule { derivationsRepository: DerivationsRepository, walletManagersFacade: WalletManagersFacade, cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher, - stakingIdFactory: StakingIdFactory, - networksCleaner: NetworksCleaner, - stakingCleaner: StakingCleaner, + cryptoCurrencyMetadataCleaner: CryptoCurrencyMetadataCleaner, expressServiceFetcher: ExpressServiceFetcher, dispatchers: CoroutineDispatcherProvider, ): ManageCryptoCurrenciesUseCase { @@ -111,9 +111,7 @@ internal object AccountStatusUseCaseModule { derivationsRepository = derivationsRepository, walletManagersFacade = walletManagersFacade, cryptoCurrencyBalanceFetcher = cryptoCurrencyBalanceFetcher, - stakingIdFactory = stakingIdFactory, - networksCleaner = networksCleaner, - stakingCleaner = stakingCleaner, + cryptoCurrencyMetadataCleaner = cryptoCurrencyMetadataCleaner, expressServiceFetcher = expressServiceFetcher, parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default), dispatchers = dispatchers, @@ -157,4 +155,20 @@ internal object AccountStatusUseCaseModule { dispatchers = dispatchers, ) } + + @Provides + @Singleton + fun provideCryptoCurrencyMetadataCleaner( + networksCleaner: NetworksCleaner, + stakingCleaner: StakingCleaner, + nftCleaner: NFTCleaner, + dispatchers: CoroutineDispatcherProvider, + ): CryptoCurrencyMetadataCleaner { + return CryptoCurrencyMetadataCleaner( + networksCleaner = networksCleaner, + stakingCleaner = stakingCleaner, + nftCleaner = nftCleaner, + dispatchers = dispatchers, + ) + } } \ No newline at end of file diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt index 51ffcfa1e8..dd64e3d0cc 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt @@ -7,6 +7,7 @@ import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher +import com.tangem.domain.account.status.utils.CryptoCurrencyMetadataCleaner import com.tangem.domain.core.utils.eitherOn import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.express.models.ExpressAsset @@ -17,9 +18,6 @@ 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.UserWalletId -import com.tangem.domain.networks.utils.NetworksCleaner -import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.utils.StakingCleaner import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.derivations.DerivationsRepository @@ -36,9 +34,7 @@ import timber.log.Timber * @property currenciesRepository Repository for managing currencies. * @property derivationsRepository Repository for deriving public keys. * @property cryptoCurrencyBalanceFetcher Fetcher for updating crypto currency balances. - * @property stakingIdFactory Factory for creating staking IDs. - * @property networksCleaner Cleaner for removing obsolete network data. - * @property stakingCleaner Cleaner for removing obsolete staking data. + * @property cryptoCurrencyMetadataCleaner Cleaner for removing metadata of deleted currencies. * @property expressServiceFetcher Fetcher for updating express service data. * @property parallelUpdatingScope Coroutine scope for parallel updates. * @property dispatchers Coroutine dispatchers for managing threading. @@ -53,9 +49,7 @@ class ManageCryptoCurrenciesUseCase( private val derivationsRepository: DerivationsRepository, private val walletManagersFacade: WalletManagersFacade, private val cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher, - private val stakingIdFactory: StakingIdFactory, - private val networksCleaner: NetworksCleaner, - private val stakingCleaner: StakingCleaner, + private val cryptoCurrencyMetadataCleaner: CryptoCurrencyMetadataCleaner, private val expressServiceFetcher: ExpressServiceFetcher, private val parallelUpdatingScope: CoroutineScope, private val dispatchers: CoroutineDispatcherProvider, @@ -306,21 +300,12 @@ class ManageCryptoCurrenciesUseCase( if (currencies.isEmpty()) return coroutineScope { - listOf( - launch { networksCleaner(userWalletId = userWalletId, currencies = currencies) }, - launch { clearStaking(userWalletId = userWalletId, currencies = currencies) }, - ) + launch { + cryptoCurrencyMetadataCleaner(userWalletId = userWalletId, currencies = currencies) + } } } - private suspend fun clearStaking(userWalletId: UserWalletId, currencies: List) { - val stakingIds = currencies.mapNotNullTo(hashSetOf()) { - stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() - } - - stakingCleaner(userWalletId = userWalletId, stakingIds = stakingIds) - } - private data class TempID( val networkId: String, val derivationPath: Network.DerivationPath, diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyMetadataCleaner.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyMetadataCleaner.kt new file mode 100644 index 0000000000..15fdc5ff07 --- /dev/null +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyMetadataCleaner.kt @@ -0,0 +1,69 @@ +package com.tangem.domain.account.status.utils + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.networks.utils.NetworksCleaner +import com.tangem.domain.nft.utils.NFTCleaner +import com.tangem.domain.staking.utils.StakingCleaner +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext + +/** + * Class to clean up all data related to a specific cryptocurrency in a user's wallet. + * + * @property networksCleaner Utility to clean network-related data. + * @property stakingCleaner Utility to clean staking-related data. + * @property nftCleaner Utility to clean NFT-related data. + * @property dispatchers Coroutine dispatchers for managing threading. + * + * @see Notion + * +[REDACTED_AUTHOR] + */ +class CryptoCurrencyMetadataCleaner( + private val networksCleaner: NetworksCleaner, + private val stakingCleaner: StakingCleaner, + private val nftCleaner: NFTCleaner, + private val dispatchers: CoroutineDispatcherProvider, +) { + + /** + * Cleans up data for a single cryptocurrency in the specified user wallet. + * + * @param userWalletId The ID of the user's wallet. + * @param currency The cryptocurrency to be cleaned. + */ + suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency) { + invoke(userWalletId = userWalletId, currencies = listOf(currency)) + } + + /** + * Cleans up data for multiple cryptocurrencies in the specified user wallet. + * + * @param userWalletId The ID of the user's wallet. + * @param currencies The list of cryptocurrencies to be cleaned. + */ + suspend operator fun invoke(userWalletId: UserWalletId, currencies: List) { + if (currencies.isEmpty()) return + + return withContext(dispatchers.default) { + awaitAll( + async { networksCleaner(userWalletId = userWalletId, currencies = currencies) }, + async { clearStaking(userWalletId = userWalletId, currencies = currencies) }, + async { clearNFTs(userWalletId = userWalletId, currencies = currencies) }, + ) + } + } + + private suspend fun clearStaking(userWalletId: UserWalletId, currencies: List) { + stakingCleaner(userWalletId = userWalletId, currencies = currencies) + } + + private suspend fun clearNFTs(userWalletId: UserWalletId, currencies: List) { + val networks = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::network) + + nftCleaner(userWalletId = userWalletId, networks = networks) + } +} \ No newline at end of file diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/CryptoCurrencyCleanerTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/CryptoCurrencyCleanerTest.kt new file mode 100644 index 0000000000..e1ce153ad3 --- /dev/null +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/CryptoCurrencyCleanerTest.kt @@ -0,0 +1,74 @@ +package com.tangem.domain.account.status.utils + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.networks.utils.NetworksCleaner +import com.tangem.domain.nft.utils.NFTCleaner +import com.tangem.domain.staking.utils.StakingCleaner +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.clearMocks +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class CryptoCurrencyCleanerTest { + + private val networksCleaner: NetworksCleaner = mockk(relaxUnitFun = true) + private val stakingCleaner: StakingCleaner = mockk(relaxUnitFun = true) + private val nftCleaner: NFTCleaner = mockk(relaxUnitFun = true) + + private val cleaner = CryptoCurrencyMetadataCleaner( + networksCleaner = networksCleaner, + stakingCleaner = stakingCleaner, + nftCleaner = nftCleaner, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + private val userWalletId = UserWalletId("011") + + private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() + private val coin = cryptoCurrencyFactory.ethereum + private val token = cryptoCurrencyFactory.createToken(Blockchain.Ethereum) + + @AfterEach + fun tearDown() { + clearMocks(networksCleaner, stakingCleaner, nftCleaner) + } + + @Test + fun `should not call cleaners when currencies list is empty`() = runTest { + // Act + cleaner(userWalletId = userWalletId, currencies = emptyList()) + + // Assert + coVerify(inverse = true) { + networksCleaner(userWalletId = any(), currencies = any()) + stakingCleaner(userWalletId = any(), currencies = any()) + nftCleaner(userWalletId = any(), networks = any()) + } + } + + @Test + fun `should call both cleaners when currencies list is not empty`() = runTest { + // Arrange + val currencies = listOf(coin, token) + + // Act + cleaner(userWalletId = userWalletId, currencies = currencies) + + // Assert + coVerify { + networksCleaner(userWalletId = userWalletId, currencies = currencies) + stakingCleaner(userWalletId = userWalletId, currencies = currencies) + nftCleaner(userWalletId = userWalletId, networks = setOf(coin.network, token.network)) + } + } +} \ 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 index 5db343c0c4..4a5ac9acd8 100644 --- 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 @@ -10,6 +10,16 @@ import com.tangem.domain.models.wallet.UserWalletId */ interface NetworksCleaner { + /** + * Cleans up network-related data for the given [userWalletId] and [currency]. + * + * @param userWalletId The ID of the user wallet for which to clean up data. + * @param currency The cryptocurrency whose associated network data should be cleaned. + */ + suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency) { + invoke(userWalletId = userWalletId, currencies = listOf(currency)) + } + /** * Cleans up network-related data for the given [userWalletId] and list of [currencies]. * diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/ObserveAndClearNFTCacheIfNeedUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/ObserveAndClearNFTCacheIfNeedUseCase.kt deleted file mode 100644 index 86e2e2ac34..0000000000 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/ObserveAndClearNFTCacheIfNeedUseCase.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.domain.nft - -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles -import com.tangem.domain.account.models.AccountList -import com.tangem.domain.account.supplier.SingleAccountListSupplier -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.nft.utils.NFTCleaner -import com.tangem.domain.tokens.repository.CurrenciesRepository -import kotlinx.coroutines.flow.* - -class ObserveAndClearNFTCacheIfNeedUseCase( - private val nftCleaner: NFTCleaner, - private val currenciesRepository: CurrenciesRepository, - private val accountsFeatureToggles: AccountsFeatureToggles, - private val singleAccountListSupplier: SingleAccountListSupplier, -) { - - operator fun invoke(userWalletId: UserWalletId): Flow> = getCryptoCurrencies(userWalletId) - .map { it.map(CryptoCurrency::network) } - .mapDiff { old, new -> - // calculate networks sets difference to determine which networks were removed - old.toSet() - new.toSet() - } - .distinctUntilChanged() - .onEach { removedNetworks -> - if (removedNetworks.isNotEmpty()) { - nftCleaner(userWalletId, removedNetworks) - } - } - - private fun getCryptoCurrencies(userWalletId: UserWalletId): Flow> { - return if (accountsFeatureToggles.isFeatureEnabled) { - singleAccountListSupplier(userWalletId).map(AccountList::flattenCurrencies) - } else { - currenciesRepository.getWalletCurrenciesUpdates(userWalletId) - } - } - - private fun Flow.mapDiff(diff: (old: T, new: T) -> R): Flow = flow { - var previous: T? = null - collect { current -> - val prev = previous - if (prev != null) { - emit(diff(prev, current)) - } - previous = current - } - } -} \ 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 index 84925b509b..9e1ce60e74 100644 --- 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 @@ -1,5 +1,6 @@ package com.tangem.domain.staking.utils +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWalletId @@ -10,6 +11,34 @@ import com.tangem.domain.models.wallet.UserWalletId */ interface StakingCleaner { + /** + * Cleans up staking-related data for the given [userWalletId] and single [currency]. + * + * @param userWalletId The ID of the user wallet for which to clean up data. + * @param currency The cryptocurrency whose associated staking data should be cleaned. + */ + suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency) { + invoke(userWalletId = userWalletId, currencies = listOf(currency)) + } + + /** + * Cleans up staking-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 staking data should be cleaned. + */ + suspend operator fun invoke(userWalletId: UserWalletId, currencies: List) + + /** + * Cleans up staking-related data for the given [userWalletId] and single [stakingId]. + * + * @param userWalletId The ID of the user wallet for which to clean up data. + * @param stakingId The staking ID whose associated data should be cleaned. + */ + suspend operator fun invoke(userWalletId: UserWalletId, stakingId: StakingID) { + invoke(userWalletId = userWalletId, stakingIds = setOf(stakingId)) + } + /** * Cleans up staking-related data for the given [userWalletId] and set of [stakingIds]. * diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index ba456f80f5..6759eefd27 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -18,7 +18,6 @@ import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.* -import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.pay.repository.OnboardingRepository @@ -84,7 +83,6 @@ internal class WalletModel @Inject constructor( private val onrampStatusFactory: OnrampStatusFactory, private val analyticsEventsHandler: AnalyticsEventHandler, private val walletContentFetcher: WalletContentFetcher, - private val observeAndClearNFTCacheIfNeedUseCase: ObserveAndClearNFTCacheIfNeedUseCase, private val walletDeepLinkActionListener: WalletDeepLinkActionListener, private val notificationsRepository: NotificationsRepository, private val getWalletsListForEnablingUseCase: GetWalletsForAutomaticallyPushEnablingUseCase, @@ -114,7 +112,6 @@ internal class WalletModel @Inject constructor( private val walletsUpdateJobHolder = JobHolder() private val refreshWalletJobHolder = JobHolder() - private val clearNFTCacheJobHolder = JobHolder() private val updateTangemPayJobHolder = JobHolder() private var needToRefreshWallet = false @@ -334,7 +331,6 @@ internal class WalletModel @Inject constructor( } subscribeOnExpressTransactionsUpdates(selectedWallet) - observeAndClearNFTCacheIfNeedUseCase(selectedWallet) } .flowOn(dispatchers.main) .launchIn(modelScope) @@ -390,13 +386,6 @@ internal class WalletModel @Inject constructor( } } - private fun observeAndClearNFTCacheIfNeedUseCase(selectedWallet: UserWallet) { - observeAndClearNFTCacheIfNeedUseCase - .invoke(selectedWallet.walletId) - .launchIn(modelScope) - .saveIn(clearNFTCacheJobHolder) - } - private fun subscribeTangemPayOnWalletState() { /** * Update state each time a user opens/returns to wallet screen