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 92e80ff6db..9995a3753b 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 @@ -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( diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 8a3d691e60..d70fc8ae08 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -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() { diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt index c946177dc7..695c1c8d45 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt @@ -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(), + defaultValue = emptyList(), + ), + produceFile = { context.dataStoreFile(fileName = "p2p_eth_pool_vaults") }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ), + ) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultP2PEthPoolVaultsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultP2PEthPoolVaultsStore.kt new file mode 100644 index 0000000000..f48e2457ad --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultP2PEthPoolVaultsStore.kt @@ -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>, +) : P2PEthPoolVaultsStore { + + override fun get(): Flow> { + return dataStore.data + } + + override suspend fun getSync(): List { + return dataStore.data.firstOrNull().orEmpty() + } + + override suspend fun store(vaults: List) { + dataStore.updateData { vaults } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/P2PEthPoolVaultsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/P2PEthPoolVaultsStore.kt new file mode 100644 index 0000000000..a99eba1730 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/P2PEthPoolVaultsStore.kt @@ -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> + + /** + * Get all stored vaults synchronously + */ + suspend fun getSync(): List + + /** + * Store vaults from P2P API + */ + suspend fun store(vaults: List) +} \ No newline at end of file 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 4c0870aec5..4af834f017 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 @@ -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> = either { withContext(dispatchers.io) { val response = p2pApi.getVaults(network.value) 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 7ee32bed6f..689dd885f3 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 @@ -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 { + return p2pEthPoolVaultsStore.getSync() + } + + private fun getP2PEthPoolVaults(): Flow> { + return p2pEthPoolVaultsStore.get() + } + + private fun findP2PEthPoolVault(vaults: List): P2PEthPoolVault? { + return vaults.firstOrNull { vault -> !vault.isPrivate } + } + + private fun ProducerScope.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.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) 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 cfab0afe66..8e82d81a8f 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 @@ -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, ) } diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingAvailability.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingAvailability.kt index 189d86172e..b8afc62152 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingAvailability.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingAvailability.kt @@ -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() diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingEntryInfo.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingEntryInfo.kt index 004608a9f3..1a0c954937 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingEntryInfo.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingEntryInfo.kt @@ -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, ) \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingOption.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingOption.kt new file mode 100644 index 0000000000..1a4563d0b5 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingOption.kt @@ -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, + ) + } + } +} \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PEthPoolVault.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PEthPoolVault.kt index c10b932460..a316620bec 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PEthPoolVault.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PEthPoolVault.kt @@ -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, diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingOptionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingOptionsUseCase.kt new file mode 100644 index 0000000000..135f589770 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingOptionsUseCase.kt @@ -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 { + return either { + catch( + block = { + coroutineScope { + launch { stakingRepository.fetchYields() } + launch { p2pRepository.fetchVaults() } + } + }, + catch = { stakingErrorResolver.resolve(it) }, + ) + } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt index 168a65fc0f..5aef2d9cf1 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt @@ -17,7 +17,7 @@ class FetchStakingTokensUseCase( suspend operator fun invoke(): Either { return either { catch( - block = { stakingRepository.fetchEnabledYields() }, + block = { stakingRepository.fetchYields() }, catch = { stakingErrorResolver.resolve(it) }, ) } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt index de21864225..b8e2633bd7 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt @@ -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 { 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) } } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/P2PEthPoolRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/P2PEthPoolRepository.kt index 469d2cb74f..4c26844a7c 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/P2PEthPoolRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/P2PEthPoolRepository.kt @@ -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 * diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 7e360eb780..ab8b969f7b 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -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> diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt index 2809346d58..d18e2e36f4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt @@ -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, ) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt index 2e0d1cbf54..f6ff91d2fd 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt @@ -96,7 +96,7 @@ internal class OutdatedDataActionsFactory( } else { val stakingAction = ActionState.Stake( unavailabilityReason = ScenarioUnavailabilityReason.UsedOutdatedData, - yield = null, + option = null, ) stakingAction.disabled() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt index e2c0e5ff7a..4850068923 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt @@ -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 diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt index 306a8cba00..1260157432 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt @@ -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( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt index a0d60beb71..c8f42a8843 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt @@ -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, ), ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 8502b00c26..f7fc2be325 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -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, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt index bebc4bd474..09c16514ad 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt @@ -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, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt index 72e55cfb23..c6ef0d9981 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -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)