Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-09 13:47:13 +04:00
parent 55f731f1c3
commit b89d98aa35
12 changed files with 358 additions and 0 deletions

View file

@ -8,6 +8,7 @@ import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.networks.repository.DefaultNetworksRepository import com.tangem.data.networks.repository.DefaultNetworksRepository
import com.tangem.data.networks.store.DefaultNetworksStatusesStore import com.tangem.data.networks.store.DefaultNetworksStatusesStore
import com.tangem.data.networks.store.NetworksStatusesStore import com.tangem.data.networks.store.NetworksStatusesStore
import com.tangem.data.networks.utils.DefaultNetworksCleaner
import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.network.entity.NetworkStatusDM 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.mapWithStringKeyTypes
import com.tangem.datasource.utils.setTypes import com.tangem.datasource.utils.setTypes
import com.tangem.domain.networks.repository.NetworksRepository import com.tangem.domain.networks.repository.NetworksRepository
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 dagger.Module import dagger.Module
@ -67,4 +69,18 @@ internal object NetworkDataModule {
dispatchers = dispatchers, dispatchers = dispatchers,
) )
} }
@Provides
@Singleton
fun provideNetworksCleaner(
networksStatusesStore: NetworksStatusesStore,
walletManagersFacade: WalletManagersFacade,
dispatchers: CoroutineDispatcherProvider,
): NetworksCleaner {
return DefaultNetworksCleaner(
networksStatusesStore = networksStatusesStore,
walletManagersFacade = walletManagersFacade,
dispatchers = dispatchers,
)
}
} }

View file

@ -96,6 +96,20 @@ internal class DefaultNetworksStatusesStore(
} }
} }
override suspend fun clear(userWalletId: UserWalletId, networks: Set<Network>) {
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( private suspend fun updateInRuntime(
userWalletId: UserWalletId, userWalletId: UserWalletId,
networks: Set<Network>, networks: Set<Network>,

View file

@ -41,4 +41,7 @@ internal interface NetworksStatusesStore {
* See complex methods in `NetworksStatusesStoreExt`. * See complex methods in `NetworksStatusesStoreExt`.
*/ */
suspend fun store(userWalletId: UserWalletId, status: NetworkStatus) suspend fun store(userWalletId: UserWalletId, status: NetworkStatus)
/** Clear statuses of [networks] by [userWalletId] */
suspend fun clear(userWalletId: UserWalletId, networks: Set<Network>)
} }

View file

@ -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<CryptoCurrency>) {
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<Network>) {
if (networks.isNotEmpty()) {
networksStatusesStore.clear(userWalletId = userWalletId, networks = networks)
}
}
private suspend fun cleanWalletManager(
userWalletId: UserWalletId,
networks: Set<Network>,
tokens: Set<CryptoCurrency.Token>,
) {
if (networks.isNotEmpty()) {
walletManagersFacade.remove(userWalletId = userWalletId, networks = networks)
}
if (tokens.isNotEmpty()) {
walletManagersFacade.removeTokens(userWalletId = userWalletId, tokens = tokens)
}
}
private fun List<CryptoCurrency>.partitionByType(): Pair<Set<Network>, Set<CryptoCurrency.Token>> {
val networks = mutableSetOf<Network>()
val tokens = mutableSetOf<CryptoCurrency.Token>()
for (currency in this) {
when (currency) {
is CryptoCurrency.Coin -> networks.add(currency.network)
is CryptoCurrency.Token -> tokens.add(currency)
}
}
return Pair(networks, tokens)
}
}

View file

@ -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<NetworksStatusesStore>(relaxed = true)
private val walletManagersFacade = mockk<WalletManagersFacade>(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())
}
}
}

View file

@ -10,6 +10,7 @@ import com.tangem.data.staking.DefaultStakingTransactionHashRepository
import com.tangem.data.staking.converters.error.StakeKitErrorConverter import com.tangem.data.staking.converters.error.StakeKitErrorConverter
import com.tangem.data.staking.store.YieldsBalancesStore import com.tangem.data.staking.store.YieldsBalancesStore
import com.tangem.data.staking.toggles.DefaultStakingFeatureToggles 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.StakeKitApi
import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse
import com.tangem.datasource.di.NetworkMoshi 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.StakingRepository
import com.tangem.domain.staking.repositories.StakingTransactionHashRepository import com.tangem.domain.staking.repositories.StakingTransactionHashRepository
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.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -102,4 +104,16 @@ internal object StakingDataModule {
fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): StakingFeatureToggles { fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): StakingFeatureToggles {
return DefaultStakingFeatureToggles(featureTogglesManager) return DefaultStakingFeatureToggles(featureTogglesManager)
} }
@Provides
@Singleton
fun provideStakingCleaner(
yieldsBalancesStore: YieldsBalancesStore,
dispatchers: CoroutineDispatcherProvider,
): StakingCleaner {
return DefaultStakingCleaner(
yieldsBalancesStore = yieldsBalancesStore,
dispatchers = dispatchers,
)
}
} }

View file

@ -96,6 +96,16 @@ internal class DefaultYieldsBalancesStore(
) )
} }
override suspend fun clear(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
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<YieldBalanceWrapperDTO>) { private suspend fun storeInRuntime(userWalletId: UserWalletId, values: Set<YieldBalanceWrapperDTO>) {
val newBalances = YieldBalanceConverter(isCached = false).convertSet(input = values) val newBalances = YieldBalanceConverter(isCached = false).convertSet(input = values)
.filterNotNull() .filterNotNull()

View file

@ -33,4 +33,7 @@ interface YieldsBalancesStore {
/** Store error by [userWalletId] and [stakingIds] */ /** Store error by [userWalletId] and [stakingIds] */
suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set<StakingID>) suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
/** Clear balances of [stakingIds] by [userWalletId] */
suspend fun clear(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
} }

View file

@ -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<StakingID>) {
if (stakingIds.isEmpty()) return
with(dispatchers.default) {
yieldsBalancesStore.clear(userWalletId, stakingIds)
}
}
}

View file

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

View file

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

View file

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