From 52a607c1f5f4e479be9bb9dd65bc78d18f1eb540 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 2 Apr 2026 19:19:32 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/StakingDomainModule.kt | 11 +++- .../staking/DefaultP2PEthPoolRepository.kt | 3 +- .../data/staking/DefaultStakingRepository.kt | 38 +++--------- .../data/staking/di/StakingDataModule.kt | 2 +- .../toggles/DefaultStakingFeatureToggles.kt | 27 +++++++- .../DefaultStakingFeatureTogglesTest.kt | 61 +++++++++++++++++++ .../tangem/domain/staking/StakingIdFactory.kt | 8 ++- .../staking/toggles/StakingFeatureToggles.kt | 5 +- .../domain/staking/StakingIdFactoryTest.kt | 38 +++++++++++- 9 files changed, 153 insertions(+), 40 deletions(-) create mode 100644 data/staking/src/test/kotlin/com/tangem/data/staking/toggles/DefaultStakingFeatureTogglesTest.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index a823079f33..5633c8744b 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -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 diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt index b189675a38..1403dcdaf9 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt @@ -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() diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index b46d6ea594..f98c982fa6 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -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 { 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") diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index 0aa8abf1b5..a997cbce26 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -65,7 +65,7 @@ internal object StakingDataModule { return DefaultStakingRepository( stakeKitRepository = stakeKitRepository, p2pEthPoolRepository = p2pEthPoolRepository, - stakingBalanceStoreV2 = stakeKitBalancesStore, + stakeKitBalancesStore = stakeKitBalancesStore, dispatchers = dispatchers, getUserWalletUseCase = getUserWalletUseCase, stakingFeatureToggles = stakingFeatureToggles, diff --git a/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt b/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt index 011cc73bf9..6f62c8bc06 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt @@ -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 + } + } } \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/toggles/DefaultStakingFeatureTogglesTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/toggles/DefaultStakingFeatureTogglesTest.kt new file mode 100644 index 0000000000..ca6766c8c8 --- /dev/null +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/toggles/DefaultStakingFeatureTogglesTest.kt @@ -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()) } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/StakingIdFactory.kt b/domain/staking/src/main/java/com/tangem/domain/staking/StakingIdFactory.kt index c8648ad64c..6fb6297f3f 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/StakingIdFactory.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/StakingIdFactory.kt @@ -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) } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt b/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt index 3553692065..80761562fc 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt @@ -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 } \ No newline at end of file diff --git a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt index 71a5d271ed..3211cdff64 100644 --- a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt +++ b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt @@ -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