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 f6341c0957..7ee32bed6f 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 @@ -94,7 +94,7 @@ internal class DefaultStakingRepository( override suspend fun fetchEnabledYields() { withContext(dispatchers.io) { - val yieldsResponses = getAvailableIntegrationsIds().map { + val yieldsResponses = getAvailableStakeKitIntegrationsIds().map { async { it.getYieldRequest() } }.awaitAll() @@ -157,13 +157,13 @@ internal class DefaultStakingRepository( } } - private suspend fun StakingIntegrationID.getYieldRequest(): ApiResponse { + private suspend fun StakingIntegrationID.StakeKit.getYieldRequest(): ApiResponse { return when (this) { - is StakingIntegrationID.Coin -> stakeKitApi.getEnabledYields( + is StakingIntegrationID.StakeKit.Coin -> stakeKitApi.getEnabledYields( preferredValidatorsOnly = false, network = networkId, ) - is StakingIntegrationID.EthereumToken -> stakeKitApi.getEnabledYields( + is StakingIntegrationID.StakeKit.EthereumToken -> stakeKitApi.getEnabledYields( preferredValidatorsOnly = false, yieldId = value, network = networkId, @@ -171,8 +171,8 @@ internal class DefaultStakingRepository( } } - private fun getAvailableIntegrationsIds(): List { - return StakingIntegrationID.entries.filterNot { + private fun getAvailableStakeKitIntegrationsIds(): List { + return StakingIntegrationID.StakeKit.entries.filterNot { it.blockchain == Blockchain.Cardano && !stakingFeatureToggles.isCardanoStakingEnabled } } @@ -296,6 +296,7 @@ internal class DefaultStakingRepository( return when (networkId.toBlockchain()) { Blockchain.TON -> stakingFeatureToggles.isTonStakingEnabled Blockchain.Cardano -> stakingFeatureToggles.isCardanoStakingEnabled + Blockchain.Ethereum -> stakingFeatureToggles.isEthStakingEnabled else -> true } } diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/utils/DefaultStakingCleanerTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/utils/DefaultStakingCleanerTest.kt index 08dc91f9d6..32751b6fc6 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/utils/DefaultStakingCleanerTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/utils/DefaultStakingCleanerTest.kt @@ -23,7 +23,7 @@ class DefaultStakingCleanerTest { ) private val userWalletId = UserWalletId("011") private val stakingIds = setOf( - StakingID(integrationId = StakingIntegrationID.Coin.Cardano.value, address = "0x1"), + StakingID(integrationId = StakingIntegrationID.StakeKit.Coin.Cardano.value, address = "0x1"), ) @BeforeEach diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetActionRequirementAmountUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetActionRequirementAmountUseCase.kt index c3c236025c..462344cd70 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetActionRequirementAmountUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetActionRequirementAmountUseCase.kt @@ -7,7 +7,7 @@ import java.math.BigDecimal class GetActionRequirementAmountUseCase { operator fun invoke(integrationId: String, actionType: StakingActionType): BigDecimal? { - return if (StakingIntegrationID.EthereumToken.Polygon.value == integrationId && + return if (StakingIntegrationID.StakeKit.EthereumToken.Polygon.value == integrationId && actionType == StakingActionType.CLAIM_REWARDS ) { BigDecimal.ONE diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegrationID.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegrationID.kt index 43e2488895..3ce629bf6f 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegrationID.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegrationID.kt @@ -31,56 +31,79 @@ sealed interface StakingIntegrationID { */ val networkId: String - /** Represents blockchains whose native coins can be staked */ - enum class Coin : StakingIntegrationID { - Ton { - override val value: String = "ton-ton-chorus-one-pools-staking" - override val blockchain: Blockchain = Blockchain.TON - override val networkId: String = "ton" - }, - Solana { - override val value: String = "solana-sol-native-multivalidator-staking" - override val blockchain: Blockchain = Blockchain.Solana - override val networkId: String = "solana" - }, - Cosmos { - override val value: String = "cosmos-atom-native-staking" - override val blockchain: Blockchain = Blockchain.Cosmos - override val networkId: String = "cosmos" - }, - Tron { - override val value: String = "tron-trx-native-staking" - override val blockchain: Blockchain = Blockchain.Tron - override val networkId: String = "tron" - }, - BSC { - override val value: String = "bsc-bnb-native-staking" - override val blockchain: Blockchain = Blockchain.BSC - override val networkId: String = "binance" - }, - Cardano { - override val value: String = "cardano-ada-native-staking" - override val blockchain: Blockchain = Blockchain.Cardano - override val networkId: String = "cardano" - }, + /** + * Represents StakeKit staking integrations + */ + sealed interface StakeKit : StakingIntegrationID { + + /** Represents blockchains whose native coins can be staked via StakeKit */ + enum class Coin : StakeKit { + Ton { + override val value: String = "ton-ton-chorus-one-pools-staking" + override val blockchain: Blockchain = Blockchain.TON + override val networkId: String = "ton" + }, + Solana { + override val value: String = "solana-sol-native-multivalidator-staking" + override val blockchain: Blockchain = Blockchain.Solana + override val networkId: String = "solana" + }, + Cosmos { + override val value: String = "cosmos-atom-native-staking" + override val blockchain: Blockchain = Blockchain.Cosmos + override val networkId: String = "cosmos" + }, + Tron { + override val value: String = "tron-trx-native-staking" + override val blockchain: Blockchain = Blockchain.Tron + override val networkId: String = "tron" + }, + BSC { + override val value: String = "bsc-bnb-native-staking" + override val blockchain: Blockchain = Blockchain.BSC + override val networkId: String = "binance" + }, + Cardano { + override val value: String = "cardano-ada-native-staking" + override val blockchain: Blockchain = Blockchain.Cardano + override val networkId: String = "cardano" + }, + } + + /** + * Represents staking integrations for Ethereum-based tokens via StakeKit + * + * @property subBlockchain the specific Ethereum-based blockchain associated with the integration + */ + enum class EthereumToken(val subBlockchain: Blockchain) : StakeKit { + Polygon(subBlockchain = Blockchain.Polygon) { + override val value: String = "ethereum-matic-native-staking" + override val approval: StakingApproval.Needed = + StakingApproval.Needed(spenderAddress = "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908") + override val networkId: String = "ethereum" + }, + ; + + /** Blockchain associated with Ethereum-based staking integrations. */ + override val blockchain: Blockchain = Blockchain.Ethereum + } + + companion object { + val entries: List by lazy { + Coin.entries + EthereumToken.entries + } + } } /** - * Represents staking integrations for Ethereum-based tokens - * - * @property subBlockchain the specific Ethereum-based blockchain associated with the integration + * Represents P2P staking integrations */ - enum class EthereumToken(val subBlockchain: Blockchain) : StakingIntegrationID { - Polygon(subBlockchain = Blockchain.Polygon) { - override val value: String = "ethereum-matic-native-staking" - override val approval: StakingApproval.Needed = - StakingApproval.Needed(spenderAddress = "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908") + enum class P2P : StakingIntegrationID { + EthereumPooled { + override val value: String = "p2p-ethereum-pooled" + override val blockchain: Blockchain = Blockchain.Ethereum override val networkId: String = "ethereum" }, - ; - - /** Blockchain associated with Ethereum-based staking integrations. */ - override val blockchain: Blockchain = Blockchain.Ethereum } // Polkadot { @@ -111,7 +134,9 @@ sealed interface StakingIntegrationID { companion object { /** List of all native staking integration IDs */ - val entries: List by lazy { Coin.entries + EthereumToken.entries } + val entries: List by lazy { + StakeKit.Coin.entries + StakeKit.EthereumToken.entries + P2P.entries + } /** * Creates a [StakingIntegrationID] for the given cryptocurrency ID @@ -123,22 +148,14 @@ sealed interface StakingIntegrationID { fun create(currencyId: CryptoCurrency.ID): StakingIntegrationID? { val blockchain = Blockchain.fromId(id = currencyId.rawNetworkId) - val integrationId = blockchain.integrationId ?: return null - - if (integrationId is Coin && !currencyId.contractAddress.isNullOrBlank()) { - return null - } - - return when (integrationId) { - is Coin -> integrationId - is EthereumToken -> { - val coinId = integrationId.subBlockchain.toMigratedCoinId() - - if (coinId == currencyId.rawCurrencyId?.value) { - integrationId - } else { - null - } + return if (currencyId.contractAddress.isNullOrBlank()) { + // Order is not important — either P2P or Stakekit.Coin can be in any order + P2P.entries.firstOrNull { it.blockchain == blockchain } + ?: StakeKit.Coin.entries.firstOrNull { it.blockchain == blockchain } + } else { + StakeKit.EthereumToken.entries.firstOrNull { token -> + token.blockchain == blockchain && + token.subBlockchain.toMigratedCoinId() == currencyId.rawCurrencyId?.value } } } @@ -154,12 +171,4 @@ val Network.isStakingSupported: Boolean * @return `true` if staking is supported, `false` otherwise. */ val Blockchain.isStakingSupported: Boolean - get() = StakingIntegrationID.entries.any { it.blockchain == this } - -/** - * Extension property to retrieve the staking integration ID for a blockchain. - * - * @return the [StakingIntegrationID] if available, or `null` if not supported. - */ -val Blockchain.integrationId: StakingIntegrationID? - get() = StakingIntegrationID.entries.firstOrNull { it.blockchain == this } \ No newline at end of file + get() = StakingIntegrationID.entries.any { it.blockchain == this } \ 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 962f74dec7..dc18df883b 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 @@ -47,7 +47,7 @@ internal class StakingIdFactoryTest { fun `create returns UnsupportedCurrency if integrationId is null`() = runTest { // Arrange val userWalletId = UserWalletId(stringValue = "011") - val currency = MockCryptoCurrencyFactory().ethereum + val currency = MockCryptoCurrencyFactory().createCoin(Blockchain.Bitcoin) // Act val actual = factory.create( @@ -85,7 +85,7 @@ internal class StakingIdFactoryTest { // Assert val expected = StakingIdFactory.Error.UnableToGetAddress( - integrationId = StakingIntegrationID.Coin.Ton, + integrationId = StakingIntegrationID.StakeKit.Coin.Ton, ) Truth.assertThat(actual.leftOrNull()).isEqualTo(expected) @@ -126,31 +126,37 @@ internal class StakingIdFactoryTest { ), CreateModel( currencyId = createCurrencyId(blockchain = Blockchain.TON), - expected = createStakingId(integrationId = StakingIntegrationID.Coin.Ton), + expected = createStakingId(integrationId = StakingIntegrationID.StakeKit.Coin.Ton), ), CreateModel( currencyId = createCurrencyId(blockchain = Blockchain.Solana), - expected = createStakingId(integrationId = StakingIntegrationID.Coin.Solana), + expected = createStakingId(integrationId = StakingIntegrationID.StakeKit.Coin.Solana), ), CreateModel( currencyId = createCurrencyId(blockchain = Blockchain.Cosmos), - expected = createStakingId(integrationId = StakingIntegrationID.Coin.Cosmos), + expected = createStakingId(integrationId = StakingIntegrationID.StakeKit.Coin.Cosmos), ), CreateModel( currencyId = createCurrencyId(blockchain = Blockchain.Tron), - expected = createStakingId(integrationId = StakingIntegrationID.Coin.Tron), + expected = createStakingId(integrationId = StakingIntegrationID.StakeKit.Coin.Tron), ), CreateModel( currencyId = createCurrencyId(blockchain = Blockchain.BSC), - expected = createStakingId(integrationId = StakingIntegrationID.Coin.BSC), + expected = createStakingId(integrationId = StakingIntegrationID.StakeKit.Coin.BSC), ), CreateModel( currencyId = createCurrencyId(blockchain = Blockchain.Cardano), - expected = createStakingId(integrationId = StakingIntegrationID.Coin.Cardano), + expected = createStakingId(integrationId = StakingIntegrationID.StakeKit.Coin.Cardano), ), CreateModel( - currencyId = CryptoCurrency.ID.fromValue(value = "coin⟨ETH⟩polygon-ecosystem-token⚓"), - expected = createStakingId(integrationId = StakingIntegrationID.EthereumToken.Polygon), + currencyId = createCurrencyId(blockchain = Blockchain.Ethereum), + expected = createStakingId(integrationId = StakingIntegrationID.P2P.EthereumPooled), + ), + CreateModel( + currencyId = CryptoCurrency.ID.fromValue( + value = "token⟨ETH⟩polygon-ecosystem-token⚓0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0", + ), + expected = createStakingId(integrationId = StakingIntegrationID.StakeKit.EthereumToken.Polygon), ), ) diff --git a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIntegrationIDTest.kt b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIntegrationIDTest.kt index 2ded0db928..ed60c5fd0c 100644 --- a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIntegrationIDTest.kt +++ b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIntegrationIDTest.kt @@ -28,23 +28,35 @@ class StakingIntegrationIDTest { } @Test - fun `all blockchains are unique`() { + fun `all Coin blockchains are unique`() { // Act - val actual = StakingIntegrationID.entries.distinctBy(StakingIntegrationID::blockchain) + val actual = StakingIntegrationID.StakeKit.Coin.entries + .distinctBy(StakingIntegrationID.StakeKit.Coin::blockchain) // Assert - val expected = StakingIntegrationID.entries.size + val expected = StakingIntegrationID.StakeKit.Coin.entries.size + Truth.assertThat(actual).hasSize(expected) + } + + @Test + fun `all P2P blockchains are unique`() { + // Act + val actual = StakingIntegrationID.P2P.entries + .distinctBy(StakingIntegrationID.P2P::blockchain) + + // Assert + val expected = StakingIntegrationID.P2P.entries.size Truth.assertThat(actual).hasSize(expected) } @Test fun `all sub blockchains are unique`() { // Act - val actual = StakingIntegrationID.EthereumToken.entries - .distinctBy(StakingIntegrationID.EthereumToken::subBlockchain) + val actual = StakingIntegrationID.StakeKit.EthereumToken.entries + .distinctBy(StakingIntegrationID.StakeKit.EthereumToken::subBlockchain) // Assert - val expected = StakingIntegrationID.EthereumToken.entries.size + val expected = StakingIntegrationID.StakeKit.EthereumToken.entries.size Truth.assertThat(actual).hasSize(expected) } @@ -65,8 +77,8 @@ class StakingIntegrationIDTest { @Test fun `all EthereumToken IDs are ethereum based blockchains`() { // Act - val actual = StakingIntegrationID.EthereumToken.entries - .map(StakingIntegrationID.EthereumToken::subBlockchain) + val actual = StakingIntegrationID.StakeKit.EthereumToken.entries + .map(StakingIntegrationID.StakeKit.EthereumToken::subBlockchain) .all { it.isEvm() } // Assert @@ -76,8 +88,8 @@ class StakingIntegrationIDTest { @Test fun `all Coin IDs do not need staking approval`() { // Act - val actual = StakingIntegrationID.Coin.entries - .map(StakingIntegrationID.Coin::approval) + val actual = StakingIntegrationID.StakeKit.Coin.entries + .map(StakingIntegrationID.StakeKit.Coin::approval) .none { it is StakingApproval.Needed } // Assert @@ -87,8 +99,8 @@ class StakingIntegrationIDTest { @Test fun `all EthereumToken IDs need staking approval`() { // Act - val actual = StakingIntegrationID.EthereumToken.entries - .map(StakingIntegrationID.EthereumToken::approval) + val actual = StakingIntegrationID.StakeKit.EthereumToken.entries + .map(StakingIntegrationID.StakeKit.EthereumToken::approval) .all { it is StakingApproval.Needed } // Assert @@ -116,31 +128,35 @@ class StakingIntegrationIDTest { ), CreateModel( currencyId = createCurrencyId(blockchain = Blockchain.TON), - expected = StakingIntegrationID.Coin.Ton, + expected = StakingIntegrationID.StakeKit.Coin.Ton, ), CreateModel( currencyId = createCurrencyId(blockchain = Blockchain.Solana), - expected = StakingIntegrationID.Coin.Solana, + expected = StakingIntegrationID.StakeKit.Coin.Solana, ), CreateModel( currencyId = createCurrencyId(blockchain = Blockchain.Cosmos), - expected = StakingIntegrationID.Coin.Cosmos, + expected = StakingIntegrationID.StakeKit.Coin.Cosmos, ), CreateModel( currencyId = createCurrencyId(blockchain = Blockchain.Tron), - expected = StakingIntegrationID.Coin.Tron, + expected = StakingIntegrationID.StakeKit.Coin.Tron, ), CreateModel( currencyId = createCurrencyId(blockchain = Blockchain.BSC), - expected = StakingIntegrationID.Coin.BSC, + expected = StakingIntegrationID.StakeKit.Coin.BSC, ), CreateModel( currencyId = createCurrencyId(blockchain = Blockchain.Cardano), - expected = StakingIntegrationID.Coin.Cardano, + expected = StakingIntegrationID.StakeKit.Coin.Cardano, + ), + CreateModel( + currencyId = createCurrencyId(blockchain = Blockchain.Ethereum), + expected = StakingIntegrationID.P2P.EthereumPooled, ), CreateModel( currencyId = CryptoCurrency.ID.fromValue(value = "token⟨ETH⟩polygon-ecosystem-token⚓1234567890"), - expected = StakingIntegrationID.EthereumToken.Polygon, + expected = StakingIntegrationID.StakeKit.EthereumToken.Polygon, ), CreateModel( currencyId = CryptoCurrency.ID.fromValue(value = "token⟨SOLANA⟩solana⚓1234567890"), diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactoryTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactoryTest.kt index 5888b68d94..22c3bb6eff 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactoryTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactoryTest.kt @@ -422,7 +422,7 @@ class CryptoCurrencyStatusFactoryTest { // Arrange val yieldBalance = YieldBalance.Data( stakingId = StakingID( - integrationId = StakingIntegrationID.Coin.Cardano.value, + integrationId = StakingIntegrationID.StakeKit.Coin.Cardano.value, address = networkAddress.defaultAddress.value, ), source = StatusSource.ACTUAL, @@ -435,7 +435,7 @@ class CryptoCurrencyStatusFactoryTest { every { this@mockk.token.coinGeckoId } returns "unknown" }, ), - integrationId = StakingIntegrationID.Coin.Cardano.value, + integrationId = StakingIntegrationID.StakeKit.Coin.Cardano.value, ), ) @@ -519,7 +519,7 @@ class CryptoCurrencyStatusFactoryTest { // Arrange val yieldBalance = YieldBalance.Data( stakingId = StakingID( - integrationId = StakingIntegrationID.Coin.Cardano.value, + integrationId = StakingIntegrationID.StakeKit.Coin.Cardano.value, address = networkAddress.defaultAddress.value, ), source = StatusSource.ACTUAL, @@ -532,7 +532,7 @@ class CryptoCurrencyStatusFactoryTest { every { this@mockk.token.coinGeckoId } returns "unknown" }, ), - integrationId = StakingIntegrationID.Coin.Cardano.value, + integrationId = StakingIntegrationID.StakeKit.Coin.Cardano.value, ), ) @@ -608,7 +608,7 @@ class CryptoCurrencyStatusFactoryTest { // Arrange val yieldBalance = YieldBalance.Data( stakingId = StakingID( - integrationId = StakingIntegrationID.Coin.Cardano.value, + integrationId = StakingIntegrationID.StakeKit.Coin.Cardano.value, address = networkAddress.defaultAddress.value, ), source = StatusSource.ACTUAL, @@ -621,7 +621,7 @@ class CryptoCurrencyStatusFactoryTest { every { this@mockk.token.coinGeckoId } returns "unknown" }, ), - integrationId = StakingIntegrationID.Coin.Cardano.value, + integrationId = StakingIntegrationID.StakeKit.Coin.Cardano.value, ), ) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt index 62fa4b32c3..f2eb68dbdd 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt @@ -385,7 +385,9 @@ internal class WalletBalanceFetcherTest { val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() val stakingId = Either.Left( - StakingIdFactory.Error.UnableToGetAddress(integrationId = StakingIntegrationID.EthereumToken.Polygon), + StakingIdFactory.Error.UnableToGetAddress( + integrationId = StakingIntegrationID.StakeKit.EthereumToken.Polygon, + ), ) every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver @@ -425,7 +427,9 @@ internal class WalletBalanceFetcherTest { val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() val ethereumStakingId = Either.Left( - StakingIdFactory.Error.UnableToGetAddress(integrationId = StakingIntegrationID.EthereumToken.Polygon), + StakingIdFactory.Error.UnableToGetAddress( + integrationId = StakingIntegrationID.StakeKit.EthereumToken.Polygon, + ), ) val stellarStakingId = Either.Left(StakingIdFactory.Error.UnsupportedCurrency)