Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-09 14:19:18 +03:00
commit 59228972a9
1705 changed files with 82417 additions and 14905 deletions

View file

@ -5,66 +5,76 @@ plugins {
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
alias(deps.plugins.ksp)
id("configuration")
}
android {
namespace = "com.tangem.data.staking"
}
dependencies {
/** Core modules */
implementation(projects.core.datasource)
implementation(projects.core.utils)
/** Common modules */
implementation(projects.data.common)
/** Domain modules */
implementation(projects.core.configToggles)
implementation(projects.domain.tokens.models)
implementation(projects.domain.staking)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.legacy)
implementation(projects.domain.walletManager)
implementation(projects.domain.card)
implementation(projects.domain.models)
/** Feature Api modules */
implementation(projects.features.staking.api)
// region DI
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
// region Kotlin
api(deps.kotlin.coroutines)
implementation(deps.kotlin.datetime)
// endregion
// region Others dependencies
implementation(deps.androidx.datastore)
implementation(deps.jodatime)
implementation(deps.kotlin.coroutines)
implementation(deps.kotlin.datetime)
implementation(deps.kotlin.immutable.collections)
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
// region Other libraries
api(deps.androidx.datastore)
api(deps.moshi)
implementation(deps.arrow.core)
implementation(deps.firebase.crashlytics)
ksp(deps.moshi.kotlin.codegen)
implementation(deps.jodatime)
implementation(deps.kotlin.immutable.collections)
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
// endregion
implementation(projects.libs.blockchainSdk)
implementation(projects.libs.crypto)
implementation(tangemDeps.card.core)
// region Tangem SDK
implementation(tangemDeps.blockchain) {
exclude(module = "joda-time")
}
implementation(tangemDeps.card.core)
// endregion
testImplementation(tangemDeps.card.core)
// region DI
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
// endregion
// region Core modules
api(projects.core.analytics)
api(projects.core.configToggles)
api(projects.core.datasource)
api(projects.core.utils)
implementation(projects.core.analytics.models)
// endregion
// region Data
implementation(projects.data.common)
// endregion
// region Domain
api(projects.domain.common)
api(projects.domain.core)
api(projects.domain.models)
api(projects.domain.staking)
api(projects.domain.walletManager)
api(projects.domain.wallets)
implementation(projects.domain.card)
// endregion
// region Domain models
api(projects.domain.staking.models)
implementation(projects.domain.wallets.models)
// endregion
// region Libs
implementation(projects.libs.blockchainSdk)
implementation(projects.libs.crypto)
// endregion
// region Tests
testImplementation(projects.common.test)
testImplementation(projects.test.core)
}
// endregion
}

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

@ -21,7 +21,7 @@ internal class DefaultStakingFeatureToggles(
}
private fun StakingIntegrationID.getFeatureToggle(): FeatureToggles? = when (this) {
is StakingIntegrationID.P2PEthPool -> FeatureToggles.STAKING_ETH_ENABLED
is StakingIntegrationID.P2PEthPool -> null
is StakingIntegrationID.StakeKit -> this.getStakeKitFeatureToggle()
}

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

@ -1,9 +1,9 @@
package com.tangem.data.staking.toggles
import com.tangem.core.configtoggle.FeatureToggles
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.staking.model.StakingIntegrationID
import com.google.common.truth.Truth.assertThat
import com.tangem.core.configtoggle.FeatureToggles
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockk
@ -24,21 +24,10 @@ internal class DefaultStakingFeatureTogglesTest {
}
@Test
fun `P2PEthPool returns true when STAKING_ETH_ENABLED is enabled`() {
every { featureTogglesManager.isFeatureEnabled(FeatureToggles.STAKING_ETH_ENABLED) } returns true
fun `P2PEthPool integration is always enabled`() {
assertThat(toggles.isIntegrationEnabled(StakingIntegrationID.P2PEthPool)).isTrue()
verify(exactly = 1) { featureTogglesManager.isFeatureEnabled(FeatureToggles.STAKING_ETH_ENABLED) }
}
@Test
fun `P2PEthPool returns false when STAKING_ETH_ENABLED is disabled`() {
every { featureTogglesManager.isFeatureEnabled(FeatureToggles.STAKING_ETH_ENABLED) } returns false
assertThat(toggles.isIntegrationEnabled(StakingIntegrationID.P2PEthPool)).isFalse()
verify(exactly = 1) { featureTogglesManager.isFeatureEnabled(FeatureToggles.STAKING_ETH_ENABLED) }
verify(exactly = 0) { featureTogglesManager.isFeatureEnabled(any()) }
}
@Test

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