Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-02 19:19:32 +04:00
parent a9fbfc8a53
commit 52a607c1f5
9 changed files with 153 additions and 40 deletions

View file

@ -2,6 +2,7 @@ package com.tangem.tap.di.domain
import com.tangem.domain.staking.*
import com.tangem.domain.staking.repositories.*
import com.tangem.domain.staking.toggles.StakingFeatureToggles
import com.tangem.domain.staking.single.SingleStakingBalanceFetcher
import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -226,8 +227,14 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideStakingIdFactory(walletManagersFacade: WalletManagersFacade): StakingIdFactory {
return StakingIdFactory(walletManagersFacade = walletManagersFacade)
fun provideStakingIdFactory(
walletManagersFacade: WalletManagersFacade,
stakingFeatureToggles: StakingFeatureToggles,
): StakingIdFactory {
return StakingIdFactory(
walletManagersFacade = walletManagersFacade,
stakingFeatureToggles = stakingFeatureToggles,
)
}
@Provides

View file

@ -22,6 +22,7 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolUnsignedTx
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.staking.toggles.StakingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
@ -65,7 +66,7 @@ internal class DefaultP2PEthPoolRepository(
}
override suspend fun fetchVaults(network: P2PEthPoolNetwork) {
val vaults = if (stakingFeatureToggles.isEthStakingEnabled) {
val vaults = if (stakingFeatureToggles.isIntegrationEnabled(StakingIntegrationID.P2PEthPool)) {
getVaults(network).getOrElse { error ->
TangemLogger.e("Error fetching P2PEthPool vaults: $error")
emptyList()

View file

@ -1,8 +1,6 @@
package com.tangem.data.staking
import arrow.core.getOrElse
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.data.staking.store.StakeKitBalancesStore
import com.tangem.domain.card.common.TapWorkarounds.isWallet2
import com.tangem.domain.models.currency.CryptoCurrency
@ -22,14 +20,13 @@ import com.tangem.lib.crypto.BlockchainUtils.isSolana
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.withContext
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
internal class DefaultStakingRepository(
private val stakeKitRepository: StakeKitRepository,
private val p2pEthPoolRepository: P2PEthPoolRepository,
private val stakingBalanceStoreV2: StakeKitBalancesStore,
private val stakeKitBalancesStore: StakeKitBalancesStore,
private val dispatchers: CoroutineDispatcherProvider,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val stakingFeatureToggles: StakingFeatureToggles,
@ -40,7 +37,8 @@ internal class DefaultStakingRepository(
cryptoCurrency: CryptoCurrency,
): Flow<StakingAvailability> {
return channelFlow {
if (!checkFeatureToggleEnabled(cryptoCurrency)) {
val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id)
if (stakingIntegration == null || !stakingFeatureToggles.isIntegrationEnabled(stakingIntegration)) {
send(StakingAvailability.Unavailable)
return@channelFlow
}
@ -56,8 +54,6 @@ internal class DefaultStakingRepository(
return@channelFlow
}
val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id)
val availabilityFlow = when (stakingIntegration) {
StakingIntegrationID.P2PEthPool -> p2pEthPoolRepository.getStakingAvailability()
is StakingIntegrationID.StakeKit -> stakeKitRepository.getStakingAvailability(
@ -65,7 +61,6 @@ internal class DefaultStakingRepository(
rawCurrencyId,
cryptoCurrency.symbol,
)
null -> flowOf(StakingAvailability.Unavailable)
}
availabilityFlow.collect { send(it) }
@ -76,20 +71,15 @@ internal class DefaultStakingRepository(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): StakingAvailability {
if (!checkFeatureToggleEnabled(cryptoCurrency)) {
return StakingAvailability.Unavailable
}
val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id)
?.takeIf(stakingFeatureToggles::isIntegrationEnabled)
?: return StakingAvailability.Unavailable
if (checkForInvalidCardBatch(userWalletId, cryptoCurrency)) {
return StakingAvailability.Unavailable
}
val rawCurrencyId = cryptoCurrency.id.rawCurrencyId
if (rawCurrencyId == null) {
return StakingAvailability.Unavailable
}
val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id)
?: return StakingAvailability.Unavailable
return when (stakingIntegration) {
@ -104,7 +94,7 @@ internal class DefaultStakingRepository(
override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean {
return withContext(dispatchers.default) {
val balances = stakingBalanceStoreV2.getAllSyncOrNull(userWalletId) ?: return@withContext false
val balances = stakeKitBalancesStore.getAllSyncOrNull(userWalletId) ?: return@withContext false
val hasDataStakingBalance by lazy {
balances.any { stakingBalance ->
@ -116,18 +106,6 @@ internal class DefaultStakingRepository(
}
}
private fun checkFeatureToggleEnabled(cryptoCurrency: CryptoCurrency): Boolean {
return when (cryptoCurrency.network.id.toBlockchain()) {
Blockchain.Ethereum -> {
when (cryptoCurrency) {
is CryptoCurrency.Coin -> stakingFeatureToggles.isEthStakingEnabled
is CryptoCurrency.Token -> true
}
}
else -> true
}
}
private fun checkForInvalidCardBatch(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
val userWallet = getUserWalletUseCase(userWalletId).getOrElse {
error("Failed to get user wallet")

View file

@ -65,7 +65,7 @@ internal object StakingDataModule {
return DefaultStakingRepository(
stakeKitRepository = stakeKitRepository,
p2pEthPoolRepository = p2pEthPoolRepository,
stakingBalanceStoreV2 = stakeKitBalancesStore,
stakeKitBalancesStore = stakeKitBalancesStore,
dispatchers = dispatchers,
getUserWalletUseCase = getUserWalletUseCase,
stakingFeatureToggles = stakingFeatureToggles,

View file

@ -2,12 +2,35 @@ 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.tangem.domain.staking.toggles.StakingFeatureToggles
internal class DefaultStakingFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : StakingFeatureToggles {
override val isEthStakingEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.STAKING_ETH_ENABLED)
override fun isIntegrationEnabled(integrationId: StakingIntegrationID): Boolean {
val toggle = integrationId.getFeatureToggle() ?: return true
return featureTogglesManager.isFeatureEnabled(toggle)
}
private fun StakingIntegrationID.getFeatureToggle(): FeatureToggles? = when (this) {
is StakingIntegrationID.P2PEthPool -> FeatureToggles.STAKING_ETH_ENABLED
is StakingIntegrationID.StakeKit -> this.getStakeKitFeatureToggle()
}
private fun StakingIntegrationID.StakeKit.getStakeKitFeatureToggle(): FeatureToggles? = when (this) {
is StakingIntegrationID.StakeKit.Coin -> when (this) {
StakingIntegrationID.StakeKit.Coin.Ton,
StakingIntegrationID.StakeKit.Coin.Solana,
StakingIntegrationID.StakeKit.Coin.Cosmos,
StakingIntegrationID.StakeKit.Coin.Tron,
StakingIntegrationID.StakeKit.Coin.BSC,
StakingIntegrationID.StakeKit.Coin.Cardano,
-> null
}
is StakingIntegrationID.StakeKit.EthereumToken -> when (this) {
StakingIntegrationID.StakeKit.EthereumToken.Polygon -> null
}
}
}

View file

@ -0,0 +1,61 @@
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 io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class DefaultStakingFeatureTogglesTest {
private val featureTogglesManager: FeatureTogglesManager = mockk()
private val toggles = DefaultStakingFeatureToggles(featureTogglesManager = featureTogglesManager)
@BeforeEach
fun resetMocks() {
clearMocks(featureTogglesManager)
}
@Test
fun `P2PEthPool returns true when STAKING_ETH_ENABLED is enabled`() {
every { featureTogglesManager.isFeatureEnabled(FeatureToggles.STAKING_ETH_ENABLED) } returns true
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) }
}
@Test
fun `existing StakeKit Coin integrations are always enabled`() {
StakingIntegrationID.StakeKit.Coin.entries.forEach { coin ->
assertThat(toggles.isIntegrationEnabled(coin)).isTrue()
}
verify(exactly = 0) { featureTogglesManager.isFeatureEnabled(any()) }
}
@Test
fun `existing StakeKit EthereumToken integrations are always enabled`() {
StakingIntegrationID.StakeKit.EthereumToken.entries.forEach { token ->
assertThat(toggles.isIntegrationEnabled(token)).isTrue()
}
verify(exactly = 0) { featureTogglesManager.isFeatureEnabled(any()) }
}
}

View file

@ -2,23 +2,27 @@ package com.tangem.domain.staking
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import arrow.core.raise.ensureNotNull
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.models.staking.StakingID
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.staking.toggles.StakingFeatureToggles
import com.tangem.domain.walletmanager.WalletManagersFacade
/**
* Factory class for creating instances of [StakingID]
*
* @property walletManagersFacade wallet manager facade
* @property walletManagersFacade wallet manager facade
* @property stakingFeatureToggles staking feature toggles
*
[REDACTED_AUTHOR]
*/
class StakingIdFactory(
private val walletManagersFacade: WalletManagersFacade,
private val stakingFeatureToggles: StakingFeatureToggles,
) {
/**
@ -72,6 +76,8 @@ class StakingIdFactory(
ensureNotNull(integrationId) { Error.UnsupportedCurrency }
ensure(stakingFeatureToggles.isIntegrationEnabled(integrationId)) { Error.UnsupportedCurrency }
val address = defaultAddressProvider().takeUnless { it.isNullOrEmpty() }
ensureNotNull(address) { Error.UnableToGetAddress(integrationId = integrationId) }

View file

@ -1,5 +1,8 @@
package com.tangem.domain.staking.toggles
import com.tangem.domain.staking.model.StakingIntegrationID
interface StakingFeatureToggles {
val isEthStakingEnabled: Boolean
fun isIntegrationEnabled(integrationId: StakingIntegrationID): Boolean
}

View file

@ -10,11 +10,13 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.staking.StakingID
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.staking.toggles.StakingFeatureToggles
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.test.core.ProvideTestModels
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
@ -30,11 +32,16 @@ import org.junit.jupiter.params.ParameterizedTest
internal class StakingIdFactoryTest {
private val walletManagersFacade: WalletManagersFacade = mockk()
private val factory = StakingIdFactory(walletManagersFacade = walletManagersFacade)
private val stakingFeatureToggles: StakingFeatureToggles = mockk()
private val factory = StakingIdFactory(
walletManagersFacade = walletManagersFacade,
stakingFeatureToggles = stakingFeatureToggles,
)
@BeforeEach
fun resetMocks() {
clearMocks(walletManagersFacade)
clearMocks(walletManagersFacade, stakingFeatureToggles)
every { stakingFeatureToggles.isIntegrationEnabled(any()) } returns true
}
@Nested
@ -66,6 +73,33 @@ internal class StakingIdFactoryTest {
}
}
@Test
fun `create returns UnsupportedCurrency if integration is disabled by toggle`() = runTest {
// Arrange
val userWalletId = UserWalletId(stringValue = "011")
val currency = MockCryptoCurrencyFactory().createCoin(Blockchain.TON)
every {
stakingFeatureToggles.isIntegrationEnabled(StakingIntegrationID.StakeKit.Coin.Ton)
} returns false
// Act
val actual = factory.create(
userWalletId = userWalletId,
currencyId = currency.id,
network = currency.network,
)
// Assert
val expected = StakingIdFactory.Error.UnsupportedCurrency
Truth.assertThat(actual.leftOrNull()).isEqualTo(expected)
coVerify(inverse = true) {
walletManagersFacade.getDefaultAddress(userWalletId = any(), network = any())
}
}
@Test
fun `create returns UnableToGetAddress if address is null`() = runTest {
// Arrange