Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-06 17:08:05 +04:00
parent 017eece8b7
commit d96dd19f01
23 changed files with 456 additions and 8 deletions

View file

@ -32,4 +32,7 @@ interface BaseStakingBalancesStore {
/** Clear staking balances */
suspend fun clear(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
/** Remove all staking balances for the given [userWalletIds] */
suspend fun remove(userWalletIds: List<UserWalletId>)
}

View file

@ -5,6 +5,8 @@ import com.tangem.data.staking.converters.ethpool.P2PEthPoolStakingBalanceConver
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.coroutines.runSuspendCatching
import com.tangem.utils.logging.TangemLogger
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.staking.StakingID
@ -108,6 +110,25 @@ internal class DefaultP2PEthPoolBalancesStore(
}
}
override suspend fun remove(userWalletIds: List<UserWalletId>) {
if (userWalletIds.isEmpty()) return
val walletIds = userWalletIds.toSet()
val stringKeys = userWalletIds.mapTo(hashSetOf()) { it.stringValue }
// Best-effort: attempt both runtime and persistence removal even if one fails.
coroutineScope {
launch {
runSuspendCatching { runtimeStore.update(default = emptyMap()) { stored -> stored - walletIds } }
.onFailure { TangemLogger.e("Failed to remove P2PEthPool staking balances from runtime", it) }
}
launch {
runSuspendCatching { persistenceStore.updateData { stored -> stored - stringKeys } }
.onFailure { TangemLogger.e("Failed to remove P2PEthPool staking balances from persistence", it) }
}
}
}
private suspend fun storeInRuntime(userWalletId: UserWalletId, values: Set<P2PEthPoolAccountResponse>) {
val newBalances = P2PEthPoolStakingBalanceConverter.convertAll(
responses = values,

View file

@ -5,6 +5,8 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrap
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.token.converter.StakingBalanceConverter
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.coroutines.runSuspendCatching
import com.tangem.utils.logging.TangemLogger
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.staking.StakingID
@ -99,6 +101,25 @@ internal class DefaultStakeKitBalancesStore(
}
}
override suspend fun remove(userWalletIds: List<UserWalletId>) {
if (userWalletIds.isEmpty()) return
val walletIds = userWalletIds.toSet()
val stringKeys = userWalletIds.mapTo(hashSetOf()) { it.stringValue }
// Best-effort: attempt both runtime and persistence removal even if one fails.
coroutineScope {
launch {
runSuspendCatching { runtimeStore.update(default = emptyMap()) { stored -> stored - walletIds } }
.onFailure { TangemLogger.e("Failed to remove StakeKit staking balances from runtime", it) }
}
launch {
runSuspendCatching { persistenceStore.updateData { stored -> stored - stringKeys } }
.onFailure { TangemLogger.e("Failed to remove StakeKit staking balances from persistence", it) }
}
}
}
private suspend fun clearInRuntime(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
runtimeStore.update(default = emptyMap()) { stored ->
stored.toMutableMap().apply {

View file

@ -30,6 +30,23 @@ internal class DefaultStakingCleaner(
private val dispatchers: CoroutineDispatcherProvider,
) : StakingCleaner {
override suspend fun clear(userWalletIds: List<UserWalletId>) {
if (userWalletIds.isEmpty()) return
// Best-effort: isolate failures per store so one failing store does not cancel the other.
withContext(dispatchers.default) {
awaitAll(
async { removeSafely(store = "StakeKit") { stakeKitBalancesStore.remove(userWalletIds) } },
async { removeSafely(store = "P2PEthPool") { p2pEthPoolBalancesStore.remove(userWalletIds) } },
)
}
}
private suspend fun removeSafely(store: String, remove: suspend () -> Unit) {
runSuspendCatching { remove() }
.onFailure { TangemLogger.e("Failed to remove $store staking balances", it) }
}
override suspend fun invoke(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
if (currencies.isEmpty()) {
TangemLogger.d("No currencies to clear for wallet: $userWalletId")

View file

@ -167,4 +167,36 @@ class DefaultStakingCleanerTest {
}
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class ClearByWalletIds {
@Test
fun `GIVEN wallets WHEN clear THEN both balances stores removed once with all ids`() = runTest {
// Arrange
val ids = listOf(UserWalletId("011"), UserWalletId("022"))
// Act
cleaner.clear(userWalletIds = ids)
// Assert
coVerify(exactly = 1) {
stakeKitBalancesStore.remove(ids)
p2pEthPoolBalancesStore.remove(ids)
}
}
@Test
fun `GIVEN empty list WHEN clear THEN stores are not touched`() = runTest {
// Act
cleaner.clear(userWalletIds = emptyList())
// Assert
coVerify(inverse = true) {
stakeKitBalancesStore.remove(any())
p2pEthPoolBalancesStore.remove(any())
}
}
}
}