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

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

View file

@ -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<StakingID>) {
if (stakingIds.isEmpty()) return
override suspend fun invoke(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
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<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
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<StakingIdFactory>(relaxed = true)
private val stakingBalancesStore = mockk<StakingBalancesStore>(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))
}
}
}
}