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.tap.di.domain
import com.tangem.domain.staking.*
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
import com.tangem.domain.staking.repositories.StakingActionRepository
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
@ -16,6 +17,7 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
@Suppress("TooManyFunctions")
internal object StakingDomainModule {
@Provides
@ -92,6 +94,20 @@ internal object StakingDomainModule {
)
}
@Provides
@Singleton
fun provideFetchStakingOptionsUseCase(
stakingRepository: StakingRepository,
p2pRepository: P2PEthPoolRepository,
stakingErrorResolver: StakingErrorResolver,
): FetchStakingOptionsUseCase {
return FetchStakingOptionsUseCase(
stakingRepository = stakingRepository,
p2pRepository = p2pRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideFetchStakingYieldBalanceUseCase(

View file

@ -32,7 +32,7 @@ import com.tangem.domain.quotes.multi.MultiQuoteUpdater
import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase
import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase
import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase
import com.tangem.domain.staking.FetchStakingTokensUseCase
import com.tangem.domain.staking.FetchStakingOptionsUseCase
import com.tangem.domain.wallets.usecase.AssociateWalletsWithApplicationIdUseCase
import com.tangem.domain.wallets.usecase.GetSavedWalletsCountUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
@ -62,7 +62,7 @@ internal class MainViewModel @Inject constructor(
private val incrementAppLaunchCounterUseCase: IncrementAppLaunchCounterUseCase,
private val blockchainSDKFactory: BlockchainSDKFactory,
private val dispatchers: CoroutineDispatcherProvider,
private val fetchStakingTokensUseCase: FetchStakingTokensUseCase,
private val fetchStakingOptionsUseCase: FetchStakingOptionsUseCase,
private val fetchUserCountryUseCase: FetchUserCountryUseCase,
@GlobalUiMessageSender private val messageSender: UiMessageSender,
private val keyboardValidator: KeyboardValidator,
@ -104,7 +104,7 @@ internal class MainViewModel @Inject constructor(
launch { fetchAppCurrenciesUseCase() }
launch { fetchStakingTokens() }
launch { fetchStakingOptions() }
launch { initPushNotifications() }
}
@ -183,10 +183,10 @@ internal class MainViewModel @Inject constructor(
}
}
private suspend fun fetchStakingTokens() {
fetchStakingTokensUseCase()
.onLeft { Timber.e(it.toString(), "Unable to fetch the staking tokens list") }
.onRight { Timber.d("Staking token list was fetched successfully") }
private suspend fun fetchStakingOptions() {
fetchStakingOptionsUseCase()
.onLeft { Timber.e(it.toString(), "Unable to fetch staking options") }
.onRight { Timber.d("Staking options were fetched successfully") }
}
private fun initializeOffRamp() {

View file

@ -8,10 +8,13 @@ import com.squareup.moshi.Moshi
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.token.DefaultP2PEthPoolVaultsStore
import com.tangem.datasource.local.token.DefaultStakingActionsStore
import com.tangem.datasource.local.token.DefaultStakingYieldsStore
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.model.ethpool.P2PEthPoolVault
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.listTypes
import com.tangem.datasource.utils.mapWithStringKeyTypes
@ -73,4 +76,24 @@ internal object StakingStoreModule {
fun provideStakingActionsStore(): StakingActionsStore {
return DefaultStakingActionsStore(dataStore = RuntimeDataStore())
}
@Provides
@Singleton
fun provideP2PEthPoolVaultsStore(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): P2PEthPoolVaultsStore {
return DefaultP2PEthPoolVaultsStore(
dataStore = DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = listTypes<P2PEthPoolVault>(),
defaultValue = emptyList(),
),
produceFile = { context.dataStoreFile(fileName = "p2p_eth_pool_vaults") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
),
)
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.datasource.local.token
import androidx.datastore.core.DataStore
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
internal class DefaultP2PEthPoolVaultsStore(
private val dataStore: DataStore<List<P2PEthPoolVault>>,
) : P2PEthPoolVaultsStore {
override fun get(): Flow<List<P2PEthPoolVault>> {
return dataStore.data
}
override suspend fun getSync(): List<P2PEthPoolVault> {
return dataStore.data.firstOrNull().orEmpty()
}
override suspend fun store(vaults: List<P2PEthPoolVault>) {
dataStore.updateData { vaults }
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.datasource.local.token
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
import kotlinx.coroutines.flow.Flow
/**
* Store for P2P Ethereum pooled staking vaults
* (similar to StakingYieldsStore for StakeKit yields)
*
* Vault is ETH-specific concept for pooled staking.
* For other blockchains, P2P may use different structures.
*/
interface P2PEthPoolVaultsStore {
/**
* Get all stored vaults as Flow
*/
fun get(): Flow<List<P2PEthPoolVault>>
/**
* Get all stored vaults synchronously
*/
suspend fun getSync(): List<P2PEthPoolVault>
/**
* Store vaults from P2P API
*/
suspend fun store(vaults: List<P2PEthPoolVault>)
}

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

View file

@ -1,10 +1,8 @@
package com.tangem.domain.staking.model
import com.tangem.domain.staking.model.stakekit.Yield
sealed class StakingAvailability {
data class Available(val yield: Yield) : StakingAvailability()
data class Available(val option: StakingOption) : StakingAvailability()
data object Unavailable : StakingAvailability()

View file

@ -1,9 +1,5 @@
package com.tangem.domain.staking.model
import com.tangem.domain.staking.model.stakekit.Yield
data class StakingEntryInfo(
val rewardInfo: Yield.RewardInfo,
val rewardSchedule: Yield.Metadata.RewardSchedule,
val tokenSymbol: String,
)

View file

@ -0,0 +1,62 @@
package com.tangem.domain.staking.model
import com.tangem.domain.models.serialization.SerializedBigDecimal
import com.tangem.domain.models.staking.NetworkType
import com.tangem.domain.models.staking.YieldToken
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
import com.tangem.domain.staking.model.stakekit.Yield
/**
* Represents a staking option from any provider
* Unified abstraction over StakeKit and P2P staking integrations
*/
sealed interface StakingOption {
/** Unique identifier for the staking option */
val integrationId: String
/** Annual Percentage Yield */
val apy: SerializedBigDecimal
/** Token being staked */
val token: YieldToken // TODO p2p
/** Whether this staking option is available */
val isAvailable: Boolean
/**
* StakeKit staking option
* Wraps StakeKit Yield with all validator and metadata information
*/
data class StakeKit(val yield: Yield) : StakingOption {
override val integrationId: String = yield.id
override val apy: SerializedBigDecimal = yield.apy
override val token: YieldToken = yield.token
override val isAvailable: Boolean = yield.isAvailable
}
/**
* P2P pooled staking option
* Wraps P2P ETH Pool vault information
*/
data class P2P(val vault: P2PEthPoolVault) : StakingOption {
override val integrationId: String =
"p2p-ethereum-pooled:${vault.vaultAddress}"
override val apy: SerializedBigDecimal = vault.apy
override val token: YieldToken = createEthToken()
override val isAvailable: Boolean = !vault.isPrivate
private fun createEthToken(): YieldToken { // TODO
return YieldToken(
name = "Ethereum",
network = NetworkType.ETHEREUM,
symbol = "ETH",
decimals = 18,
address = null, // Native token
coinGeckoId = "ethereum",
logoURI = null,
isPoints = false,
)
}
}
}

View file

@ -1,11 +1,14 @@
package com.tangem.domain.staking.model.ethpool
import com.tangem.domain.models.serialization.SerializedBigDecimal
import kotlinx.serialization.Serializable
/**
* P2P.org pooled staking vault information
* Simplified version for vault list display
* Analogue of stakekit yield
*/
@Serializable
data class P2PEthPoolVault(
val vaultAddress: String,
val displayName: String,

View file

@ -0,0 +1,35 @@
package com.tangem.domain.staking
import arrow.core.Either
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
/**
* Use case for fetching all staking options from all providers
* Fetches both StakeKit yields and P2P vaults
*/
class FetchStakingOptionsUseCase(
private val stakingRepository: StakingRepository,
private val p2pRepository: P2PEthPoolRepository,
private val stakingErrorResolver: StakingErrorResolver,
) {
suspend operator fun invoke(): Either<StakingError, Unit> {
return either {
catch(
block = {
coroutineScope {
launch { stakingRepository.fetchYields() }
launch { p2pRepository.fetchVaults() }
}
},
catch = { stakingErrorResolver.resolve(it) },
)
}
}
}

View file

@ -17,7 +17,7 @@ class FetchStakingTokensUseCase(
suspend operator fun invoke(): Either<StakingError, Unit> {
return either {
catch(
block = { stakingRepository.fetchEnabledYields() },
block = { stakingRepository.fetchYields() },
catch = { stakingErrorResolver.resolve(it) },
)
}

View file

@ -3,6 +3,7 @@ package com.tangem.domain.staking
import arrow.core.Either
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.StakingOption
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
@ -18,13 +19,23 @@ class GetStakingEntryInfoUseCase(
suspend operator fun invoke(
cryptoCurrencyId: CryptoCurrency.ID,
symbol: String,
stakingOption: StakingOption,
): Either<StakingError, StakingEntryInfo> {
return Either
.catch {
stakingRepository.getEntryInfo(
cryptoCurrencyId = cryptoCurrencyId,
symbol = symbol,
)
when (stakingOption) {
is StakingOption.StakeKit -> {
stakingRepository.getEntryInfo(
cryptoCurrencyId = cryptoCurrencyId,
symbol = symbol,
)
}
is StakingOption.P2P -> {
StakingEntryInfo(
tokenSymbol = "ETH",
)
}
}
}
.mapLeft { stakingErrorResolver.resolve(it) }
}

View file

@ -4,11 +4,15 @@ import arrow.core.Either
import com.tangem.domain.staking.model.ethpool.*
import com.tangem.domain.staking.model.stakekit.StakingError
/**
* P2P staking repository interface
*/
interface P2PEthPoolRepository {
/**
* Fetch and store available staking vaults
*
* @param network P2P network (MAINNET or TESTNET)
*/
suspend fun fetchVaults(network: P2PEthPoolNetwork = P2PEthPoolNetwork.MAINNET)
/**
* Get list of available staking vaults
*

View file

@ -17,10 +17,11 @@ import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import kotlinx.coroutines.flow.Flow
// TODO p2p make 3 repos: stakekit, p2p, common staking
@Suppress("TooManyFunctions")
interface StakingRepository {
suspend fun fetchEnabledYields()
suspend fun fetchYields()
fun getEnabledYields(): Flow<List<Yield>>

View file

@ -172,12 +172,12 @@ internal open class BaseActionsFactory(
return if (stakingAvailability is StakingAvailability.Available) {
ActionState.Stake(
unavailabilityReason = ScenarioUnavailabilityReason.None,
yield = stakingAvailability.yield,
option = stakingAvailability.option,
)
} else {
ActionState.Stake(
unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(currency.name),
yield = null,
option = null,
)
}
}

View file

@ -96,7 +96,7 @@ internal class OutdatedDataActionsFactory(
} else {
val stakingAction = ActionState.Stake(
unavailabilityReason = ScenarioUnavailabilityReason.UsedOutdatedData,
yield = null,
option = null,
)
stakingAction.disabled()

View file

@ -65,7 +65,7 @@ internal class UnreachableActionsFactory(
showBadge = false,
),
ActionState.Sell(unavailabilityReason = ScenarioUnavailabilityReason.Unreachable),
ActionState.Stake(unavailabilityReason = ScenarioUnavailabilityReason.Unreachable, yield = null),
ActionState.Stake(unavailabilityReason = ScenarioUnavailabilityReason.Unreachable, option = null),
).disabled()
// endregion

View file

@ -2,7 +2,7 @@ package com.tangem.domain.tokens.model
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingOption
data class TokenActionsState(
val walletId: UserWalletId,
@ -24,7 +24,7 @@ data class TokenActionsState(
data class Stake(
override val unavailabilityReason: ScenarioUnavailabilityReason,
val yield: Yield?,
val option: StakingOption?,
) : ActionState()
data class Swap(

View file

@ -165,15 +165,15 @@ internal class TokenActionsHandler @AssistedInject constructor(
}
private fun onStakeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
val yield = cryptoCurrencyData.actions.firstOrNull { it is TokenActionsState.ActionState.Stake }
val option = cryptoCurrencyData.actions.firstOrNull { it is TokenActionsState.ActionState.Stake }
?.let { it as TokenActionsState.ActionState.Stake }
?.yield ?: return
?.option ?: return
router.push(
AppRoute.Staking(
userWalletId = cryptoCurrencyData.userWallet.walletId,
cryptoCurrency = cryptoCurrencyData.status.currency,
yieldId = yield.id,
yieldId = option.integrationId,
),
)
}

View file

@ -440,11 +440,12 @@ internal class TokenDetailsModel @Inject constructor(
)
.map { it.getOrElse { StakingAvailability.Unavailable } }
.distinctUntilChanged()
.onEach {
val stakingEntryInfo = if (it is StakingAvailability.Available) {
.onEach { availability ->
val stakingEntryInfo = if (availability is StakingAvailability.Available) {
getStakingEntryInfoUseCase(
cryptoCurrencyId = cryptoCurrency.id,
symbol = cryptoCurrency.symbol,
stakingOption = availability.option,
).getOrNull()
} else {
null
@ -454,7 +455,7 @@ internal class TokenDetailsModel @Inject constructor(
stateFactory.getStakingInfoState(
state = state,
stakingEntryInfo = stakingEntryInfo,
stakingAvailability = it,
stakingAvailability = availability,
cryptoCurrencyStatus = cryptoCurrencyStatus,
)
}

View file

@ -43,7 +43,7 @@ import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.promo.models.StoryContentIds
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingOption
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
@ -106,7 +106,7 @@ interface WalletCurrencyActionsClickIntents {
event: AnalyticsEvent? = null,
)
fun onStakeClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?)
fun onStakeClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, option: StakingOption?)
fun onCopyAddressLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus): TextReference?
@ -498,7 +498,11 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
}
}
override fun onStakeClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?) {
override fun onStakeClick(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
option: StakingOption?,
) {
stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId))
modelScope.launch {
@ -508,7 +512,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
AppRoute.Staking(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
yieldId = yield?.id ?: return@launch,
yieldId = option?.integrationId ?: return@launch,
),
)
}

View file

@ -67,7 +67,7 @@ internal class MultiWalletCurrencyActionsConverter(
is TokenActionsState.ActionState.Stake -> {
title = resourceReference(R.string.common_stake)
icon = R.drawable.ic_staking_24
action = { clickIntents.onStakeClick(userWalletId, cryptoCurrencyStatus, actionsState.yield) }
action = { clickIntents.onStakeClick(userWalletId, cryptoCurrencyStatus, actionsState.option) }
}
is TokenActionsState.ActionState.Sell -> {
title = resourceReference(R.string.common_sell)