Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-22 12:41:07 +02:00
parent fc0295bf33
commit d9320f8c2a
25 changed files with 369 additions and 89 deletions

View file

@ -1,6 +1,7 @@
package com.tangem.data.staking
import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.tangem.data.staking.converters.ethpool.*
@ -10,17 +11,20 @@ import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolBroadcastReque
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolDepositRequest
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolUnstakeRequest
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolWithdrawRequest
import com.tangem.datasource.local.token.P2PEthPoolVaultsStore
import com.tangem.domain.staking.model.ethpool.*
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
/**
* P2P staking repository implementation
*/
internal class DefaultP2PEthPoolRepository(
private val p2pApi: P2PEthPoolApi,
private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : P2PEthPoolRepository {
@ -30,6 +34,14 @@ internal class DefaultP2PEthPoolRepository(
private val broadcastResultConverter = P2PEthPoolBroadcastResultConverter
private val errorConverter = P2PEthPoolErrorConverter
override suspend fun fetchVaults(network: P2PEthPoolNetwork) {
val vaults = getVaults(network).getOrElse { error ->
Timber.e("Error fetching P2P vaults: $error")
emptyList()
}
p2pEthPoolVaultsStore.store(vaults)
}
override suspend fun getVaults(network: P2PEthPoolNetwork): Either<StakingError, List<P2PEthPoolVault>> = either {
withContext(dispatchers.io) {
val response = p2pApi.getVaults(network.value)

View file

@ -28,6 +28,7 @@ import com.tangem.datasource.api.stakekit.models.response.EnabledYieldsResponse
import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO
import com.tangem.datasource.api.stakekit.models.response.model.transaction.tron.TronStakeKitTransaction
import com.tangem.datasource.local.token.P2PEthPoolVaultsStore
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.datasource.local.token.converter.StakingNetworkTypeConverter
import com.tangem.datasource.local.token.converter.YieldTokenConverter
@ -42,6 +43,8 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.staking.model.StakingOption
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
@ -56,9 +59,9 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.lib.crypto.BlockchainUtils.isCardano
import com.tangem.lib.crypto.BlockchainUtils.isSolana
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
import timber.log.Timber
@ -67,6 +70,7 @@ import timber.log.Timber
internal class DefaultStakingRepository(
private val stakeKitApi: StakeKitApi,
private val stakingYieldsStore: StakingYieldsStore,
private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
private val stakingBalanceStoreV2: YieldsBalancesStore,
private val dispatchers: CoroutineDispatcherProvider,
private val walletManagersFacade: WalletManagersFacade,
@ -92,7 +96,7 @@ internal class DefaultStakingRepository(
private val networkTypeAdapter by lazy { moshi.adapter(NetworkTypeDTO::class.java) }
private val stakingActionStatusAdapter by lazy { moshi.adapter(StakingActionStatusDTO::class.java) }
override suspend fun fetchEnabledYields() {
override suspend fun fetchYields() {
withContext(dispatchers.io) {
val yieldsResponses = getAvailableStakeKitIntegrationsIds().map {
async { it.getYieldRequest() }
@ -190,12 +194,6 @@ internal class DefaultStakingRepository(
val yield = getYield(cryptoCurrencyId, symbol)
StakingEntryInfo(
rewardInfo = requireNotNull(
yield
.preferredValidators
.maxByOrNull { it.rewardInfo?.rate.orZero() }?.rewardInfo,
),
rewardSchedule = yield.metadata.rewardSchedule,
tokenSymbol = yield.token.symbol,
)
}
@ -222,32 +220,16 @@ internal class DefaultStakingRepository(
return@channelFlow
}
val isSupportedInMobileApp = StakingIntegrationID.create(currencyId = cryptoCurrency.id) != null
val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id)
getEnabledYields()
.distinctUntilChanged()
.onEach { yields ->
if (yields.isEmpty()) {
send(StakingAvailability.TemporaryUnavailable)
return@onEach
}
val prefetchedYield = findPrefetchedYield(
yields = yields,
currencyId = rawCurrencyId,
symbol = cryptoCurrency.symbol,
)
when {
prefetchedYield != null && isSupportedInMobileApp -> {
send(StakingAvailability.Available(prefetchedYield))
}
prefetchedYield == null && isSupportedInMobileApp -> {
send(StakingAvailability.TemporaryUnavailable)
}
else -> send(StakingAvailability.Unavailable)
}
}
.launchIn(this)
when (stakingIntegration) {
is StakingIntegrationID.P2P -> subscribeToP2PStakingAvailability()
is StakingIntegrationID.StakeKit -> subscribeToStakeKitStakingAvailability(
rawCurrencyId,
cryptoCurrency,
)
null -> send(StakingAvailability.Unavailable)
}
}
}
@ -268,27 +250,43 @@ internal class DefaultStakingRepository(
return StakingAvailability.Unavailable
}
val isSupportedInMobileApp = StakingIntegrationID.create(currencyId = cryptoCurrency.id) != null
val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id)
?: return StakingAvailability.Unavailable
val yields = getEnabledYieldsSync()
if (yields.isEmpty()) {
return StakingAvailability.TemporaryUnavailable
}
return when (stakingIntegration) {
is StakingIntegrationID.P2P -> {
val vaults = getP2PEthPoolVaultsSync()
if (vaults.isEmpty()) {
return StakingAvailability.TemporaryUnavailable
}
val prefetchedYield = findPrefetchedYield(
yields = yields,
currencyId = rawCurrencyId,
symbol = cryptoCurrency.symbol,
)
val vault = findP2PEthPoolVault(
vaults = vaults,
)
return when {
prefetchedYield != null && isSupportedInMobileApp -> {
StakingAvailability.Available(prefetchedYield)
if (vault != null) {
StakingAvailability.Available(StakingOption.P2P(vault))
} else {
StakingAvailability.TemporaryUnavailable
}
}
prefetchedYield == null && isSupportedInMobileApp -> {
StakingAvailability.TemporaryUnavailable
is StakingIntegrationID.StakeKit -> {
val yields = getEnabledYieldsSync()
if (yields.isEmpty()) {
return StakingAvailability.TemporaryUnavailable
}
val prefetchedYield = findPrefetchedYield(
yields = yields,
currencyId = rawCurrencyId,
symbol = cryptoCurrency.symbol,
)
when {
prefetchedYield != null -> StakingAvailability.Available(StakingOption.StakeKit(prefetchedYield))
else -> StakingAvailability.TemporaryUnavailable
}
}
else -> StakingAvailability.Unavailable
}
}
@ -510,6 +508,65 @@ internal class DefaultStakingRepository(
}
}
private suspend fun getP2PEthPoolVaultsSync(): List<P2PEthPoolVault> {
return p2pEthPoolVaultsStore.getSync()
}
private fun getP2PEthPoolVaults(): Flow<List<P2PEthPoolVault>> {
return p2pEthPoolVaultsStore.get()
}
private fun findP2PEthPoolVault(vaults: List<P2PEthPoolVault>): P2PEthPoolVault? {
return vaults.firstOrNull { vault -> !vault.isPrivate }
}
private fun ProducerScope<StakingAvailability>.subscribeToP2PStakingAvailability() {
getP2PEthPoolVaults()
.distinctUntilChanged()
.onEach { vaults ->
if (vaults.isEmpty()) {
send(StakingAvailability.TemporaryUnavailable)
return@onEach
}
val vault = findP2PEthPoolVault(vaults = vaults)
if (vault != null) {
send(StakingAvailability.Available(StakingOption.P2P(vault)))
} else {
send(StakingAvailability.TemporaryUnavailable)
}
}
.launchIn(this)
}
private fun ProducerScope<StakingAvailability>.subscribeToStakeKitStakingAvailability(
rawCurrencyId: CryptoCurrency.RawID,
cryptoCurrency: CryptoCurrency,
) {
getEnabledYields()
.distinctUntilChanged()
.onEach { yields ->
if (yields.isEmpty()) {
send(StakingAvailability.TemporaryUnavailable)
return@onEach
}
val prefetchedYield = findPrefetchedYield(
yields = yields,
currencyId = rawCurrencyId,
symbol = cryptoCurrency.symbol,
)
if (prefetchedYield != null) {
send(StakingAvailability.Available(StakingOption.StakeKit(prefetchedYield)))
} else {
send(StakingAvailability.TemporaryUnavailable)
}
}
.launchIn(this)
}
private fun getTronResource(network: Network): TronResource? {
val blockchain = Blockchain.fromNetworkId(network.backendId)

View file

@ -17,6 +17,7 @@ import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.P2PEthPoolVaultsStore
import com.tangem.datasource.local.token.StakingActionsStore
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
@ -44,6 +45,7 @@ internal object StakingDataModule {
fun provideStakingRepository(
stakeKitApi: StakeKitApi,
stakingYieldsStore: StakingYieldsStore,
p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
yieldsBalancesStore: YieldsBalancesStore,
dispatchers: CoroutineDispatcherProvider,
walletManagersFacade: WalletManagersFacade,
@ -54,6 +56,7 @@ internal object StakingDataModule {
return DefaultStakingRepository(
stakeKitApi = stakeKitApi,
stakingYieldsStore = stakingYieldsStore,
p2pEthPoolVaultsStore = p2pEthPoolVaultsStore,
stakingBalanceStoreV2 = yieldsBalancesStore,
dispatchers = dispatchers,
walletManagersFacade = walletManagersFacade,
@ -67,10 +70,12 @@ internal object StakingDataModule {
@Singleton
fun provideP2PEthPoolRepository(
p2pApi: P2PEthPoolApi,
p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
dispatchers: CoroutineDispatcherProvider,
): P2PEthPoolRepository {
return DefaultP2PEthPoolRepository(
p2pApi = p2pApi,
p2pEthPoolVaultsStore = p2pEthPoolVaultsStore,
dispatchers = dispatchers,
)
}