Updated on 2026-08-14
This commit is contained in:
parent
fc0295bf33
commit
d9320f8c2a
25 changed files with 369 additions and 89 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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>)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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>>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ internal class OutdatedDataActionsFactory(
|
|||
} else {
|
||||
val stakingAction = ActionState.Stake(
|
||||
unavailabilityReason = ScenarioUnavailabilityReason.UsedOutdatedData,
|
||||
yield = null,
|
||||
option = null,
|
||||
)
|
||||
|
||||
stakingAction.disabled()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue