Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-04 10:15:05 +04:00
parent ebb562b2a0
commit ae759af1c7
14 changed files with 422 additions and 140 deletions

View file

@ -142,22 +142,6 @@ internal object NFTDomainModule {
return GetWalletNFTEnabledUseCase(walletsRepository) 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 @Provides
@Singleton @Singleton
fun provideGetNftCurrencyUseCase(nftRepository: NFTRepository): GetNFTCurrencyUseCase { fun provideGetNftCurrencyUseCase(nftRepository: NFTRepository): GetNFTCurrencyUseCase {

View file

@ -7,9 +7,11 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.networks.utils.NetworksCleaner import com.tangem.domain.networks.utils.NetworksCleaner
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.coroutineScope import com.tangem.utils.coroutines.runSuspendCatching
import kotlinx.coroutines.launch import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber
/** /**
* Default implementation of [NetworksCleaner]. * Default implementation of [NetworksCleaner].
@ -27,33 +29,51 @@ internal class DefaultNetworksCleaner(
) : NetworksCleaner { ) : NetworksCleaner {
override suspend fun invoke(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) { override suspend fun invoke(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
if (currencies.isEmpty()) {
Timber.d("No currencies to clear for wallet: $userWalletId")
return
}
withContext(dispatchers.default) { withContext(dispatchers.default) {
val (networks, tokens) = currencies.partitionByType() val (networks, tokens) = currencies.partitionByType()
coroutineScope { awaitAll(
launch { cleanStore(userWalletId = userWalletId, networks = networks) } async { clearStatusesStore(userWalletId = userWalletId, networks = networks) },
launch { cleanWalletManager(userWalletId = userWalletId, networks = networks, tokens = tokens) } async { clearBlockchainSDK(userWalletId = userWalletId, networks = networks, tokens = tokens) },
} )
} }
} }
private suspend fun cleanStore(userWalletId: UserWalletId, networks: Set<Network>) { private suspend fun clearStatusesStore(userWalletId: UserWalletId, networks: Set<Network>) {
if (networks.isNotEmpty()) { 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, userWalletId: UserWalletId,
networks: Set<Network>, networks: Set<Network>,
tokens: Set<CryptoCurrency.Token>, tokens: Set<CryptoCurrency.Token>,
) { ) {
if (networks.isNotEmpty()) { 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()) { 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")
}
} }
} }

View file

@ -7,6 +7,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.clearMocks import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerifyOrder import io.mockk.coVerifyOrder
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -45,7 +46,7 @@ class DefaultNetworksCleanerTest {
// Assert // Assert
coVerifyOrder { coVerifyOrder {
networksStatusesStore.clear(userWalletId, setOf(network)) networksStatusesStore.clear(userWalletId = userWalletId, networks = setOf(network))
walletManagersFacade.remove(userWalletId = userWalletId, networks = setOf(network)) walletManagersFacade.remove(userWalletId = userWalletId, networks = setOf(network))
walletManagersFacade.removeTokens(userWalletId = userWalletId, tokens = setOf(token)) walletManagersFacade.removeTokens(userWalletId = userWalletId, tokens = setOf(token))
} }
@ -66,12 +67,15 @@ class DefaultNetworksCleanerTest {
@Test @Test
fun `should clear only networks when there are no tokens`() = runTest { fun `should clear only networks when there are no tokens`() = runTest {
// Arrange
val currencies = listOf(coin) val currencies = listOf(coin)
// Act
cleaner(userWalletId = userWalletId, currencies = currencies) cleaner(userWalletId = userWalletId, currencies = currencies)
// Assert
coVerifyOrder { coVerifyOrder {
networksStatusesStore.clear(userWalletId, setOf(network)) networksStatusesStore.clear(userWalletId = userWalletId, networks = setOf(network))
walletManagersFacade.remove(userWalletId = userWalletId, networks = setOf(network)) walletManagersFacade.remove(userWalletId = userWalletId, networks = setOf(network))
} }
@ -98,4 +102,27 @@ class DefaultNetworksCleanerTest {
walletManagersFacade.remove(userWalletId = any(), networks = any()) 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))
}
}
} }

View file

@ -16,6 +16,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.P2PEthPoolVaultsStore import com.tangem.datasource.local.token.P2PEthPoolVaultsStore
import com.tangem.datasource.local.token.StakingActionsStore import com.tangem.datasource.local.token.StakingActionsStore
import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.repositories.* import com.tangem.domain.staking.repositories.*
import com.tangem.domain.staking.toggles.StakingFeatureToggles import com.tangem.domain.staking.toggles.StakingFeatureToggles
import com.tangem.domain.staking.utils.StakingCleaner import com.tangem.domain.staking.utils.StakingCleaner
@ -136,10 +137,12 @@ internal object StakingDataModule {
@Provides @Provides
@Singleton @Singleton
fun provideStakingCleaner( fun provideStakingCleaner(
stakingIdFactory: StakingIdFactory,
stakingBalancesStore: StakingBalancesStore, stakingBalancesStore: StakingBalancesStore,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
): StakingCleaner { ): StakingCleaner {
return DefaultStakingCleaner( return DefaultStakingCleaner(
stakingIdFactory = stakingIdFactory,
stakingBalancesStore = stakingBalancesStore, stakingBalancesStore = stakingBalancesStore,
dispatchers = dispatchers, dispatchers = dispatchers,
) )

View file

@ -1,10 +1,17 @@
package com.tangem.data.staking.utils package com.tangem.data.staking.utils
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.data.staking.store.StakingBalancesStore import com.tangem.data.staking.store.StakingBalancesStore
import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.staking.StakingID
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.utils.StakingCleaner import com.tangem.domain.staking.utils.StakingCleaner
import com.tangem.utils.coroutines.CoroutineDispatcherProvider 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]. * Default implementation of [StakingCleaner].
@ -15,15 +22,46 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
internal class DefaultStakingCleaner( internal class DefaultStakingCleaner(
private val stakingIdFactory: StakingIdFactory,
private val stakingBalancesStore: StakingBalancesStore, private val stakingBalancesStore: StakingBalancesStore,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
) : StakingCleaner { ) : StakingCleaner {
override suspend fun invoke(userWalletId: UserWalletId, stakingIds: Set<StakingID>) { override suspend fun invoke(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
if (stakingIds.isEmpty()) return if (currencies.isEmpty()) {
Timber.d("No currencies to clear for wallet: $userWalletId")
return
}
with(dispatchers.default) { val stakingIds = currencies.mapNotNullTo(hashSetOf()) {
stakingBalancesStore.clear(userWalletId, stakingIds) 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<StakingID>) {
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<StakingID>) {
runSuspendCatching {
stakingBalancesStore.clear(userWalletId, stakingIds)
}
.onFailure { Timber.e(it, "Failed to clear yield balance statuses for wallet: $userWalletId") }
}
} }

View file

@ -1,55 +1,146 @@
package com.tangem.data.staking.utils 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.data.staking.store.StakingBalancesStore
import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.staking.StakingID
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.clearMocks import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerifyOrder import io.mockk.coVerifyOrder
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DefaultStakingCleanerTest { class DefaultStakingCleanerTest {
private val stakingIdFactory = mockk<StakingIdFactory>(relaxed = true)
private val stakingBalancesStore = mockk<StakingBalancesStore>(relaxed = true) private val stakingBalancesStore = mockk<StakingBalancesStore>(relaxed = true)
private val cleaner = DefaultStakingCleaner( private val cleaner = DefaultStakingCleaner(
stakingIdFactory = stakingIdFactory,
stakingBalancesStore = stakingBalancesStore, stakingBalancesStore = stakingBalancesStore,
dispatchers = TestingCoroutineDispatcherProvider(), dispatchers = TestingCoroutineDispatcherProvider(),
) )
private val userWalletId = UserWalletId("011") private val userWalletId = UserWalletId("011")
private val stakingIds = setOf(
StakingID(integrationId = StakingIntegrationID.StakeKit.Coin.Cardano.value, address = "0x1"),
)
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {
clearMocks(stakingBalancesStore) clearMocks(stakingIdFactory, stakingBalancesStore)
} }
@Test @Nested
fun `should clear yields balances when called`() = runTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS)
// Act inner class ClearByStakingIds {
cleaner(userWalletId = userWalletId, stakingIds = stakingIds)
// Assert private val stakingIds = setOf(
coVerifyOrder { StakingID(integrationId = StakingIntegrationID.StakeKit.Coin.Cardano.value, address = "0x1"),
stakingBalancesStore.clear(userWalletId = userWalletId, stakingIds = stakingIds) )
@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 @Nested
fun `should handle empty stakingIds`() = runTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS)
// Act inner class ClearByCurrencies {
cleaner(userWalletId = userWalletId, stakingIds = emptySet())
// Assert private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
coVerifyOrder(inverse = true) { private val coin = cryptoCurrencyFactory.ethereum
stakingBalancesStore.clear(userWalletId = any(), stakingIds = any())
@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))
}
} }
} }
} }

View file

@ -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.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.* import com.tangem.domain.account.status.usecase.*
import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher 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.account.supplier.SingleAccountListSupplier
import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
import com.tangem.domain.networks.utils.NetworksCleaner 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.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
@ -98,9 +100,7 @@ internal object AccountStatusUseCaseModule {
derivationsRepository: DerivationsRepository, derivationsRepository: DerivationsRepository,
walletManagersFacade: WalletManagersFacade, walletManagersFacade: WalletManagersFacade,
cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher, cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher,
stakingIdFactory: StakingIdFactory, cryptoCurrencyMetadataCleaner: CryptoCurrencyMetadataCleaner,
networksCleaner: NetworksCleaner,
stakingCleaner: StakingCleaner,
expressServiceFetcher: ExpressServiceFetcher, expressServiceFetcher: ExpressServiceFetcher,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
): ManageCryptoCurrenciesUseCase { ): ManageCryptoCurrenciesUseCase {
@ -111,9 +111,7 @@ internal object AccountStatusUseCaseModule {
derivationsRepository = derivationsRepository, derivationsRepository = derivationsRepository,
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
cryptoCurrencyBalanceFetcher = cryptoCurrencyBalanceFetcher, cryptoCurrencyBalanceFetcher = cryptoCurrencyBalanceFetcher,
stakingIdFactory = stakingIdFactory, cryptoCurrencyMetadataCleaner = cryptoCurrencyMetadataCleaner,
networksCleaner = networksCleaner,
stakingCleaner = stakingCleaner,
expressServiceFetcher = expressServiceFetcher, expressServiceFetcher = expressServiceFetcher,
parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default), parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default),
dispatchers = dispatchers, dispatchers = dispatchers,
@ -157,4 +155,20 @@ internal object AccountStatusUseCaseModule {
dispatchers = dispatchers, 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,
)
}
} }

View file

@ -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.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher 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.core.utils.eitherOn
import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.express.models.ExpressAsset 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.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId 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.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.domain.wallets.derivations.DerivationsRepository
@ -36,9 +34,7 @@ import timber.log.Timber
* @property currenciesRepository Repository for managing currencies. * @property currenciesRepository Repository for managing currencies.
* @property derivationsRepository Repository for deriving public keys. * @property derivationsRepository Repository for deriving public keys.
* @property cryptoCurrencyBalanceFetcher Fetcher for updating crypto currency balances. * @property cryptoCurrencyBalanceFetcher Fetcher for updating crypto currency balances.
* @property stakingIdFactory Factory for creating staking IDs. * @property cryptoCurrencyMetadataCleaner Cleaner for removing metadata of deleted currencies.
* @property networksCleaner Cleaner for removing obsolete network data.
* @property stakingCleaner Cleaner for removing obsolete staking data.
* @property expressServiceFetcher Fetcher for updating express service data. * @property expressServiceFetcher Fetcher for updating express service data.
* @property parallelUpdatingScope Coroutine scope for parallel updates. * @property parallelUpdatingScope Coroutine scope for parallel updates.
* @property dispatchers Coroutine dispatchers for managing threading. * @property dispatchers Coroutine dispatchers for managing threading.
@ -53,9 +49,7 @@ class ManageCryptoCurrenciesUseCase(
private val derivationsRepository: DerivationsRepository, private val derivationsRepository: DerivationsRepository,
private val walletManagersFacade: WalletManagersFacade, private val walletManagersFacade: WalletManagersFacade,
private val cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher, private val cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher,
private val stakingIdFactory: StakingIdFactory, private val cryptoCurrencyMetadataCleaner: CryptoCurrencyMetadataCleaner,
private val networksCleaner: NetworksCleaner,
private val stakingCleaner: StakingCleaner,
private val expressServiceFetcher: ExpressServiceFetcher, private val expressServiceFetcher: ExpressServiceFetcher,
private val parallelUpdatingScope: CoroutineScope, private val parallelUpdatingScope: CoroutineScope,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
@ -306,21 +300,12 @@ class ManageCryptoCurrenciesUseCase(
if (currencies.isEmpty()) return if (currencies.isEmpty()) return
coroutineScope { coroutineScope {
listOf( launch {
launch { networksCleaner(userWalletId = userWalletId, currencies = currencies) }, cryptoCurrencyMetadataCleaner(userWalletId = userWalletId, currencies = currencies)
launch { clearStaking(userWalletId = userWalletId, currencies = currencies) }, }
)
} }
} }
private suspend fun clearStaking(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
val stakingIds = currencies.mapNotNullTo(hashSetOf()) {
stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull()
}
stakingCleaner(userWalletId = userWalletId, stakingIds = stakingIds)
}
private data class TempID( private data class TempID(
val networkId: String, val networkId: String,
val derivationPath: Network.DerivationPath, val derivationPath: Network.DerivationPath,

View file

@ -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 <a href="https://www.notion.so/tangem/2be5d34eb67880008f95cb779dcafac9">Notion</a>
*
[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<CryptoCurrency>) {
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<CryptoCurrency>) {
stakingCleaner(userWalletId = userWalletId, currencies = currencies)
}
private suspend fun clearNFTs(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
val networks = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::network)
nftCleaner(userWalletId = userWalletId, networks = networks)
}
}

View file

@ -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))
}
}
}

View file

@ -10,6 +10,16 @@ import com.tangem.domain.models.wallet.UserWalletId
*/ */
interface NetworksCleaner { 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]. * Cleans up network-related data for the given [userWalletId] and list of [currencies].
* *

View file

@ -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<Set<Network>> = 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<Collection<CryptoCurrency>> {
return if (accountsFeatureToggles.isFeatureEnabled) {
singleAccountListSupplier(userWalletId).map(AccountList::flattenCurrencies)
} else {
currenciesRepository.getWalletCurrenciesUpdates(userWalletId)
}
}
private fun <T, R> Flow<T>.mapDiff(diff: (old: T, new: T) -> R): Flow<R> = flow {
var previous: T? = null
collect { current ->
val prev = previous
if (prev != null) {
emit(diff(prev, current))
}
previous = current
}
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.domain.staking.utils 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.staking.StakingID
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
@ -10,6 +11,34 @@ import com.tangem.domain.models.wallet.UserWalletId
*/ */
interface StakingCleaner { 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<CryptoCurrency>)
/**
* 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]. * Cleans up staking-related data for the given [userWalletId] and set of [stakingIds].
* *

View file

@ -18,7 +18,6 @@ import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.* import com.tangem.domain.models.wallet.*
import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase
import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.repository.OnboardingRepository
@ -84,7 +83,6 @@ internal class WalletModel @Inject constructor(
private val onrampStatusFactory: OnrampStatusFactory, private val onrampStatusFactory: OnrampStatusFactory,
private val analyticsEventsHandler: AnalyticsEventHandler, private val analyticsEventsHandler: AnalyticsEventHandler,
private val walletContentFetcher: WalletContentFetcher, private val walletContentFetcher: WalletContentFetcher,
private val observeAndClearNFTCacheIfNeedUseCase: ObserveAndClearNFTCacheIfNeedUseCase,
private val walletDeepLinkActionListener: WalletDeepLinkActionListener, private val walletDeepLinkActionListener: WalletDeepLinkActionListener,
private val notificationsRepository: NotificationsRepository, private val notificationsRepository: NotificationsRepository,
private val getWalletsListForEnablingUseCase: GetWalletsForAutomaticallyPushEnablingUseCase, private val getWalletsListForEnablingUseCase: GetWalletsForAutomaticallyPushEnablingUseCase,
@ -114,7 +112,6 @@ internal class WalletModel @Inject constructor(
private val walletsUpdateJobHolder = JobHolder() private val walletsUpdateJobHolder = JobHolder()
private val refreshWalletJobHolder = JobHolder() private val refreshWalletJobHolder = JobHolder()
private val clearNFTCacheJobHolder = JobHolder()
private val updateTangemPayJobHolder = JobHolder() private val updateTangemPayJobHolder = JobHolder()
private var needToRefreshWallet = false private var needToRefreshWallet = false
@ -334,7 +331,6 @@ internal class WalletModel @Inject constructor(
} }
subscribeOnExpressTransactionsUpdates(selectedWallet) subscribeOnExpressTransactionsUpdates(selectedWallet)
observeAndClearNFTCacheIfNeedUseCase(selectedWallet)
} }
.flowOn(dispatchers.main) .flowOn(dispatchers.main)
.launchIn(modelScope) .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() { private fun subscribeTangemPayOnWalletState() {
/** /**
* Update state each time a user opens/returns to wallet screen * Update state each time a user opens/returns to wallet screen