Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-18 10:31:03 +02:00
parent 9e0b8b3234
commit b449775ad8
112 changed files with 979 additions and 740 deletions

View file

@ -100,12 +100,12 @@ internal object StakingDomainModule {
@Singleton
fun provideFetchStakingOptionsUseCase(
stakeKitRepository: StakeKitRepository,
p2pRepository: P2PEthPoolRepository,
p2pEthPoolRepository: P2PEthPoolRepository,
stakingErrorResolver: StakingErrorResolver,
): FetchStakingOptionsUseCase {
return FetchStakingOptionsUseCase(
stakeKitRepository = stakeKitRepository,
p2pRepository = p2pRepository,
p2pEthPoolRepository = p2pEthPoolRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@ -240,8 +240,9 @@ internal object StakingDomainModule {
@Singleton
fun provideStakingApyFlowUseCase(
stakeKitRepository: StakeKitRepository,
p2pEthPoolRepository: P2PEthPoolRepository,
stakingFeatureToggles: StakingFeatureToggles,
): StakingApyFlowUseCase {
return StakingApyFlowUseCase(stakeKitRepository, stakingFeatureToggles)
return StakingApyFlowUseCase(stakeKitRepository, p2pEthPoolRepository, stakingFeatureToggles)
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.tap.network.auth
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
import com.tangem.lib.auth.P2PEthPoolAuthProvider
internal class DefaultP2PEthPoolAuthProvider(
@ -12,6 +12,6 @@ internal class DefaultP2PEthPoolAuthProvider(
val keys = environmentConfigStorage.getConfigSync().p2pApiKey
?: error("No P2P api keys provided")
return if (P2PStakingConfig.USE_TESTNET) keys.hoodi else keys.mainnet
return if (P2PEthPoolStakingConfig.USE_TESTNET) keys.hoodi else keys.mainnet
}
}

View file

@ -311,7 +311,7 @@ internal class ChildFactory @Inject constructor(
params = StakingComponent.Params(
userWalletId = route.userWalletId,
cryptoCurrency = route.cryptoCurrency,
yieldId = route.yieldId,
integrationId = route.integrationId,
),
componentFactory = stakingComponentFactory,
)

View file

@ -19,7 +19,7 @@ dependencies {
implementation(projects.domain.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.staking.models)
implementation(projects.domain.staking)
implementation(projects.domain.markets.models)
implementation(projects.domain.onramp.models)
implementation(projects.domain.appCurrency.models)

View file

@ -7,6 +7,7 @@ import android.os.Bundle
import com.tangem.common.routing.bundle.RouteBundleParams
import com.tangem.common.routing.bundle.bundle
import com.tangem.common.routing.entity.InitScreenLaunchMode
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.feedback.models.WalletMetaInfo
@ -208,8 +209,8 @@ sealed class AppRoute(val path: String) : Route {
data class Staking(
val userWalletId: UserWalletId,
val cryptoCurrency: CryptoCurrency,
val yieldId: String,
) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrency.id.value}/$yieldId")
val integrationId: StakingIntegrationID,
) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrency.id.value}/${integrationId.value}")
@Serializable
data class PushNotification(

View file

@ -8,7 +8,7 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
import java.math.BigDecimal
/**
* Factory for creating mock P2P ETH Pool account responses for testing
* Factory for creating mock P2PEthPool account responses for testing
*/
object MockP2PEthPoolAccountResponseFactory {

View file

@ -22,8 +22,10 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.currency.yieldSupplyKey
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.isStakingSupported
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.common.RewardInfo
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.StringsSigns.DASH_SIGN
@ -44,7 +46,7 @@ import java.math.BigDecimal
class TokenItemStateConverter(
private val appCurrency: AppCurrency,
private val yieldModuleApyMap: Map<String, BigDecimal> = emptyMap(),
private val stakingApyMap: Map<String, List<Yield.Validator>> = emptyMap(),
private val stakingApyMap: Map<String, List<StakingTarget>> = emptyMap(),
private val yieldSupplyPromoBannerKey: String? = null,
private val iconStateProvider: (CryptoCurrencyStatus) -> CurrencyIconState = {
CryptoCurrencyToIconStateConverter().convert(it)
@ -177,7 +179,7 @@ class TokenItemStateConverter(
private fun createTitleState(
currencyStatus: CryptoCurrencyStatus,
yieldModuleApyMap: Map<String, BigDecimal>,
stakingApyMap: Map<String, List<Yield.Validator>>,
stakingApyMap: Map<String, List<StakingTarget>>,
onApyLabelClick: ((CryptoCurrencyStatus, ApySource, String) -> Unit)?,
): TokenItemState.TitleState {
return when (val value = currencyStatus.value) {
@ -217,7 +219,7 @@ class TokenItemStateConverter(
private fun resolveEarnApy(
cryptoCurrencyStatus: CryptoCurrencyStatus,
yieldModuleApyMap: Map<String, BigDecimal>,
stakingApyMap: Map<String, List<Yield.Validator>>,
stakingApyMap: Map<String, List<StakingTarget>>,
): EarnApyInfo? {
val token = cryptoCurrencyStatus.currency as? CryptoCurrency.Token
if (token != null && yieldModuleApyMap.isNotEmpty()) {
@ -247,9 +249,9 @@ class TokenItemStateConverter(
stakingApyMap = stakingApyMap,
)
val rewardTypeRes = when (stakingInfo.rewardType) {
Yield.RewardType.APR -> R.string.staking_apr_earn_badge
Yield.RewardType.UNKNOWN,
Yield.RewardType.APY,
RewardType.APR -> R.string.staking_apr_earn_badge
RewardType.UNKNOWN,
RewardType.APY,
null,
-> R.string.yield_module_earn_badge
}
@ -272,49 +274,35 @@ class TokenItemStateConverter(
private fun findStakingRate(
currencyStatus: CryptoCurrencyStatus,
stakingApyMap: Map<String, List<Yield.Validator>>,
stakingApyMap: Map<String, List<StakingTarget>>,
): StakingLocalInfo {
val stakingKey = currencyStatus.currency.stakingKey()
val validators = stakingApyMap[stakingKey]
val targets = stakingApyMap[stakingKey]
?: return StakingLocalInfo(rate = null, isActive = false, rewardType = null)
val stakingBalance = currencyStatus.value.stakingBalance as? StakingBalance.Data
val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit
val rateInfo: Pair<BigDecimal, Yield.RewardType?>? = if (stakeKitBalance != null) {
val rewardInfo: RewardInfo? = if (stakeKitBalance != null) {
// StakeKit-specific: try to find rate from validator address
val validatorsByAddress = validators.associateBy { it.address }
val targetsByAddress = targets.associateBy { it.address }
stakeKitBalance.balance.items
.mapNotNull { it.validatorAddress }
.firstNotNullOfOrNull { address ->
val validator = validatorsByAddress[address]
validator?.rewardInfo?.rate?.let { rate ->
rate to validator.rewardInfo?.type
}
}
?: validators
.filter { it.preferred }
.mapNotNull { validator ->
validator.rewardInfo?.rate?.let { rate -> rate to validator.rewardInfo?.type }
}
.maxByOrNull { it.first }
.firstNotNullOfOrNull { address -> targetsByAddress[address]?.rewardInfo }
?: targets
.filter { it.isPreferred }
.mapNotNull { it.rewardInfo }
.maxByOrNull { it.rate }
} else {
// P2P or no balance: use preferred validators
// TODO p2p
validators
.filter { it.preferred }
.mapNotNull { validator ->
validator.rewardInfo?.rate?.let { rate ->
rate to validator.rewardInfo?.type
}
}
.maxByOrNull { it.first }
targets
.mapNotNull { it.rewardInfo }
.maxByOrNull { it.rate }
}
return StakingLocalInfo(
rate = rateInfo?.first,
rate = rewardInfo?.rate,
isActive = stakingBalance != null,
rewardType = rateInfo?.second,
rewardType = rewardInfo?.type,
)
}
@ -470,7 +458,7 @@ class TokenItemStateConverter(
private data class StakingLocalInfo(
val rate: BigDecimal?,
val isActive: Boolean,
val rewardType: Yield.RewardType?,
val rewardType: RewardType?,
)
private data class EarnApyInfo(

View file

@ -1,7 +1,7 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
import com.tangem.lib.auth.P2PEthPoolAuthProvider
import com.tangem.utils.ProviderSuspend
@ -23,7 +23,7 @@ internal class P2PEthPool(
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK
else -> if (P2PStakingConfig.USE_TESTNET) ApiEnvironment.DEV else ApiEnvironment.PROD
else -> if (P2PEthPoolStakingConfig.USE_TESTNET) ApiEnvironment.DEV else ApiEnvironment.PROD
}
}

View file

@ -29,7 +29,7 @@ data class P2PEthPoolBroadcastResponse(
)
/**
* Transaction status from P2P API
* Transaction status from P2PEthPool API
*/
@JsonClass(generateAdapter = false)
enum class P2PEthPoolTxStatusDTO {

View file

@ -4,9 +4,9 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Error response structure for P2P.org API
* Error response structure for P2P.org eth pooled API
*
* All P2P API endpoints return errors in this format
* All P2PEthPool API endpoints return errors in this format
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolErrorResponse(

View file

@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass
/**
* Unified response wrapper for all P2P.org API responses
*
* All P2P API endpoints return responses in this format:
* All P2PEthPool API endpoints return responses in this format:
* ```json
* {
* "error": null | { code, message, name, errors },

View file

@ -15,7 +15,7 @@ data class P2PEthPoolVaultsResponse(
)
/**
* Network identifier in P2P API
* Network identifier in P2PEthPool API
*/
@JsonClass(generateAdapter = false)
enum class P2PEthPoolNetworkDTO {

View file

@ -80,7 +80,7 @@ internal object StakingStoreModule {
@Provides
@Singleton
fun provideP2PBalancesPersistenceStore(
fun provideP2PEthPoolBalancesPersistenceStore(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
@ -91,7 +91,7 @@ internal object StakingStoreModule {
types = mapWithStringKeyTypes(valueTypes = setTypes<P2PEthPoolAccountResponse>()),
defaultValue = emptyMap(),
),
produceFile = { context.dataStoreFile(fileName = "p2p_balances") },
produceFile = { context.dataStoreFile(fileName = "p2p_eth_pool_balances") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
)
}

View file

@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow
* (similar to StakingYieldsStore for StakeKit yields)
*
* Vault is ETH-specific concept for pooled staking.
* For other blockchains, P2P may use different structures.
* For other blockchains, P2PEthPool may use different structures.
*/
interface P2PEthPoolVaultsStore {
@ -23,7 +23,7 @@ interface P2PEthPoolVaultsStore {
suspend fun getSync(): List<P2PEthPoolVault>
/**
* Store vaults from P2P API
* Store vaults from P2PEthPool API
*/
suspend fun store(vaults: List<P2PEthPoolVault>)
}

View file

@ -12,7 +12,7 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_
import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_API_KEY
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_API_KEY
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.P2PEthPoolAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider
@ -300,7 +300,7 @@ internal class ProdApiConfigsManagerTest {
}
private fun createP2PModel(): TestModel {
val (environment, baseUrl) = if (P2PStakingConfig.USE_TESTNET) {
val (environment, baseUrl) = if (P2PEthPoolStakingConfig.USE_TESTNET) {
ApiEnvironment.DEV to "https://api-test.p2p.org/"
} else {
ApiEnvironment.PROD to "https://api.p2p.org/"

View file

@ -25,10 +25,10 @@ import kotlinx.coroutines.withContext
import timber.log.Timber
/**
* P2P staking repository implementation
* P2PEthPool staking repository implementation
*/
internal class DefaultP2PEthPoolRepository(
private val p2pApi: P2PEthPoolApi,
private val p2pEthPoolApi: P2PEthPoolApi,
private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : P2PEthPoolRepository {
@ -41,7 +41,7 @@ internal class DefaultP2PEthPoolRepository(
override suspend fun fetchVaults(network: P2PEthPoolNetwork) {
val vaults = getVaults(network).getOrElse { error ->
Timber.e("Error fetching P2P vaults: $error")
Timber.e("Error fetching P2PEthPool vaults: $error")
emptyList()
}
p2pEthPoolVaultsStore.store(vaults)
@ -49,7 +49,7 @@ internal class DefaultP2PEthPoolRepository(
override suspend fun getVaults(network: P2PEthPoolNetwork): Either<StakingError, List<P2PEthPoolVault>> = either {
withContext(dispatchers.io) {
val response = p2pApi.getVaults(network.value)
val response = p2pEthPoolApi.getVaults(network.value)
when (response) {
is ApiResponse.Success -> {
val data = response.data
@ -76,7 +76,7 @@ internal class DefaultP2PEthPoolRepository(
vaultAddress = vaultAddress,
amount = amount.toDoubleOrNull() ?: raise(StakingError.InvalidAmount("Invalid amount format: $amount")),
)
val response = p2pApi.createDepositTransaction(network.value, requestBody)
val response = p2pEthPoolApi.createDepositTransaction(network.value, requestBody)
when (response) {
is ApiResponse.Success -> {
val data = response.data
@ -101,7 +101,7 @@ internal class DefaultP2PEthPoolRepository(
stakerPublicKey = stakerPublicKey,
stakeTransactionHash = stakeTransactionHash,
)
val response = p2pApi.createUnstakeTransaction(network.value, requestBody)
val response = p2pEthPoolApi.createUnstakeTransaction(network.value, requestBody)
when (response) {
is ApiResponse.Success -> {
val data = response.data
@ -133,7 +133,7 @@ internal class DefaultP2PEthPoolRepository(
): Either<StakingError, P2PEthPoolUnsignedTx> = either {
withContext(dispatchers.io) {
val requestBody = P2PEthPoolWithdrawRequest(stakerAddress = stakerAddress)
val response = p2pApi.createWithdrawTransaction(network.value, requestBody)
val response = p2pEthPoolApi.createWithdrawTransaction(network.value, requestBody)
when (response) {
is ApiResponse.Success -> {
val data = response.data
@ -154,7 +154,7 @@ internal class DefaultP2PEthPoolRepository(
): Either<StakingError, P2PEthPoolBroadcastResult> = either {
withContext(dispatchers.io) {
val requestBody = P2PEthPoolBroadcastRequest(signedTransaction = signedTransaction)
val response = p2pApi.broadcastTransaction(network.value, requestBody)
val response = p2pEthPoolApi.broadcastTransaction(network.value, requestBody)
when (response) {
is ApiResponse.Success -> {
val data = response.data
@ -175,7 +175,7 @@ internal class DefaultP2PEthPoolRepository(
vaultAddress: String,
): Either<StakingError, P2PEthPoolAccount> = either {
withContext(dispatchers.io) {
val response = p2pApi.getAccountInfo(network.value, delegatorAddress, vaultAddress)
val response = p2pEthPoolApi.getAccountInfo(network.value, delegatorAddress, vaultAddress)
when (response) {
is ApiResponse.Success -> {
val data = response.data
@ -197,7 +197,7 @@ internal class DefaultP2PEthPoolRepository(
period: Int?,
): Either<StakingError, List<P2PEthPoolReward>> = either {
withContext(dispatchers.io) {
val response = p2pApi.getRewards(
val response = p2pEthPoolApi.getRewards(
network = network.value,
delegatorAddress = delegatorAddress,
vaultAddress = vaultAddress,
@ -217,6 +217,10 @@ internal class DefaultP2PEthPoolRepository(
}
}
override fun getVaultsFlow(): Flow<List<P2PEthPoolVault>> {
return p2pEthPoolVaultsStore.get()
}
override fun getStakingAvailability(): Flow<StakingAvailability> {
return getVaultsFlow()
.distinctUntilChanged()
@ -224,7 +228,7 @@ internal class DefaultP2PEthPoolRepository(
if (vaults.isEmpty()) {
return@map StakingAvailability.TemporaryUnavailable
} else {
StakingAvailability.Available(StakingOption.P2P(vaults))
StakingAvailability.Available(StakingOption.P2PEthPool(vaults))
}
}
}
@ -234,15 +238,11 @@ internal class DefaultP2PEthPoolRepository(
return if (vaults.isEmpty()) {
StakingAvailability.TemporaryUnavailable
} else {
StakingAvailability.Available(StakingOption.P2P(vaults))
StakingAvailability.Available(StakingOption.P2PEthPool(vaults))
}
}
private suspend fun getVaultsSync(): List<P2PEthPoolVault> {
return p2pEthPoolVaultsStore.getSync()
}
private fun getVaultsFlow(): Flow<List<P2PEthPoolVault>> {
return p2pEthPoolVaultsStore.get()
}
}

View file

@ -364,6 +364,7 @@ internal class DefaultStakeKitRepository(
}
override fun getStakingAvailability(
integrationId: StakingIntegrationID.StakeKit,
rawCurrencyId: CryptoCurrency.RawID,
symbol: String,
): Flow<StakingAvailability> {
@ -381,7 +382,7 @@ internal class DefaultStakeKitRepository(
)
if (prefetchedYield != null) {
StakingAvailability.Available(StakingOption.StakeKit(prefetchedYield))
StakingAvailability.Available(StakingOption.StakeKit(integrationId, prefetchedYield))
} else {
StakingAvailability.TemporaryUnavailable
}
@ -389,6 +390,7 @@ internal class DefaultStakeKitRepository(
}
override suspend fun getStakingAvailabilitySync(
integrationId: StakingIntegrationID.StakeKit,
rawCurrencyId: CryptoCurrency.RawID,
symbol: String,
): StakingAvailability {
@ -404,7 +406,7 @@ internal class DefaultStakeKitRepository(
)
return if (prefetchedYield != null) {
StakingAvailability.Available(StakingOption.StakeKit(prefetchedYield))
StakingAvailability.Available(StakingOption.StakeKit(integrationId, prefetchedYield))
} else {
StakingAvailability.TemporaryUnavailable
}

View file

@ -29,12 +29,12 @@ internal class DefaultStakingErrorResolver(
is StakingError.DomainError -> {
analyticsEventHandler.send(StakingAnalyticsEvent.DomainError(error))
}
// P2P errors
// P2PEthPool errors
is StakingError.InvalidAmount,
is StakingError.DataError,
is StakingError.UnknownError,
-> {
// P2P errors - no specific analytics event yet
// P2PEthPool errors - no specific analytics event yet
}
}

View file

@ -61,8 +61,9 @@ internal class DefaultStakingRepository(
val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id)
val availabilityFlow = when (stakingIntegration) {
is StakingIntegrationID.P2P -> p2pEthPoolRepository.getStakingAvailability()
StakingIntegrationID.P2PEthPool -> p2pEthPoolRepository.getStakingAvailability()
is StakingIntegrationID.StakeKit -> stakeKitRepository.getStakingAvailability(
stakingIntegration,
rawCurrencyId,
cryptoCurrency.symbol,
)
@ -94,8 +95,9 @@ internal class DefaultStakingRepository(
?: return StakingAvailability.Unavailable
return when (stakingIntegration) {
is StakingIntegrationID.P2P -> p2pEthPoolRepository.getStakingAvailabilitySync()
StakingIntegrationID.P2PEthPool -> p2pEthPoolRepository.getStakingAvailabilitySync()
is StakingIntegrationID.StakeKit -> stakeKitRepository.getStakingAvailabilitySync(
stakingIntegration,
rawCurrencyId,
cryptoCurrency.symbol,
)

View file

@ -5,6 +5,8 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.MetadataDTO.RewardScheduleDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.ValidatorDTO.ValidatorStatusDTO
import com.tangem.datasource.local.token.converter.YieldTokenConverter
import com.tangem.domain.staking.model.common.RewardInfo
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.domain.staking.model.stakekit.AddressArgument
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.Yield.Metadata.RewardSchedule
@ -124,7 +126,7 @@ internal object YieldConverter : Converter<YieldDTO, Yield> {
)
}
private fun convertValidator(validatorDTO: YieldDTO.ValidatorDTO, rewardType: Yield.RewardType): Yield.Validator {
private fun convertValidator(validatorDTO: YieldDTO.ValidatorDTO, rewardType: RewardType): Yield.Validator {
val address = validatorDTO.address.asMandatory("address")
return Yield.Validator(
@ -142,7 +144,7 @@ internal object YieldConverter : Converter<YieldDTO, Yield> {
)
}
private fun createRewardInfo(validatorDTO: YieldDTO.ValidatorDTO, rewardType: Yield.RewardType): Yield.RewardInfo? {
private fun createRewardInfo(validatorDTO: YieldDTO.ValidatorDTO, rewardType: RewardType): RewardInfo? {
val aprOrApy = validatorDTO.apr
val commission = validatorDTO.commission
// gross = net / (1 - commission)
@ -162,17 +164,17 @@ internal object YieldConverter : Converter<YieldDTO, Yield> {
} else {
netApy
}
grossAprOrApy?.let { Yield.RewardInfo(rate = it, type = rewardType) }
grossAprOrApy?.let { RewardInfo(rate = it, type = rewardType) }
} catch (_: Exception) {
aprOrApy?.let { Yield.RewardInfo(rate = it, type = rewardType) }
aprOrApy?.let { RewardInfo(rate = it, type = rewardType) }
}
}
private fun convertRewardType(rewardTypeDTO: YieldDTO.RewardTypeDTO): Yield.RewardType {
private fun convertRewardType(rewardTypeDTO: YieldDTO.RewardTypeDTO): RewardType {
return when (rewardTypeDTO) {
YieldDTO.RewardTypeDTO.APY -> Yield.RewardType.APY
YieldDTO.RewardTypeDTO.APR -> Yield.RewardType.APR
else -> Yield.RewardType.UNKNOWN
YieldDTO.RewardTypeDTO.APY -> RewardType.APY
YieldDTO.RewardTypeDTO.APR -> RewardType.APR
else -> RewardType.UNKNOWN
}
}

View file

@ -9,7 +9,7 @@ import com.tangem.utils.converter.Converter
import org.joda.time.Instant
/**
* Converter from P2P Account Info Response to Domain model
* Converter from P2PEthPool Account Info Response to Domain model
*/
internal object P2PEthPoolAccountConverter : Converter<P2PEthPoolAccountResponse, P2PEthPoolAccount> {

View file

@ -8,7 +8,7 @@ import com.tangem.utils.converter.Converter
import java.math.BigDecimal
/**
* Converter from P2P Broadcast Transaction Response to Domain model
* Converter from P2PEthPool Broadcast Transaction Response to Domain model
*/
internal object P2PEthPoolBroadcastResultConverter : Converter<P2PEthPoolBroadcastResponse, P2PEthPoolBroadcastResult> {

View file

@ -6,7 +6,7 @@ import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.utils.converter.Converter
/**
* Converter from P2P Error Response to Domain StakingError
* Converter from P2PEthPool Error Response to Domain StakingError
*/
@Suppress("MagicNumber")
internal object P2PEthPoolErrorConverter : Converter<P2PEthPoolErrorResponse, StakingError> {

View file

@ -5,7 +5,7 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolReward
import com.tangem.utils.converter.Converter
/**
* Converter from P2P Reward Entry DTO to Domain model
* Converter from P2PEthPool Reward Entry DTO to Domain model
*/
internal object P2PEthPoolRewardConverter : Converter<P2PEthPoolRewardDTO, P2PEthPoolReward> {

View file

@ -9,16 +9,16 @@ import com.tangem.domain.models.staking.*
import com.tangem.domain.staking.model.StakingIntegrationID
import kotlinx.datetime.Instant
/** Converts P2P ETH Pool API response to [StakingBalance.Data.P2P] */
internal object P2PStakingBalanceConverter {
/** Converts P2PEthPool API response to [StakingBalance.Data.P2PEthPool] */
internal object P2PEthPoolStakingBalanceConverter {
fun convert(response: P2PEthPoolAccountResponse, source: StatusSource): StakingBalance.Data.P2P {
fun convert(response: P2PEthPoolAccountResponse, source: StatusSource): StakingBalance.Data.P2PEthPool {
val stakingId = StakingID(
integrationId = StakingIntegrationID.P2P.EthereumPooled.value,
integrationId = StakingIntegrationID.P2PEthPool.value,
address = response.delegatorAddress,
)
val account = P2PStakingAccount(
val account = P2PEthPoolStakingAccount(
delegatorAddress = response.delegatorAddress,
vaultAddress = response.vaultAddress,
stake = convertStake(response.stake),
@ -27,29 +27,29 @@ internal object P2PStakingBalanceConverter {
exitQueue = convertExitQueue(response.exitQueue),
)
return StakingBalance.Data.P2P(
return StakingBalance.Data.P2PEthPool(
stakingId = stakingId,
source = source,
account = account,
)
}
private fun convertStake(dto: P2PEthPoolStakeDTO): P2PStake {
return P2PStake(
private fun convertStake(dto: P2PEthPoolStakeDTO): P2PEthPoolStake {
return P2PEthPoolStake(
assets = dto.assets,
totalEarnedAssets = dto.totalEarnedAssets,
)
}
private fun convertExitQueue(dto: P2PEthPoolExitQueueDTO): P2PExitQueue {
return P2PExitQueue(
private fun convertExitQueue(dto: P2PEthPoolExitQueueDTO): P2PEthPoolExitQueue {
return P2PEthPoolExitQueue(
total = dto.total.toBigDecimal(),
requests = dto.requests.map(::convertExitRequest),
)
}
private fun convertExitRequest(dto: P2PEthPoolExitRequestDTO): P2PExitRequest {
return P2PExitRequest(
private fun convertExitRequest(dto: P2PEthPoolExitRequestDTO): P2PEthPoolExitRequest {
return P2PEthPoolExitRequest(
ticket = dto.ticket,
totalAssets = dto.totalAssets.toBigDecimal(),
timestamp = Instant.fromEpochSeconds(dto.timestamp),

View file

@ -6,7 +6,7 @@ import com.tangem.utils.converter.Converter
import java.math.BigDecimal
/**
* Converter from P2P Unsigned Transaction DTO to Domain model
* Converter from P2PEthPool Unsigned Transaction DTO to Domain model
*/
internal object P2PEthPoolUnsignedTxConverter : Converter<P2PEthPoolUnsignedTxDTO, P2PEthPoolUnsignedTx> {

View file

@ -5,7 +5,7 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
import com.tangem.utils.converter.Converter
/**
* Converter from P2P Vault DTO to Domain model
* Converter from P2PEthPool Vault DTO to Domain model
*/
internal object P2PEthPoolVaultConverter : Converter<P2PEthPoolVaultDTO, P2PEthPoolVault> {

View file

@ -1,92 +0,0 @@
package com.tangem.data.staking.converters.ethpool
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.staking.*
import com.tangem.domain.staking.model.ethpool.P2PEthPoolAccount
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
import java.math.BigDecimal
/**
* tmp solution before facade implementation
*/
internal object P2PYieldBalanceConverter {
private const val ETH_DECIMALS = 18
private const val ETH_SYMBOL = "ETH"
private const val ETH_NAME = "Ethereum"
private const val ETH_COINGECKO_ID = "ethereum"
fun convert(
account: P2PEthPoolAccount,
vault: P2PEthPoolVault,
address: String,
source: StatusSource,
): YieldBalance {
val integrationId = "p2p-ethereum-pooled"
val stakingId = StakingID(
integrationId = integrationId,
address = address,
)
val balanceItems = buildBalanceItems(account, vault)
return if (balanceItems.isEmpty()) {
YieldBalance.Empty(stakingId = stakingId, source = source)
} else {
YieldBalance.Data(
stakingId = stakingId,
source = source,
balance = YieldBalanceItem(
items = balanceItems,
integrationId = integrationId,
),
)
}
}
private fun buildBalanceItems(account: P2PEthPoolAccount, vault: P2PEthPoolVault): List<BalanceItem> = buildList {
if (account.stake.assets > BigDecimal.ZERO) {
add(
createBalanceItem(
groupId = "p2p-staked",
amount = account.stake.assets,
type = BalanceType.STAKED,
validatorAddress = vault.vaultAddress,
),
)
}
}
private fun createBalanceItem(
groupId: String,
amount: BigDecimal,
type: BalanceType,
validatorAddress: String,
): BalanceItem {
return BalanceItem(
groupId = groupId,
token = createEthToken(),
type = type,
amount = amount,
rawCurrencyId = ETH_COINGECKO_ID,
validatorAddress = validatorAddress,
date = null,
pendingActions = emptyList(),
pendingActionsConstraints = emptyList(),
isPending = false,
)
}
private fun createEthToken(): YieldToken {
return YieldToken(
name = ETH_NAME,
network = NetworkType.ETHEREUM,
symbol = ETH_SYMBOL,
decimals = ETH_DECIMALS,
address = null,
coinGeckoId = ETH_COINGECKO_ID,
logoURI = null,
isPoints = false,
)
}
}

View file

@ -1,9 +1,9 @@
package com.tangem.data.staking.di
import androidx.datastore.core.DataStore
import com.tangem.data.staking.store.DefaultP2PBalancesStore
import com.tangem.data.staking.store.DefaultP2PEthPoolBalancesStore
import com.tangem.data.staking.store.DefaultStakingBalancesStore
import com.tangem.data.staking.store.P2PBalancesStore
import com.tangem.data.staking.store.P2PEthPoolBalancesStore
import com.tangem.data.staking.store.StakingBalancesStore
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
@ -38,11 +38,11 @@ internal object StakingBalanceSupplierModule {
@Provides
@Singleton
fun provideP2PBalancesStore(
fun provideP2PEthPoolBalancesStore(
persistenceStore: DataStore<Map<String, Set<P2PEthPoolAccountResponse>>>,
dispatchers: CoroutineDispatcherProvider,
): P2PBalancesStore {
return DefaultP2PBalancesStore(
): P2PEthPoolBalancesStore {
return DefaultP2PEthPoolBalancesStore(
runtimeStore = RuntimeSharedStore(),
persistenceStore = persistenceStore,
dispatchers = dispatchers,

View file

@ -75,12 +75,12 @@ internal object StakingDataModule {
@Provides
@Singleton
fun provideP2PEthPoolRepository(
p2pApi: P2PEthPoolApi,
p2pEthPoolApi: P2PEthPoolApi,
p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
dispatchers: CoroutineDispatcherProvider,
): P2PEthPoolRepository {
return DefaultP2PEthPoolRepository(
p2pApi = p2pApi,
p2pEthPoolApi = p2pEthPoolApi,
p2pEthPoolVaultsStore = p2pEthPoolVaultsStore,
dispatchers = dispatchers,
)

View file

@ -5,7 +5,7 @@ import arrow.core.left
import arrow.core.right
import arrow.core.toOption
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.staking.store.P2PBalancesStore
import com.tangem.data.staking.store.P2PEthPoolBalancesStore
import com.tangem.data.staking.store.StakingBalancesStore
import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory
import com.tangem.datasource.api.common.response.ApiResponse
@ -24,7 +24,8 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runSuspendCatching
@ -38,16 +39,16 @@ import javax.inject.Inject
/**
* Default implementation of [MultiStakingBalanceFetcher]
*
* Supports both StakeKit and P2P staking providers.
* Supports both StakeKit and P2PEthPool staking providers.
*
* @property userWalletsStore user wallets store
* @property stakingYieldsStore staking yields store
* @property stakingBalancesStore staking balances store (StakeKit)
* @property p2pBalancesStore P2P balances store
* @property stakeKitApi stake kit API
* @property p2pApi P2P ETH Pool API
* @property p2pVaultsStore P2P vaults store
* @property dispatchers dispatchers
* @property userWalletsStore user wallets store
* @property stakingYieldsStore staking yields store
* @property stakingBalancesStore staking balances store (StakeKit)
* @property p2PEthPoolBalancesStore P2PEthPool balances store
* @property stakeKitApi stake kit API
* @property p2pEthPoolApi P2PEthPool API
* @property p2pEthPoolVaultsStore P2PEthPool vaults store
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
*/
@ -56,10 +57,10 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
private val userWalletsStore: UserWalletsStore,
private val stakingYieldsStore: StakingYieldsStore,
private val stakingBalancesStore: StakingBalancesStore,
private val p2pBalancesStore: P2PBalancesStore,
private val p2PEthPoolBalancesStore: P2PEthPoolBalancesStore,
private val stakeKitApi: StakeKitApi,
private val p2pApi: P2PEthPoolApi,
private val p2pVaultsStore: P2PEthPoolVaultsStore,
private val p2pEthPoolApi: P2PEthPoolApi,
private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : MultiStakingBalanceFetcher {
@ -75,7 +76,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
return it.left()
}
val (stakeKitIds, p2pIds) = stakingIds.partition { stakingId ->
val (stakeKitIds, p2pEthPoolIds) = stakingIds.partition { stakingId ->
val stakingIntegrationID = StakingIntegrationID.entries.find {
it.value == stakingId.integrationId
}
@ -86,7 +87,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
"""
Staking IDs to fetch:
- StakeKit: ${stakeKitIds.joinToString()}
- P2P: ${p2pIds.joinToString()}
- P2PEthPool: ${p2pEthPoolIds.joinToString()}
""".trimIndent(),
)
@ -96,8 +97,8 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
launch { fetchStakeKitBalances(params.userWalletId, stakeKitIds.toSet()) }
}
if (p2pIds.isNotEmpty()) {
launch { fetchP2PBalances(params.userWalletId, p2pIds.toSet()) }
if (p2pEthPoolIds.isNotEmpty()) {
launch { fetchP2PBalances(params.userWalletId, p2pEthPoolIds.toSet()) }
}
}
}
@ -110,8 +111,11 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
stakingIds = stakeKitIds.toSet(),
)
}
if (p2pIds.isNotEmpty()) {
p2pBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = p2pIds.toSet())
if (p2pEthPoolIds.isNotEmpty()) {
p2PEthPoolBalancesStore.storeError(
userWalletId = params.userWalletId,
stakingIds = p2pEthPoolIds.toSet(),
)
}
}
}
@ -128,12 +132,12 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
}
private suspend fun fetchP2PBalances(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
p2pBalancesStore.refresh(userWalletId = userWalletId, stakingIds = stakingIds)
p2PEthPoolBalancesStore.refresh(userWalletId = userWalletId, stakingIds = stakingIds)
val vaults = runSuspendCatching { p2pVaultsStore.getSync() }.getOrNull().orEmpty()
val vaults = runSuspendCatching { p2pEthPoolVaultsStore.getSync() }.getOrNull().orEmpty()
if (vaults.isEmpty()) {
Timber.w("No P2P vaults available for $userWalletId")
p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
Timber.w("No P2PEthPool vaults available for $userWalletId")
p2PEthPoolBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
return
}
@ -143,59 +147,17 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
private suspend fun fetchFromP2P(
userWalletId: UserWalletId,
stakingIds: Set<StakingID>,
vaults: List<com.tangem.domain.staking.model.ethpool.P2PEthPoolVault>,
vaults: List<P2PEthPoolVault>,
) {
safeApiCall(
call = {
val addresses = stakingIds.map { it.address }.toSet()
val responses = fetchP2PAccountResponses(vaults = vaults, addresses = addresses)
val responses = mutableSetOf<P2PEthPoolAccountResponse>()
for (vault in vaults) {
for (address in addresses) {
runSuspendCatching {
val response = p2pApi.getAccountInfo(
network = P2PStakingConfig.activeNetwork.value,
delegatorAddress = address,
vaultAddress = vault.vaultAddress,
)
when (response) {
is ApiResponse.Success -> {
val data = response.data
if (data.error != null) {
Timber.w(
"P2P API returned error for vault ${vault.vaultAddress}, " +
"address $address: ${data.error ?: "error"}",
)
} else {
val result = requireNotNull(data.result) {
"Result is null in successful response"
}
responses.add(result)
}
}
is ApiResponse.Error -> {
Timber.w(
response.cause,
"Failed to fetch P2P balance for vault ${vault.vaultAddress}, " +
"address $address",
)
}
}
}.onFailure { error ->
Timber.w(
error,
"Failed to fetch P2P balance for vault ${vault.vaultAddress}, address $address",
)
}
}
}
Timber.i("Successfully fetched ${responses.size} P2P balances for $userWalletId")
Timber.i("Successfully fetched ${responses.size} P2PEthPool balances for $userWalletId")
if (responses.isNotEmpty()) {
p2pBalancesStore.storeActual(userWalletId = userWalletId, values = responses)
p2PEthPoolBalancesStore.storeActual(userWalletId = userWalletId, values = responses)
val missingStakingIds = stakingIds.filter { stakingId ->
responses.none { response ->
@ -205,23 +167,76 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
if (missingStakingIds.isNotEmpty()) {
Timber.i("Missing responses for ${missingStakingIds.size} staking IDs: $missingStakingIds")
p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = missingStakingIds.toSet())
p2PEthPoolBalancesStore.storeError(
userWalletId = userWalletId,
stakingIds = missingStakingIds.toSet(),
)
}
} else {
Timber.i("No P2P responses received for $userWalletId")
p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
Timber.i("No P2PEthPool responses received for $userWalletId")
p2PEthPoolBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
}
},
onError = { throwable ->
Timber.e(throwable, "Unable to fetch P2P balances $userWalletId")
Timber.e(throwable, "Unable to fetch P2PEthPool balances $userWalletId")
p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
p2PEthPoolBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
throw throwable
},
)
}
private suspend fun fetchP2PAccountResponses(
vaults: List<P2PEthPoolVault>,
addresses: Set<String>,
): Set<P2PEthPoolAccountResponse> {
val responses = mutableSetOf<P2PEthPoolAccountResponse>()
for (vault in vaults) {
for (address in addresses) {
runSuspendCatching {
val response = p2pEthPoolApi.getAccountInfo(
network = P2PEthPoolStakingConfig.activeNetwork.value,
delegatorAddress = address,
vaultAddress = vault.vaultAddress,
)
when (response) {
is ApiResponse.Success -> {
val data = response.data
if (data.error != null) {
Timber.w(
"P2PEthPool API returned error for vault ${vault.vaultAddress}, " +
"address $address: ${data.error ?: "error"}",
)
} else {
val result = requireNotNull(data.result) {
"Result is null in successful response"
}
responses.add(result)
}
}
is ApiResponse.Error -> {
Timber.w(
response.cause,
"Failed to fetch P2PEthPool balance for vault ${vault.vaultAddress}, " +
"address $address",
)
}
}
}.onFailure { error ->
Timber.w(
error,
"Failed to fetch P2PEthPool balance for vault ${vault.vaultAddress}, address $address",
)
}
}
}
return responses
}
private inline fun checkIsSupportedByWalletOrElse(userWalletId: UserWalletId, ifNotSupported: (Throwable) -> Unit) {
val maybeUserWallet = userWalletsStore.getSyncOrNull(key = userWalletId).toOption()

View file

@ -2,7 +2,7 @@ package com.tangem.data.staking.multi
import arrow.core.Option
import arrow.core.some
import com.tangem.data.staking.store.P2PBalancesStore
import com.tangem.data.staking.store.P2PEthPoolBalancesStore
import com.tangem.data.staking.store.StakingBalancesStore
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.staking.multi.MultiStakingBalanceProducer
@ -19,11 +19,11 @@ import kotlinx.coroutines.flow.onEmpty
/**
* Default implementation of [MultiStakingBalanceProducer]
*
* Combines staking balances from both StakeKit and P2P providers.
* Combines staking balances from both StakeKit and P2PEthPool providers.
*
* @property params params
* @property stakingBalancesStore StakeKit staking balances store
* @property p2pBalancesStore P2P balances store
* @property p2PEthPoolBalancesStore P2PEthPool balances store
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.onEmpty
internal class DefaultMultiStakingBalanceProducer @AssistedInject constructor(
@Assisted val params: MultiStakingBalanceProducer.Params,
private val stakingBalancesStore: StakingBalancesStore,
private val p2pBalancesStore: P2PBalancesStore,
private val p2PEthPoolBalancesStore: P2PEthPoolBalancesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : MultiStakingBalanceProducer {
@ -39,10 +39,10 @@ internal class DefaultMultiStakingBalanceProducer @AssistedInject constructor(
override fun produce(): Flow<Set<StakingBalance>> {
val stakeKitFlow = stakingBalancesStore.get(userWalletId = params.userWalletId)
val p2pFlow = p2pBalancesStore.get(userWalletId = params.userWalletId)
val p2pEthPoolFlow = p2PEthPoolBalancesStore.get(userWalletId = params.userWalletId)
return combine(stakeKitFlow, p2pFlow) { stakeKitBalances, p2pBalances ->
stakeKitBalances + p2pBalances
return combine(stakeKitFlow, p2pEthPoolFlow) { stakeKitBalances, p2pEthPoolBalances ->
stakeKitBalances + p2pEthPoolBalances
}
.distinctUntilChanged()
.onEmpty { emit(value = hashSetOf()) }

View file

@ -1,7 +1,7 @@
package com.tangem.data.staking.store
import androidx.datastore.core.DataStore
import com.tangem.data.staking.converters.ethpool.P2PStakingBalanceConverter
import com.tangem.data.staking.converters.ethpool.P2PEthPoolStakingBalanceConverter
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.models.StatusSource
@ -20,22 +20,22 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
internal typealias WalletIdWithP2PStakingBalances = Map<UserWalletId, Set<StakingBalance>>
internal typealias WalletIdWithP2PResponses = Map<String, Set<P2PEthPoolAccountResponse>>
internal typealias WalletIdWithP2PEthPoolResponses = Map<String, Set<P2PEthPoolAccountResponse>>
/**
* Default implementation of [P2PBalancesStore]
* Default implementation of [P2PEthPoolBalancesStore]
*
* Stores P2P ETH Pool staking balances.
* Stores P2PEthPool staking balances.
*
* @property runtimeStore runtime store
* @property persistenceStore persistence store
* @param dispatchers coroutine dispatchers
*/
internal class DefaultP2PBalancesStore(
internal class DefaultP2PEthPoolBalancesStore(
private val runtimeStore: RuntimeSharedStore<WalletIdWithP2PStakingBalances>,
private val persistenceStore: DataStore<WalletIdWithP2PResponses>,
private val persistenceStore: DataStore<WalletIdWithP2PEthPoolResponses>,
dispatchers: CoroutineDispatcherProvider,
) : P2PBalancesStore {
) : P2PEthPoolBalancesStore {
private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io)
@ -47,7 +47,7 @@ internal class DefaultP2PBalancesStore(
value = cachedData.map { (stringWalletId, responses) ->
val key = UserWalletId(stringWalletId)
val value = responses.map { response ->
P2PStakingBalanceConverter.convert(
P2PEthPoolStakingBalanceConverter.convert(
response = response,
source = StatusSource.CACHE,
)
@ -108,7 +108,7 @@ internal class DefaultP2PBalancesStore(
private suspend fun storeInRuntime(userWalletId: UserWalletId, values: Set<P2PEthPoolAccountResponse>) {
val newBalances = values.map { response ->
P2PStakingBalanceConverter.convert(
P2PEthPoolStakingBalanceConverter.convert(
response = response,
source = StatusSource.ACTUAL,
)
@ -152,7 +152,7 @@ internal class DefaultP2PBalancesStore(
current.toMutableMap().apply {
this[userWalletId.stringValue] = this[userWalletId.stringValue].orEmpty()
.filterNot { response ->
StakingIntegrationID.P2P.EthereumPooled.value in integrationIds
StakingIntegrationID.P2PEthPool.value in integrationIds
}
.toSet()
}

View file

@ -7,9 +7,9 @@ import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
/**
* Store for P2P ETH Pool staking balances
* Store for P2PEthPool staking balances
*/
interface P2PBalancesStore {
interface P2PEthPoolBalancesStore {
fun get(userWalletId: UserWalletId): Flow<Set<StakingBalance>>

View file

@ -1,6 +1,6 @@
package com.tangem.data.staking
import com.tangem.data.staking.converters.ethpool.P2PStakingBalanceConverter
import com.tangem.data.staking.converters.ethpool.P2PEthPoolStakingBalanceConverter
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.local.token.converter.StakingBalanceConverter
@ -11,8 +11,10 @@ internal fun YieldBalanceWrapperDTO.toDomain(source: StatusSource = StatusSource
return StakingBalanceConverter(isCached = source == StatusSource.CACHE).convert(this)!!
}
internal fun P2PEthPoolAccountResponse.toDomain(source: StatusSource = StatusSource.CACHE): StakingBalance.Data.P2P {
return P2PStakingBalanceConverter.convert(
internal fun P2PEthPoolAccountResponse.toDomain(
source: StatusSource = StatusSource.CACHE,
): StakingBalance.Data.P2PEthPool {
return P2PEthPoolStakingBalanceConverter.convert(
response = this,
source = source,
)

View file

@ -4,7 +4,7 @@ import arrow.core.toOption
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
import com.tangem.common.test.data.staking.MockYieldDTOFactory
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
import com.tangem.data.staking.store.P2PBalancesStore
import com.tangem.data.staking.store.P2PEthPoolBalancesStore
import com.tangem.data.staking.store.StakingBalancesStore
import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory
import com.tangem.datasource.api.common.response.ApiResponse
@ -36,19 +36,19 @@ internal class DefaultMultiStakingBalanceFetcherTest {
private val userWalletsStore: UserWalletsStore = mockk()
private val stakingYieldsStore: StakingYieldsStore = mockk()
private val stakingBalancesStore: StakingBalancesStore = mockk(relaxUnitFun = true)
private val p2pBalancesStore: P2PBalancesStore = mockk(relaxUnitFun = true)
private val p2PEthPoolBalancesStore: P2PEthPoolBalancesStore = mockk(relaxUnitFun = true)
private val stakeKitApi: StakeKitApi = mockk()
private val p2pApi: P2PEthPoolApi = mockk()
private val p2pVaultsStore: P2PEthPoolVaultsStore = mockk()
private val p2pEthPoolApi: P2PEthPoolApi = mockk()
private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore = mockk()
private val fetcher = DefaultMultiStakingBalanceFetcher(
userWalletsStore = userWalletsStore,
stakingYieldsStore = stakingYieldsStore,
stakingBalancesStore = stakingBalancesStore,
p2pBalancesStore = p2pBalancesStore,
p2PEthPoolBalancesStore = p2PEthPoolBalancesStore,
stakeKitApi = stakeKitApi,
p2pApi = p2pApi,
p2pVaultsStore = p2pVaultsStore,
p2pEthPoolApi = p2pEthPoolApi,
p2pEthPoolVaultsStore = p2pEthPoolVaultsStore,
dispatchers = TestingCoroutineDispatcherProvider(),
)

View file

@ -3,7 +3,7 @@ package com.tangem.data.staking.multi
import com.google.common.truth.Truth
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
import com.tangem.common.test.data.staking.MockP2PEthPoolAccountResponseFactory
import com.tangem.data.staking.store.P2PBalancesStore
import com.tangem.data.staking.store.P2PEthPoolBalancesStore
import com.tangem.data.staking.store.StakingBalancesStore
import com.tangem.data.staking.toDomain
import com.tangem.domain.models.StatusSource
@ -28,13 +28,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
private val params = MultiStakingBalanceProducer.Params(userWalletId = UserWalletId("011"))
private val stakingBalancesStore = mockk<StakingBalancesStore>()
private val p2pBalancesStore = mockk<P2PBalancesStore>()
private val p2PEthPoolBalancesStore = mockk<P2PEthPoolBalancesStore>()
private val dispatchers = TestingCoroutineDispatcherProvider()
private val producer = DefaultMultiStakingBalanceProducer(
params = params,
stakingBalancesStore = stakingBalancesStore,
p2pBalancesStore = p2pBalancesStore,
p2PEthPoolBalancesStore = p2PEthPoolBalancesStore,
dispatchers = dispatchers,
)
@ -48,13 +48,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
val networksStatusesFlow = flowOf(balances)
every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
val actual = producer.produce()
// check after producer.produce()
verify { stakingBalancesStore.get(params.userWalletId) }
verify { p2pBalancesStore.get(params.userWalletId) }
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
val values = getEmittedValues(flow = actual)
@ -67,13 +67,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
val networksStatusesFlow = MutableSharedFlow<Set<StakingBalance>>(replay = 2)
every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
val actual = producer.produce()
// check after producer.produce()
verify { stakingBalancesStore.get(params.userWalletId) }
verify { p2pBalancesStore.get(params.userWalletId) }
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
// first emit
val balances = setOf(
@ -108,13 +108,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
val networksStatusesFlow = MutableSharedFlow<Set<StakingBalance>>(replay = 2)
every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
val actual = producer.produce()
// check after producer.produce()
verify { stakingBalancesStore.get(params.userWalletId) }
verify { p2pBalancesStore.get(params.userWalletId) }
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
// first emit
val wrappers = setOf(
@ -157,13 +157,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
.buffer(capacity = 5)
every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
val actual = producer.produceWithFallback()
// check after producer.produce()
verify { stakingBalancesStore.get(params.userWalletId) }
verify { p2pBalancesStore.get(params.userWalletId) }
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
val values1 = getEmittedValues(flow = actual)
@ -181,13 +181,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
@Test
fun `test that flow is empty`() = runTest {
every { stakingBalancesStore.get(params.userWalletId) } returns emptyFlow()
every { p2pBalancesStore.get(params.userWalletId) } returns emptyFlow()
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns emptyFlow()
val actual = producer.produce()
// check after producer.produce()
verify { stakingBalancesStore.get(params.userWalletId) }
verify { p2pBalancesStore.get(params.userWalletId) }
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
val values = getEmittedValues(flow = actual)
@ -198,53 +198,53 @@ internal class DefaultMultiStakingBalanceProducerTest {
@Test
fun `test that StakeKit and P2P balances are combined`() = runTest {
val stakeKitBalances = createStakeKitBalances()
val p2pBalances = createP2PBalances()
val p2pEthPoolBalances = createP2PEthPoolBalances()
every { stakingBalancesStore.get(params.userWalletId) } returns flowOf(stakeKitBalances)
every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(p2pBalances)
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns flowOf(p2pEthPoolBalances)
val actual = producer.produce()
// check after producer.produce()
verify { stakingBalancesStore.get(params.userWalletId) }
verify { p2pBalancesStore.get(params.userWalletId) }
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
val values = getEmittedValues(flow = actual)
Truth.assertThat(values.size).isEqualTo(1)
Truth.assertThat(values.first()).isEqualTo(stakeKitBalances + p2pBalances)
Truth.assertThat(values.first()).isEqualTo(stakeKitBalances + p2pEthPoolBalances)
}
@Test
fun `test that P2P balances are updated independently from StakeKit`() = runTest {
val stakeKitBalances = createStakeKitBalancesWithTonOnly()
val p2pFlow = MutableSharedFlow<Set<StakingBalance>>(replay = 2)
val p2pEthPoolFlow = MutableSharedFlow<Set<StakingBalance>>(replay = 2)
every { stakingBalancesStore.get(params.userWalletId) } returns flowOf(stakeKitBalances)
every { p2pBalancesStore.get(params.userWalletId) } returns p2pFlow
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns p2pEthPoolFlow
val actual = producer.produce()
// check after producer.produce()
verify { stakingBalancesStore.get(params.userWalletId) }
verify { p2pBalancesStore.get(params.userWalletId) }
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
// first emit - empty P2P
p2pFlow.emit(emptySet())
// first emit - empty P2PEthPool
p2pEthPoolFlow.emit(emptySet())
val values1 = getEmittedValues(flow = actual)
Truth.assertThat(values1.size).isEqualTo(1)
Truth.assertThat(values1.first()).isEqualTo(stakeKitBalances)
// second emit - with P2P balance
val p2pBalances = createP2PBalances()
p2pFlow.emit(p2pBalances)
// second emit - with P2PEthPool balance
val p2pEthPoolBalances = createP2PEthPoolBalances()
p2pEthPoolFlow.emit(p2pEthPoolBalances)
val values2 = getEmittedValues(flow = actual)
Truth.assertThat(values2.size).isEqualTo(2)
Truth.assertThat(values2.last()).isEqualTo(stakeKitBalances + p2pBalances)
Truth.assertThat(values2.last()).isEqualTo(stakeKitBalances + p2pEthPoolBalances)
}
private companion object {
@ -255,7 +255,7 @@ internal class DefaultMultiStakingBalanceProducerTest {
address = "0x1",
)
val p2pEthereumId = StakingID(
integrationId = StakingIntegrationID.P2P.EthereumPooled.value,
integrationId = StakingIntegrationID.P2PEthPool.value,
address = "0x5aa711F440Eb6d4361148bBD89d03464628ace84",
)
@ -272,7 +272,7 @@ internal class DefaultMultiStakingBalanceProducerTest {
)
}
fun createP2PBalances(): Set<StakingBalance> {
fun createP2PEthPoolBalances(): Set<StakingBalance> {
return setOf(
MockP2PEthPoolAccountResponseFactory.createWithBalance(stakingId = p2pEthereumId).toDomain(
source = StatusSource.ACTUAL,

View file

@ -4,31 +4,31 @@ import com.tangem.domain.models.serialization.SerializedBigDecimal
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
/** P2P.org staking account */
/** P2P.org eth pooled staking account */
@Serializable
data class P2PStakingAccount(
data class P2PEthPoolStakingAccount(
val delegatorAddress: String,
val vaultAddress: String,
val stake: P2PStake,
val stake: P2PEthPoolStake,
val availableToUnstake: SerializedBigDecimal,
val availableToWithdraw: SerializedBigDecimal,
val exitQueue: P2PExitQueue,
val exitQueue: P2PEthPoolExitQueue,
)
@Serializable
data class P2PStake(
data class P2PEthPoolStake(
val assets: SerializedBigDecimal,
val totalEarnedAssets: SerializedBigDecimal,
)
@Serializable
data class P2PExitQueue(
data class P2PEthPoolExitQueue(
val total: SerializedBigDecimal,
val requests: List<P2PExitRequest>,
val requests: List<P2PEthPoolExitRequest>,
)
@Serializable
data class P2PExitRequest(
data class P2PEthPoolExitRequest(
val ticket: String,
val totalAssets: SerializedBigDecimal,
val timestamp: Instant,

View file

@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
import java.math.BigDecimal
/**
* Staking balance facade covering StakeKit and P2P balances
* Staking balance facade covering StakeKit and P2PEthPool balances
*/
@Serializable
sealed interface StakingBalance {
@ -50,10 +50,10 @@ sealed interface StakingBalance {
}
@Serializable
data class P2P(
data class P2PEthPool(
override val stakingId: StakingID,
override val source: StatusSource,
val account: P2PStakingAccount,
val account: P2PEthPoolStakingAccount,
) : Data {
override val totalStaked: BigDecimal
@ -93,7 +93,7 @@ sealed interface StakingBalance {
fun copySealed(source: StatusSource): StakingBalance {
return when (this) {
is Data.StakeKit -> copy(source = source)
is Data.P2P -> copy(source = source)
is Data.P2PEthPool -> copy(source = source)
is Empty -> copy(source = source)
is Error -> this
}

View file

@ -12,4 +12,17 @@ data class YieldToken(
val coinGeckoId: String?,
val logoURI: String?,
val isPoints: Boolean?,
)
) {
companion object {
val ETH = YieldToken( // TODO p2p
name = "Ethereum",
network = NetworkType.ETHEREUM,
symbol = "ETH",
decimals = 18,
address = null,
coinGeckoId = "ethereum",
logoURI = null,
isPoints = false,
)
}
}

View file

@ -29,8 +29,6 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.features.staking.api)
implementation(tangemDeps.blockchain) {
exclude(module = "joda-time")
}

View file

@ -0,0 +1,66 @@
package com.tangem.domain.staking.model
import com.tangem.domain.staking.model.common.RewardInfo
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
import com.tangem.domain.staking.model.stakekit.Yield
/**
* Represents either a StakeKit Validator or a P2P ETH Pool Vault.
*/
sealed interface StakingTarget {
/** Unique identifier (validator address or vault address) */
val address: String
/** Display name */
val name: String
/** Reward info (rate and type) */
val rewardInfo: RewardInfo?
/** Whether this target is preferred/recommended */
val isPreferred: Boolean
/** Whether this target is active and available for staking */
val isActive: Boolean
/** Image URL for display (validator logo or vault icon) */
val image: String?
/** Whether this is a strategic partner (shows special badge in UI) */
val isStrategicPartner: Boolean
/**
* StakeKit Validator wrapper
*/
data class Validator(val delegate: Yield.Validator) : StakingTarget {
override val address: String = delegate.address
override val name: String = delegate.name
override val rewardInfo: RewardInfo? = delegate.rewardInfo
override val isPreferred: Boolean = delegate.preferred
override val isActive: Boolean = delegate.status == Yield.Validator.ValidatorStatus.ACTIVE
override val image: String? = delegate.image
override val isStrategicPartner: Boolean = delegate.isStrategicPartner
}
/**
* P2P ETH Pool Vault wrapper
*/
data class Vault(val vault: P2PEthPoolVault) : StakingTarget {
override val address: String = vault.vaultAddress
override val name: String = vault.displayName
override val rewardInfo = RewardInfo(
rate = vault.apy,
type = RewardType.APY,
)
override val isPreferred: Boolean = true
override val isActive: Boolean = true
override val image: String? = null
override val isStrategicPartner: Boolean = true
}
}
fun Yield.Validator.toStakingTarget(): StakingTarget = StakingTarget.Validator(this)
fun P2PEthPoolVault.toStakingTarget(): StakingTarget = StakingTarget.Vault(this)

View file

@ -0,0 +1,10 @@
package com.tangem.domain.staking.model.common
import com.tangem.domain.models.serialization.SerializedBigDecimal
import kotlinx.serialization.Serializable
@Serializable
data class RewardInfo(
val rate: SerializedBigDecimal,
val type: RewardType,
)

View file

@ -0,0 +1,10 @@
package com.tangem.domain.staking.model.common
import kotlinx.serialization.Serializable
@Serializable
enum class RewardType {
APY, // compound rate
APR, // simple rate
UNKNOWN,
}

View file

@ -4,7 +4,7 @@ import com.tangem.domain.models.serialization.SerializedBigDecimal
import org.joda.time.Instant
/**
* P2P.org account staking information
* Account staking information
* Contains detailed balance and exit queue information
*/
data class P2PEthPoolAccount(

View file

@ -22,7 +22,7 @@ data class P2PEthPoolAction(
)
/**
* Types of P2P staking actions
* Types of P2PEthPool staking actions
*/
@Serializable
enum class P2PEthPoolActionType {
@ -33,7 +33,7 @@ enum class P2PEthPoolActionType {
}
/**
* Status of P2P staking action
* Status of P2PEthPool staking action
*/
@Serializable
enum class P2PEthPoolActionStatus {
@ -46,7 +46,7 @@ enum class P2PEthPoolActionStatus {
}
/**
* Transaction details for P2P staking action
* Transaction details for P2PEthPool staking action
*/
data class P2PEthPoolStakingTransaction(
val id: String?,

View file

@ -4,7 +4,7 @@ import com.tangem.domain.models.serialization.SerializedBigDecimal
import org.joda.time.Instant
/**
* P2P.org staking balance information (similar to StakeKit's YieldBalanceItem)
* Staking balance information (similar to StakeKit's YieldBalanceItem)
* Contains staked amounts, rewards, and pending actions
*/
data class P2PEthPoolStakingBalance(
@ -15,7 +15,7 @@ data class P2PEthPoolStakingBalance(
)
/**
* Individual balance item for P2P staking
* Individual balance item for P2PEthPool staking
*/
data class P2PEthPoolBalanceItem(
val type: P2PEthPoolBalanceType,
@ -26,7 +26,7 @@ data class P2PEthPoolBalanceItem(
)
/**
* Types of balances in P2P staking
* Types of balances in P2PEthPool staking
*/
enum class P2PEthPoolBalanceType {
STAKED,

View file

@ -61,7 +61,7 @@ enum class P2PEthPoolNetwork(
}
/**
* Check if chain ID is supported for P2P staking
* Check if chain ID is supported for P2PEthPool staking
*
* @param chainId Ethereum chain ID
* @return true if supported, false otherwise

View file

@ -4,7 +4,7 @@ import com.tangem.domain.models.serialization.SerializedBigDecimal
import org.joda.time.DateTime
/**
* P2P.org rewards history entry
* Rewards history entry
* Historical reward information for account
*/
data class P2PEthPoolReward(

View file

@ -61,7 +61,7 @@ data class P2PEthPoolStaking(
}
/**
* Detailed vault information for P2P staking
* Detailed vault information for P2PEthPool staking
*/
data class P2PEthPoolVaultDetails(
val vaultAddress: String,

View file

@ -1,11 +1,11 @@
package com.tangem.domain.staking.model.ethpool
/**
* Configuration for P2P Ethereum staking network.
* Configuration for P2PEthPool Ethereum staking network.
*
* Change [USE_TESTNET] to switch between testnet and mainnet.
*/
object P2PStakingConfig {
object P2PEthPoolStakingConfig {
const val USE_TESTNET: Boolean = true

View file

@ -3,7 +3,7 @@ package com.tangem.domain.staking.model.ethpool
import com.tangem.domain.models.serialization.SerializedBigDecimal
/**
* P2P.org unsigned transaction ready for signing
* Unsigned transaction ready for signing
* Contains all necessary data for transaction signing
*/
data class P2PEthPoolUnsignedTx(

View file

@ -2,6 +2,8 @@ package com.tangem.domain.staking.model.stakekit
import com.tangem.domain.models.serialization.SerializedBigDecimal
import com.tangem.domain.models.staking.YieldToken
import com.tangem.domain.staking.model.common.RewardInfo
import com.tangem.domain.staking.model.common.RewardType
import kotlinx.serialization.Serializable
@Serializable
@ -138,18 +140,6 @@ data class Yield(
UNKNOWN,
}
}
enum class RewardType {
APY, // compound rate
APR, // simple rate
UNKNOWN,
}
@Serializable
data class RewardInfo(
val rate: SerializedBigDecimal,
val type: RewardType,
)
}
@Serializable

View file

@ -12,11 +12,11 @@ import kotlinx.coroutines.launch
/**
* Use case for fetching all staking options from all providers
* Fetches both StakeKit yields and P2P vaults
* Fetches both StakeKit yields and P2PEthPool vaults
*/
class FetchStakingOptionsUseCase(
private val stakeKitRepository: StakeKitRepository,
private val p2pRepository: P2PEthPoolRepository,
private val p2pEthPoolRepository: P2PEthPoolRepository,
private val stakingErrorResolver: StakingErrorResolver,
) {
suspend operator fun invoke(): Either<StakingError, Unit> {
@ -25,7 +25,7 @@ class FetchStakingOptionsUseCase(
block = {
coroutineScope {
launch { stakeKitRepository.fetchYields() }
launch { p2pRepository.fetchVaults() }
launch { p2pEthPoolRepository.fetchVaults() }
}
},
catch = { stakingErrorResolver.resolve(it) },

View file

@ -30,7 +30,7 @@ class GetStakingEntryInfoUseCase(
symbol = symbol,
)
}
is StakingOption.P2P -> {
is StakingOption.P2PEthPool -> {
StakingEntryInfo(
tokenSymbol = "ETH",
)

View file

@ -0,0 +1,62 @@
package com.tangem.domain.staking.model
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.staking.YieldToken
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
import com.tangem.domain.staking.model.stakekit.Yield
import java.math.BigDecimal
/**
* StakingIntegration implementation for P2PEthPool pooled staking.
* Converts P2PEthPoolVault data to the common StakingIntegration interface.
*/
class P2PEthPoolIntegration(
override val integrationId: StakingIntegrationID,
vaults: List<P2PEthPoolVault>,
) : StakingIntegration {
// Basic
override val token: YieldToken = YieldToken.ETH
override val tokens: List<YieldToken> = listOf(token)
// Targets (vaults)
override val targets: List<StakingTarget> = vaults.map { vault ->
vault.toStakingTarget()
}
override val preferredTargets: List<StakingTarget> = targets
override val areAllTargetsFull: Boolean = false
// Enter/Exit Args
override val isPartialAmountDisabled: Boolean = false
override val enterMinimumAmount: BigDecimal = DEFAULT_MINIMUM_STAKE
override val exitMinimumAmount: BigDecimal? = null
override val enterArgs: Yield.Args.Enter? = null
override val exitArgs: Yield.Args.Enter? = null
// Metadata
override val warmupPeriodDays: Int = 0
override val cooldownPeriodDays: Int = DEFAULT_COOLDOWN_DAYS
override val rewardSchedule: Yield.Metadata.RewardSchedule = Yield.Metadata.RewardSchedule.DAY
override val rewardClaiming: Yield.Metadata.RewardClaiming = Yield.Metadata.RewardClaiming.AUTO
override fun getCurrentToken(rawCurrencyId: CryptoCurrency.RawID?): YieldToken = token
companion object {
private const val DEFAULT_COOLDOWN_DAYS = 7
private val DEFAULT_MINIMUM_STAKE = BigDecimal("0.01")
}
}

View file

@ -0,0 +1,60 @@
package com.tangem.domain.staking.model
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.staking.YieldToken
import com.tangem.domain.staking.model.stakekit.Yield
import java.math.BigDecimal
/**
* StakingIntegration implementation for StakeKit.
* Delegates all calls to the underlying Yield object.
*/
class StakeKitIntegration(
override val integrationId: StakingIntegrationID,
private val yield: Yield,
) : StakingIntegration {
// Basic
override val token: YieldToken = yield.token
override val tokens: List<YieldToken> = yield.tokens
// Targets (validators)
override val targets: List<StakingTarget> = yield.validators.map { it.toStakingTarget() }
override val preferredTargets: List<StakingTarget> = yield.preferredValidators.map { it.toStakingTarget() }
override val areAllTargetsFull: Boolean = yield.allValidatorsFull
// Enter/Exit Args
override val isPartialAmountDisabled: Boolean = yield.args.enter.isPartialAmountDisabled
override val enterMinimumAmount: BigDecimal? =
yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum
override val exitMinimumAmount: BigDecimal? =
yield.args.exit?.args
?.get(Yield.Args.ArgType.AMOUNT)?.minimum
override val enterArgs: Yield.Args.Enter = yield.args.enter
override val exitArgs: Yield.Args.Enter? = yield.args.exit
// Metadata
override val warmupPeriodDays: Int = yield.metadata.warmupPeriod.days
override val cooldownPeriodDays: Int? = yield.metadata.cooldownPeriod?.days
override val rewardSchedule: Yield.Metadata.RewardSchedule = yield.metadata.rewardSchedule
override val rewardClaiming: Yield.Metadata.RewardClaiming = yield.metadata.rewardClaiming
// Basic
override fun getCurrentToken(rawCurrencyId: CryptoCurrency.RawID?): YieldToken =
tokens.firstOrNull { rawCurrencyId?.value == it.coinGeckoId } ?: token
}

View file

@ -0,0 +1,56 @@
package com.tangem.domain.staking.model
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.staking.YieldToken
import com.tangem.domain.staking.model.stakekit.Yield
import java.math.BigDecimal
/**
* Strategy interface for staking integrations.
* Abstracts over StakeKit and P2PEthPool staking providers.
*/
// TODO p2p get rid of stakekit-specific models in StakingIntegration and implementors
interface StakingIntegration {
// Basic
val integrationId: StakingIntegrationID
val token: YieldToken
val tokens: List<YieldToken>
// Targets (validators or vaults)
val targets: List<StakingTarget>
val preferredTargets: List<StakingTarget>
val areAllTargetsFull: Boolean
// Enter/Exit Args
val isPartialAmountDisabled: Boolean
val enterMinimumAmount: BigDecimal?
val exitMinimumAmount: BigDecimal?
val enterArgs: Yield.Args.Enter?
val exitArgs: Yield.Args.Enter?
// Metadata
val warmupPeriodDays: Int
val cooldownPeriodDays: Int?
val rewardSchedule: Yield.Metadata.RewardSchedule
val rewardClaiming: Yield.Metadata.RewardClaiming
// Basic
fun getCurrentToken(rawCurrencyId: CryptoCurrency.RawID?): YieldToken
}

View file

@ -5,7 +5,7 @@ import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toMigratedCoinId
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
/**
* Represents a staking integration identifier.
@ -97,16 +97,14 @@ sealed interface StakingIntegrationID {
}
/**
* Represents P2P staking integrations
* Represents P2PEthPool staking integration
*/
enum class P2P : StakingIntegrationID {
EthereumPooled {
override val value: String = "p2p-ethereum-pooled"
override val blockchain: Blockchain
get() = if (P2PStakingConfig.USE_TESTNET) Blockchain.EthereumTestnet else Blockchain.Ethereum
override val networkId: String
get() = P2PStakingConfig.activeNetwork.stakingNetworkId
},
object P2PEthPool : StakingIntegrationID {
override val value: String = "p2p-ethereum-pooled"
override val blockchain: Blockchain
get() = if (P2PEthPoolStakingConfig.USE_TESTNET) Blockchain.EthereumTestnet else Blockchain.Ethereum
override val networkId: String
get() = P2PEthPoolStakingConfig.activeNetwork.stakingNetworkId
}
// Polkadot {
@ -138,7 +136,7 @@ sealed interface StakingIntegrationID {
/** List of all native staking integration IDs */
val entries: List<StakingIntegrationID> by lazy {
StakeKit.Coin.entries + StakeKit.EthereumToken.entries + P2P.entries
StakeKit.Coin.entries + StakeKit.EthereumToken.entries + listOf(P2PEthPool)
}
/**
@ -152,9 +150,12 @@ sealed interface StakingIntegrationID {
val blockchain = Blockchain.fromId(id = currencyId.rawNetworkId)
return if (currencyId.contractAddress.isNullOrBlank()) {
// Order is not important — either P2P or Stakekit.Coin can be in any order
P2P.entries.firstOrNull { it.blockchain == blockchain }
?: StakeKit.Coin.entries.firstOrNull { it.blockchain == blockchain }
// Order is not important — either P2PEthPool or Stakekit.Coin can be in any order
if (P2PEthPool.blockchain == blockchain) {
P2PEthPool
} else {
StakeKit.Coin.entries.firstOrNull { it.blockchain == blockchain }
}
} else {
StakeKit.EthereumToken.entries.firstOrNull { token ->
token.blockchain == blockchain &&

View file

@ -1,19 +1,18 @@
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
* Unified abstraction over StakeKit and P2PEthPool staking integrations
*/
sealed interface StakingOption {
/** Unique identifier for the staking option */
val integrationId: String
val integrationId: StakingIntegrationID
/** Annual Percentage Yield */
val apy: SerializedBigDecimal
@ -28,34 +27,23 @@ sealed interface StakingOption {
* 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
data class StakeKit(
override val integrationId: StakingIntegrationID.StakeKit,
val yield: Yield,
) : StakingOption {
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
* P2PEthPool staking option
* Wraps P2PEthPool vault information
*/
data class P2P(val vaults: List<P2PEthPoolVault>) : StakingOption {
override val integrationId: String = "p2p-ethereum-pooled"
data class P2PEthPool(val vaults: List<P2PEthPoolVault>) : StakingOption {
override val integrationId: StakingIntegrationID = StakingIntegrationID.P2PEthPool
override val apy: SerializedBigDecimal = vaults.maxOf { it.apy }
override val token: YieldToken = createEthToken()
override val token: YieldToken = YieldToken.ETH
override val isAvailable: Boolean = vaults.isNotEmpty()
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

@ -8,7 +8,7 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolNetwork
import com.tangem.domain.staking.model.ethpool.P2PEthPoolReward
import com.tangem.domain.staking.model.ethpool.P2PEthPoolUnsignedTx
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
import com.tangem.domain.staking.model.stakekit.StakingError
import kotlinx.coroutines.flow.Flow
@ -17,24 +17,24 @@ interface P2PEthPoolRepository {
/**
* Fetch and store available staking vaults
*
* @param network P2P network (MAINNET or TESTNET)
* @param network P2PEthPool network (MAINNET or TESTNET)
*/
suspend fun fetchVaults(network: P2PEthPoolNetwork = P2PStakingConfig.activeNetwork)
suspend fun fetchVaults(network: P2PEthPoolNetwork = P2PEthPoolStakingConfig.activeNetwork)
/**
* Get list of available staking vaults
*
* @param network P2P network (MAINNET or TESTNET)
* @param network P2PEthPool network (MAINNET or TESTNET)
* @return Either error or list of vaults with APY, capacity, fees
*/
suspend fun getVaults(
network: P2PEthPoolNetwork = P2PStakingConfig.activeNetwork,
network: P2PEthPoolNetwork = P2PEthPoolStakingConfig.activeNetwork,
): Either<StakingError, List<P2PEthPoolVault>>
/**
* Create unsigned transaction for depositing ETH into a vault
*
* @param network P2P network (MAINNET or TESTNET)
* @param network P2PEthPool network (MAINNET or TESTNET)
* @param delegatorAddress User's wallet address
* @param vaultAddress Vault contract address
* @param amount Amount of ETH to deposit
@ -53,7 +53,7 @@ interface P2PEthPoolRepository {
* Unstaking adds funds to exit queue. After ~1-4 days, use [createWithdrawTransaction]
* to withdraw the funds.
*
* @param network P2P network (MAINNET or TESTNET)
* @param network P2PEthPool network (MAINNET or TESTNET)
* @param stakerPublicKey Staker's public key (note: API doc may have Bitcoin terminology)
* @param stakeTransactionHash Original stake transaction hash
* @return Either error or unsigned transaction
@ -69,7 +69,7 @@ interface P2PEthPoolRepository {
*
* Only works when funds are available (after exit queue wait period).
*
* @param network P2P network (MAINNET or TESTNET)
* @param network P2PEthPool network (MAINNET or TESTNET)
* @param stakerAddress User's wallet address
* @return Either error or unsigned transaction with withdrawal tickets
*/
@ -81,7 +81,7 @@ interface P2PEthPoolRepository {
/**
* Broadcast signed transaction to blockchain
*
* @param network P2P network (MAINNET or TESTNET)
* @param network P2PEthPool network (MAINNET or TESTNET)
* @param signedTransaction Signed transaction in hex format (with 0x prefix)
* @return Either error or broadcast result with transaction hash
*/
@ -95,7 +95,7 @@ interface P2PEthPoolRepository {
*
* Returns current stake, rewards, exit queue status, and available amounts
*
* @param network P2P network (MAINNET or TESTNET)
* @param network P2PEthPool network (MAINNET or TESTNET)
* @param delegatorAddress User's wallet address
* @param vaultAddress Vault contract address
* @return Either error or account info
@ -109,7 +109,7 @@ interface P2PEthPoolRepository {
/**
* Get rewards history for account and vault
*
* @param network P2P network (MAINNET or TESTNET)
* @param network P2PEthPool network (MAINNET or TESTNET)
* @param delegatorAddress User's wallet address
* @param vaultAddress Vault contract address
* @param period Optional period filter in days (30, 60, or 90)
@ -123,17 +123,27 @@ interface P2PEthPoolRepository {
): Either<StakingError, List<P2PEthPoolReward>>
/**
* Check P2P staking availability by finding public vault
* Get flow of cached vaults.
*
* @return Flow of StakingAvailability - Available with StakingOption.P2P if public vault found,
* This returns vaults from the local cache/store.
* Call [fetchVaults] first to populate the cache from the network.
*
* @return Flow of cached vaults list
*/
fun getVaultsFlow(): Flow<List<P2PEthPoolVault>>
/**
* Check P2PEthPool staking availability by finding public vault
*
* @return Flow of StakingAvailability - Available with StakingOption.P2PEthPool if public vault found,
* TemporaryUnavailable if not found or vaults empty
*/
fun getStakingAvailability(): Flow<StakingAvailability>
/**
* Check P2P staking availability synchronously
* Check P2PEthPool staking availability synchronously
*
* @return StakingAvailability - Available with StakingOption.P2P if public vault found,
* @return StakingAvailability - Available with StakingOption.P2PEthPool if public vault found,
* TemporaryUnavailable if not found or vaults empty
*/
suspend fun getStakingAvailabilitySync(): StakingAvailability

View file

@ -8,6 +8,7 @@ import com.tangem.domain.models.network.Network
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.models.staking.NetworkType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingAction
@ -24,9 +25,17 @@ interface StakeKitRepository {
fun getEnabledYields(): Flow<List<Yield>>
fun getStakingAvailability(rawCurrencyId: CryptoCurrency.RawID, symbol: String): Flow<StakingAvailability>
fun getStakingAvailability(
integrationId: StakingIntegrationID.StakeKit,
rawCurrencyId: CryptoCurrency.RawID,
symbol: String,
): Flow<StakingAvailability>
suspend fun getStakingAvailabilitySync(rawCurrencyId: CryptoCurrency.RawID, symbol: String): StakingAvailability
suspend fun getStakingAvailabilitySync(
integrationId: StakingIntegrationID.StakeKit,
rawCurrencyId: CryptoCurrency.RawID,
symbol: String,
): StakingAvailability
suspend fun getEntryInfo(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingEntryInfo

View file

@ -2,35 +2,50 @@ package com.tangem.domain.staking.usecase
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.toStakingTarget
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
import com.tangem.domain.staking.repositories.StakeKitRepository
import com.tangem.domain.staking.toggles.StakingFeatureToggles
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.combine
/**
* Emits a map of Validators values per currency for staking.
* Emits a map of StakingTarget values per currency for staking.
*
* Return map:
* - key: currency staking key (network.backendId + "_" + symbol)
* - value: validators
* - key: currency staking key (coinGeckoId + "_" + symbol)
* - value: list of staking targets (validators or vaults)
*/
class StakingApyFlowUseCase(
private val stakeKitRepository: StakeKitRepository,
private val p2pEthPoolRepository: P2PEthPoolRepository,
private val stakingFeatureToggles: StakingFeatureToggles,
) {
operator fun invoke(): Flow<Map<String, List<Yield.Validator>>> {
return stakeKitRepository.getEnabledYields()
.map { yields ->
yields.filterNot { yield ->
val isCardanoYield = yield.token.coinGeckoId == Blockchain.Cardano.toCoinId()
isCardanoYield && !stakingFeatureToggles.isCardanoStakingEnabled
}.associate { yield ->
val key = "${yield.token.coinGeckoId}_${yield.token.symbol}"
val apy = yield.validators
key to apy
}
operator fun invoke(): Flow<Map<String, List<StakingTarget>>> {
return combine(
stakeKitRepository.getEnabledYields(),
p2pEthPoolRepository.getVaultsFlow(),
) { yields, p2pVaults ->
val stakeKitMap = yields.filterNot { yield ->
val isCardanoYield = yield.token.coinGeckoId == Blockchain.Cardano.toCoinId()
isCardanoYield && !stakingFeatureToggles.isCardanoStakingEnabled
}.associate { yield ->
val key = "${yield.token.coinGeckoId}_${yield.token.symbol}"
val targets = yield.validators.map { it.toStakingTarget() }
key to targets
}
val p2pMap = if (p2pVaults.isNotEmpty()) {
val ethKey = "${Blockchain.Ethereum.toCoinId()}_${Blockchain.Ethereum.currency}"
val targets = p2pVaults.map { it.toStakingTarget() }
mapOf(ethKey to targets)
} else {
emptyMap()
}
stakeKitMap + p2pMap
}
}
}

View file

@ -7,7 +7,7 @@ import java.math.BigDecimal
/**
* Provider-agnostic extension to get total balance including rewards.
* Works for both StakeKit and P2P providers.
* Works for both StakeKit and P2PEthPool providers.
*
* Returns sum of all staking-related balances including rewards
* (staked + unstaking + withdrawable + rewards).
@ -18,7 +18,7 @@ import java.math.BigDecimal
fun StakingBalance.Data.getTotalWithRewardsStakingBalance(blockchainId: String): BigDecimal {
return when (this) {
is StakingBalance.Data.StakeKit -> getTotalWithRewardsStakingBalanceStakeKit(blockchainId)
is StakingBalance.Data.P2P -> {
is StakingBalance.Data.P2PEthPool -> {
val rewards = totalRewards
if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId)) {
totalStaked + unstakingAmount + withdrawableAmount + rewards
@ -31,7 +31,7 @@ fun StakingBalance.Data.getTotalWithRewardsStakingBalance(blockchainId: String):
/**
* Provider-agnostic extension to get total staking balance excluding rewards.
* Works for both StakeKit and P2P providers.
* Works for both StakeKit and P2PEthPool providers.
*
* Returns sum of all staking-related balances (staked + unstaking + withdrawable)
* excluding rewards.
@ -39,7 +39,7 @@ fun StakingBalance.Data.getTotalWithRewardsStakingBalance(blockchainId: String):
fun StakingBalance.Data.getTotalStakingBalance(blockchainId: String): BigDecimal {
return when (this) {
is StakingBalance.Data.StakeKit -> getTotalStakingBalanceStakeKit(blockchainId)
is StakingBalance.Data.P2P -> totalStaked + unstakingAmount + withdrawableAmount
is StakingBalance.Data.P2PEthPool -> totalStaked + unstakingAmount + withdrawableAmount
}
}

View file

@ -149,8 +149,8 @@ internal class StakingIdFactoryTest {
expected = createStakingId(integrationId = StakingIntegrationID.StakeKit.Coin.Cardano),
),
CreateModel(
currencyId = createCurrencyId(blockchain = StakingIntegrationID.P2P.EthereumPooled.blockchain),
expected = createStakingId(integrationId = StakingIntegrationID.P2P.EthereumPooled),
currencyId = createCurrencyId(blockchain = StakingIntegrationID.P2PEthPool.blockchain),
expected = createStakingId(integrationId = StakingIntegrationID.P2PEthPool),
),
CreateModel(
currencyId = CryptoCurrency.ID.fromValue(

View file

@ -38,17 +38,6 @@ class StakingIntegrationIDTest {
Truth.assertThat(actual).hasSize(expected)
}
@Test
fun `all P2P blockchains are unique`() {
// Act
val actual = StakingIntegrationID.P2P.entries
.distinctBy(StakingIntegrationID.P2P::blockchain)
// Assert
val expected = StakingIntegrationID.P2P.entries.size
Truth.assertThat(actual).hasSize(expected)
}
@Test
fun `all sub blockchains are unique`() {
// Act
@ -151,8 +140,8 @@ class StakingIntegrationIDTest {
expected = StakingIntegrationID.StakeKit.Coin.Cardano,
),
CreateModel(
currencyId = createCurrencyId(blockchain = StakingIntegrationID.P2P.EthereumPooled.blockchain),
expected = StakingIntegrationID.P2P.EthereumPooled,
currencyId = createCurrencyId(blockchain = StakingIntegrationID.P2PEthPool.blockchain),
expected = StakingIntegrationID.P2PEthPool,
),
CreateModel(
currencyId = CryptoCurrency.ID.fromValue(value = "token⟨ETH⟩polygon-ecosystem-token⚓1234567890"),

View file

@ -267,7 +267,7 @@ object CryptoCurrencyStatusFactory {
null
}
}
is StakingBalance.Data.P2P -> {
is StakingBalance.Data.P2PEthPool -> {
// TODO p2p
val isCurrentAddressStaking = stakingBalance.stakingId.address == address.defaultAddress.value
if (isCurrentAddressStaking) stakingBalance else null

View file

@ -38,6 +38,7 @@ dependencies {
implementation(projects.domain.markets)
implementation(projects.domain.onramp.models)
implementation(projects.domain.staking.models)
implementation(projects.domain.staking)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets)

View file

@ -174,7 +174,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
AppRoute.Staking(
userWalletId = cryptoCurrencyData.userWallet.walletId,
cryptoCurrency = cryptoCurrencyData.status.currency,
yieldId = option.integrationId,
integrationId = option.integrationId,
),
)
}

View file

@ -16,7 +16,7 @@ dependencies {
/** Domain models */
api(projects.domain.models)
implementation(projects.domain.staking.models)
implementation(projects.domain.staking)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)

View file

@ -1,5 +1,6 @@
package com.tangem.features.staking.api
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.currency.CryptoCurrency
@ -10,7 +11,7 @@ interface StakingComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val cryptoCurrency: CryptoCurrency,
val yieldId: String,
val integrationId: StakingIntegrationID,
)
interface Factory : ComponentFactory<Params, StakingComponent>

View file

@ -18,10 +18,10 @@ internal class StakingAnalyticSender(
fun initialInfoScreen(value: StakingUiState) {
val initialInfoState = value.initialInfoState as? StakingStates.InitialInfoState.Data
val validatorState = initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data
val validatorCount = validatorState?.balances
?.filterNot { it.validator?.address.isNullOrBlank() }
?.distinctBy { it.validator?.address }
val balanceState = initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data
val validatorCount = balanceState?.balances
?.filterNot { it.target?.address.isNullOrBlank() }
?.distinctBy { it.target?.address }
?.size ?: 0
analyticsEventHandler.send(
@ -34,7 +34,7 @@ internal class StakingAnalyticSender(
fun confirmationScreen(value: StakingUiState) {
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
val validatorName = validatorState?.chosenValidator?.name ?: return
val validatorName = validatorState?.chosenTarget?.name ?: return
if (confirmationState?.innerState == InnerConfirmationStakingState.COMPLETED) return
@ -80,7 +80,7 @@ internal class StakingAnalyticSender(
fun sendTransactionStakingAnalytics(value: StakingUiState, cryptoCurrencyStatus: CryptoCurrencyStatus) {
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
val validatorName = validatorState?.chosenValidator?.name ?: return
val validatorName = validatorState?.chosenTarget?.name ?: return
analyticsEventHandler.send(
Basic.TransactionSent(
@ -102,7 +102,7 @@ internal class StakingAnalyticSender(
fun sendTransactionStakingClickedAnalytics(value: StakingUiState) {
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
val validatorName = validatorState?.chosenValidator?.name ?: return
val validatorName = validatorState?.chosenTarget?.name ?: return
analyticsEventHandler.send(
StakingAnalyticsEvent.ButtonAction(

View file

@ -1,6 +1,5 @@
package com.tangem.features.staking.impl.deeplink
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY
@ -8,7 +7,6 @@ import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
import com.tangem.domain.staking.GetYieldUseCase
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
@ -28,7 +26,6 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
private val appRouter: AppRouter,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val getYieldUseCase: GetYieldUseCase,
private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase,
) : StakingDeepLinkHandler {
@ -73,19 +70,13 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
return@launch
}
val isStakingEnabled = getStakingAvailabilityUseCase.invokeSync(
val availability = getStakingAvailabilityUseCase.invokeSync(
userWalletId = selectedUserWalletId,
cryptoCurrency = cryptoCurrency,
).getOrNull()
if (isStakingEnabled !is StakingAvailability.Available) {
return@launch
}
val yield = getYieldUseCase.invoke(
cryptoCurrencyId = cryptoCurrency.id,
symbol = cryptoCurrency.symbol,
).getOrElse {
val option = (availability as? StakingAvailability.Available)?.option
if (option == null) {
Timber.e("Staking is unavailable for ${cryptoCurrency.name}")
return@launch
}
@ -94,7 +85,7 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
AppRoute.Staking(
userWalletId = selectedUserWalletId,
cryptoCurrency = cryptoCurrency,
yieldId = yield.id,
integrationId = option.integrationId,
),
)
}

View file

@ -4,7 +4,7 @@ import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
import java.math.BigDecimal
@ -35,7 +35,7 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
fun openValidators()
fun onValidatorSelect(validator: Yield.Validator)
fun onTargetSelect(target: StakingTarget)
fun openRewardsValidators()

View file

@ -45,11 +45,15 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.*
import com.tangem.domain.staking.analytics.StakeScreenSource
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
import com.tangem.domain.staking.model.P2PEthPoolIntegration
import com.tangem.domain.staking.model.StakeKitIntegration
import com.tangem.domain.staking.model.StakingApproval
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.staking.utils.getValidatorsCount
@ -134,6 +138,7 @@ internal class StakingModel @Inject constructor(
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val getActionsUseCase: GetActionsUseCase,
private val getYieldUseCase: GetYieldUseCase,
private val p2pEthPoolRepository: P2PEthPoolRepository,
private val checkAccountInitializedUseCase: CheckAccountInitializedUseCase,
private val createTransferTransactionUseCase: CreateTransferTransactionUseCase,
private val getFeeUseCase: GetFeeUseCase,
@ -164,9 +169,18 @@ internal class StakingModel @Inject constructor(
private val cryptoCurrencyId: CryptoCurrency.ID = params.cryptoCurrency.id
private val userWalletId: UserWalletId = params.userWalletId
private val yield: Yield = runBlocking {
getYieldUseCase(params.yieldId).getOrElse {
error("yield must be not null")
private val integration: StakingIntegration = runBlocking {
when (val integrationId = params.integrationId) {
is StakingIntegrationID.StakeKit -> {
val yield = getYieldUseCase(integrationId.value).getOrElse {
error("yield must be not null")
}
StakeKitIntegration(integrationId, yield)
}
StakingIntegrationID.P2PEthPool -> {
val vaults = p2pEthPoolRepository.getVaults().getOrElse { emptyList() }
P2PEthPoolIntegration(integrationId, vaults)
}
}
}
@ -194,7 +208,7 @@ internal class StakingModel @Inject constructor(
return invalidatePendingTransactionsUseCase(
balanceItems = stakeKitBalance?.balance?.items.orEmpty(),
stakingActions = stakingActions,
token = yield.token,
token = integration.token,
).getOrElse { emptyList() }
}
@ -205,7 +219,7 @@ internal class StakingModel @Inject constructor(
stakingBalanceUpdater.create(
cryptoCurrencyStatus,
userWallet,
yield,
integration,
)
}
@ -213,7 +227,7 @@ internal class StakingModel @Inject constructor(
stakingFeeTransactionLoader.create(
cryptoCurrencyStatus = cryptoCurrencyStatus,
userWallet = userWallet,
yield = yield,
integration = integration,
)
}
@ -221,7 +235,7 @@ internal class StakingModel @Inject constructor(
stakingTransactionLoader.create(
cryptoCurrencyStatus = cryptoCurrencyStatus,
userWallet = userWallet,
yield = yield,
integration = integration,
isAmountSubtractAvailable = isAmountSubtractAvailable,
)
}
@ -280,7 +294,7 @@ internal class StakingModel @Inject constructor(
val hasNoYieldBalanceData = cryptoCurrencyStatus.value.stakingBalance !is StakingBalance.Data.StakeKit
when {
isInitialInfoStep && noBalanceState && yield.allValidatorsFull && hasNoYieldBalanceData -> {
isInitialInfoStep && noBalanceState && integration.areAllTargetsFull && hasNoYieldBalanceData -> {
stakingEventFactory.createStakingValidatorsUnavailableAlert()
return@launch
}
@ -293,12 +307,12 @@ internal class StakingModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
stakingApproval = stakingApproval,
stakingAllowance = stakingAllowance,
yieldArgs = yield.args,
integration = integration,
).let(::add)
if (yield.args.enter.isPartialAmountDisabled) {
if (integration.isPartialAmountDisabled) {
ValidatorSelectChangeTransformer(
selectedValidator = yield.preferredValidators.firstOrNull(),
yield = yield,
selectedTarget = integration.preferredTargets.firstOrNull(),
integration = integration,
).let(::add)
SetAmountDataTransformer(
clickIntents = this@StakingModel,
@ -313,7 +327,7 @@ internal class StakingModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
minimumTransactionAmount = minimumTransactionAmount,
actionType = uiState.value.actionType,
yield = yield,
integration = integration,
).let(::add)
}
}
@ -327,7 +341,7 @@ internal class StakingModel @Inject constructor(
override fun getFee() {
stateController.update(
SetConfirmationStateLoadingTransformer(
yield = yield,
integration = integration,
appCurrency = appCurrency,
cryptoCurrency = cryptoCurrencyStatus.currency,
),
@ -479,7 +493,7 @@ internal class StakingModel @Inject constructor(
}
override fun onAmountEnterClick() {
if (yield.preferredValidators.isEmpty()) {
if (integration.preferredTargets.isEmpty()) {
stateController.updateEvent(
StakingEvent.ShowAlert(StakingAlertUM.NoAvailableValidators),
)
@ -487,8 +501,8 @@ internal class StakingModel @Inject constructor(
if (uiState.value.actionType is StakingActionCommonType.Enter) {
stateController.updateAll(
ValidatorSelectChangeTransformer(
selectedValidator = null,
yield = yield,
selectedTarget = null,
integration = integration,
),
)
}
@ -502,7 +516,7 @@ internal class StakingModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
minimumTransactionAmount = minimumTransactionAmount,
value = value,
yield = yield,
integration = integration,
),
)
}
@ -518,7 +532,7 @@ internal class StakingModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
minimumTransactionAmount = minimumTransactionAmount,
actionType = uiState.value.actionType,
yield = yield,
integration = integration,
),
)
}
@ -539,16 +553,16 @@ internal class StakingModel @Inject constructor(
stakingStateRouter.showValidators()
}
override fun onValidatorSelect(validator: Yield.Validator) {
override fun onTargetSelect(target: StakingTarget) {
analyticsEventHandler.send(
StakingAnalyticsEvent.ValidatorChosen(
validator = validator.name,
validator = target.name,
),
)
stateController.update(
ValidatorSelectChangeTransformer(
selectedValidator = validator,
yield = yield,
selectedTarget = target,
integration = integration,
),
)
}
@ -606,7 +620,7 @@ internal class StakingModel @Inject constructor(
balanceType = activeStake.type,
pendingActions = activeStake.pendingActions,
balanceState = activeStake,
validator = activeStake.validator,
target = activeStake.target,
amountValue = activeStake.cryptoValue,
)
onNextClick(activeStake)
@ -619,7 +633,7 @@ internal class StakingModel @Inject constructor(
balanceType = activeStake.type,
pendingAction = action,
balanceState = activeStake,
validator = activeStake.validator,
target = activeStake.target,
amountValue = activeStake.cryptoValue,
)
stateController.update(DismissBottomSheetStateTransformer)
@ -788,7 +802,7 @@ internal class StakingModel @Inject constructor(
isSubtractAvailable = isAmountSubtractAvailable,
feeError = feeError,
stakingError = stakingError,
yield = yield,
integration = integration,
),
)
}
@ -911,7 +925,7 @@ internal class StakingModel @Inject constructor(
val validatorState = uiState.value.validatorState as? StakingStates.ValidatorState.Data
val feeState = confirmationState?.feeState as? FeeState.Content
val validator = validatorState?.chosenValidator
val target = validatorState?.chosenTarget
val feeAmount = feeState?.fee?.amount
val amount = amountState?.amountTextField?.cryptoAmount
saveBlockchainErrorUseCase(
@ -919,7 +933,7 @@ internal class StakingModel @Inject constructor(
errorMessage = errorMessage,
blockchainId = network.rawId,
derivationPath = network.derivationPath.value,
destinationAddress = validator?.address.orEmpty(),
destinationAddress = target?.address.orEmpty(),
tokenSymbol = (cryptoCurrencyStatus.currency as? CryptoCurrency.Token)?.symbol,
amount = amount?.run { value?.toPlainString() + currencySymbol }.orEmpty(),
fee = feeAmount?.run { value?.toPlainString() + currencySymbol }.orEmpty(),
@ -928,7 +942,7 @@ internal class StakingModel @Inject constructor(
val email = FeedbackEmailType.StakingProblem(
walletMetaInfo = metaInfo,
validatorName = validator?.name,
validatorName = target?.name,
transactionTypes = transactionsInProgress.map { it.type.name },
unsignedTransactions = transactionsInProgress.map { it.unsignedTransaction },
)
@ -1229,7 +1243,7 @@ internal class StakingModel @Inject constructor(
stateController.updateAll(
SetInitialDataStateTransformer(
clickIntents = this@StakingModel,
yield = yield,
integration = integration,
isAnyTokenStaked = isAnyTokenStaked,
cryptoCurrencyStatus = status,
userWalletProvider = Provider { userWallet },
@ -1248,7 +1262,7 @@ internal class StakingModel @Inject constructor(
balanceState: BalanceState,
pendingActions: ImmutableList<PendingAction> = persistentListOf(),
pendingAction: PendingAction? = pendingActions.firstOrNull(),
validator: Yield.Validator?,
target: StakingTarget?,
amountValue: String,
) {
stateController.updateAll(
@ -1261,11 +1275,11 @@ internal class StakingModel @Inject constructor(
pendingActions = pendingActions,
pendingAction = pendingAction,
stakingAllowance = stakingAllowance,
yieldArgs = yield.args,
integration = integration,
),
ValidatorSelectChangeTransformer(
selectedValidator = validator,
yield = yield,
selectedTarget = target,
integration = integration,
),
SetAmountDataTransformer(
clickIntents = this,
@ -1280,7 +1294,7 @@ internal class StakingModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
value = amountValue,
minimumTransactionAmount = minimumTransactionAmount,
yield = yield,
integration = integration,
),
)
}

View file

@ -6,7 +6,7 @@ import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.PendingAction
import com.tangem.domain.models.staking.PendingActionConstraints
import com.tangem.domain.models.staking.RewardBlockType
import com.tangem.domain.staking.model.stakekit.*
import com.tangem.domain.staking.model.StakingTarget
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal
@ -35,10 +35,10 @@ internal data class BalanceState(
val fiatAmount: BigDecimal?,
val formattedFiatAmount: TextReference,
val rawCurrencyId: String?,
val validator: Yield.Validator?,
val target: StakingTarget?,
val pendingActions: ImmutableList<PendingAction>,
val isPending: Boolean,
val validatorAddress: String?,
val targetAddress: String?,
)
@Immutable

View file

@ -10,7 +10,7 @@ import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.domain.models.staking.PendingAction
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
import com.tangem.features.staking.impl.presentation.state.events.StakingEvent
@ -97,9 +97,9 @@ internal sealed class StakingStates {
override val isPrimaryButtonEnabled: Boolean,
override val isClickable: Boolean,
val isVisibleOnConfirmation: Boolean,
val chosenValidator: Yield.Validator,
val activeValidator: Yield.Validator?,
val availableValidators: List<Yield.Validator>,
val chosenTarget: StakingTarget,
val activeTarget: StakingTarget?,
val availableTargets: List<StakingTarget>,
) : ValidatorState()
data class Empty(

View file

@ -12,7 +12,7 @@ import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.BalanceType.Companion.isClickable
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.staking.action.StakingActionType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.utils.getRewardStakingBalance
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.BalanceState
@ -29,24 +29,24 @@ import java.util.Calendar
internal class BalanceItemConverter(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrencyProvider: Provider<AppCurrency>,
private val yield: Yield,
private val integration: StakingIntegration,
) : Converter<BalanceItem, BalanceState?> {
override fun convert(value: BalanceItem): BalanceState? {
val appCurrency = appCurrencyProvider()
val cryptoCurrency = cryptoCurrencyStatus.currency
val validator = yield.validators.firstOrNull {
val target = integration.targets.firstOrNull {
value.validatorAddress?.contains(it.address, ignoreCase = true) == true
}
val cryptoAmount = value.getBalanceValue()
val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount)
val title = value.type.getTitle(validator?.name)
val title = value.type.getTitle(target?.name)
return title?.let {
BalanceState(
groupId = value.groupId,
validator = validator,
target = target,
title = title,
subtitle = getSubtitle(value),
type = value.type,
@ -68,7 +68,7 @@ internal class BalanceItemConverter(
pendingActions = value.pendingActions.toPersistentList(),
isClickable = value.isClickable(),
isPending = value.isPending,
validatorAddress = value.validatorAddress,
targetAddress = value.validatorAddress,
)
}
}
@ -108,7 +108,7 @@ internal class BalanceItemConverter(
resourceReference(R.string.staking_tap_to_unlock)
}
BalanceType.PREPARING -> {
val warmupPeriod = yield.metadata.warmupPeriod.days
val warmupPeriod = integration.warmupPeriodDays
combinedReference(
resourceReference(R.string.staking_details_warmup_period),
stringReference(" "),
@ -124,7 +124,7 @@ internal class BalanceItemConverter(
}
private fun getUnbondingDate(date: Instant?): TextReference? {
val unbondingPeriod = yield.metadata.cooldownPeriod?.days ?: return null
val unbondingPeriod = integration.cooldownPeriodDays ?: return null
if (date == null) {
return combinedReference(
resourceReference(R.string.staking_details_unbonding_period),

View file

@ -10,7 +10,8 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.BalanceItem
import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.utils.Provider
@ -21,7 +22,7 @@ import java.math.BigDecimal
internal class RewardsValidatorStateConverter(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrencyProvider: Provider<AppCurrency>,
private val yield: Yield,
private val integration: StakingIntegration,
) : Converter<Unit, StakingStates.RewardsValidatorsState> {
override fun convert(value: Unit): StakingStates.RewardsValidatorsState {
val stakingBalance = cryptoCurrencyStatus.value.stakingBalance
@ -42,13 +43,13 @@ internal class RewardsValidatorStateConverter(
private fun List<BalanceItem>.mapRewardBalances(cryptoCurrencyStatus: CryptoCurrencyStatus) =
this.mapNotNull { balance ->
val validator = yield.validators.firstOrNull {
val target = integration.targets.firstOrNull {
it.address.contains(balance.validatorAddress.orEmpty(), ignoreCase = true)
}
val cryptoValue = balance.amount
val fiatValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoValue)
validator?.toBalanceState(
target?.toBalanceState(
balance = balance,
cryptoCurrencyStatus = cryptoCurrencyStatus,
cryptoValue = cryptoValue,
@ -56,7 +57,7 @@ internal class RewardsValidatorStateConverter(
)
}
private fun Yield.Validator.toBalanceState(
private fun StakingTarget.toBalanceState(
balance: BalanceItem,
cryptoCurrencyStatus: CryptoCurrencyStatus,
cryptoValue: BigDecimal,
@ -80,7 +81,7 @@ internal class RewardsValidatorStateConverter(
return BalanceState(
groupId = balance.groupId,
validator = this,
target = this,
title = stringReference(this.name),
subtitle = null,
cryptoValue = cryptoValue.parseBigDecimal(cryptoCurrency.decimals),
@ -93,7 +94,7 @@ internal class RewardsValidatorStateConverter(
isClickable = true,
type = balance.type,
isPending = balance.isPending,
validatorAddress = balance.validatorAddress,
targetAddress = balance.validatorAddress,
)
}
}

View file

@ -11,7 +11,7 @@ import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.RewardBlockType
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.staking.action.StakingActionType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.utils.getRewardStakingBalance
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
import com.tangem.features.staking.impl.presentation.state.YieldReward
@ -25,11 +25,11 @@ internal class YieldBalancesConverter(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrencyProvider: Provider<AppCurrency>,
private val balancesToShowProvider: Provider<List<BalanceItem>>,
private val yield: Yield,
private val integration: StakingIntegration,
) : Converter<Unit, InnerYieldBalanceState> {
private val balanceItemConverter by lazy(LazyThreadSafetyMode.NONE) {
BalanceItemConverter(cryptoCurrencyStatus, appCurrencyProvider, yield)
BalanceItemConverter(cryptoCurrencyStatus, appCurrencyProvider, integration)
}
override fun convert(value: Unit): InnerYieldBalanceState {

View file

@ -4,7 +4,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.FetchActionsUseCase
import com.tangem.domain.staking.FetchStakingYieldBalanceUseCase
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.FetchPendingTransactionsUseCase
@ -27,7 +27,7 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
@DelayedWork private val coroutineScope: CoroutineScope,
@Assisted private val userWallet: UserWallet,
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
@Assisted private val yield: Yield,
@Assisted private val integration: StakingIntegration,
) {
fun updateAfterTransaction() {
coroutineScope.launch {
@ -105,7 +105,7 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
fetchActionsUseCase(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
networkType = yield.token.network,
networkType = integration.token.network,
stakingActionStatus = StakingActionStatus.PROCESSING,
)
}
@ -115,7 +115,7 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
fun create(
cryptoCurrencyStatus: CryptoCurrencyStatus,
userWallet: UserWallet,
yield: Yield,
integration: StakingIntegration,
): StakingBalanceUpdater
}

View file

@ -12,12 +12,11 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.PendingAction
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.EstimateGasUseCase
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.tokens.model.staking.getCurrentToken
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
@ -45,7 +44,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase,
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
@Assisted private val userWallet: UserWallet,
@Assisted private val yield: Yield,
@Assisted private val integration: StakingIntegration,
) {
suspend fun getFee(
@ -58,9 +57,9 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data
?: error("Illegal state")
val validatorAddress = (state.validatorState as? StakingStates.ValidatorState.Data)?.chosenValidator?.address
?: state.balanceState?.validatorAddress
?: error("No validator address provided")
val validatorAddress = (state.validatorState as? StakingStates.ValidatorState.Data)?.chosenTarget?.address
?: state.balanceState?.targetAddress
?: error("No target address provided")
val amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided")
@ -170,11 +169,11 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
network = cryptoCurrencyStatus.currency.network,
params = ActionParams(
actionCommonType = stateController.value.actionType,
integrationId = yield.id,
integrationId = integration.integrationId.value,
amount = amount,
address = sourceAddress,
validatorAddress = validatorAddress,
token = yield.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
token = integration.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
passthrough = action?.passthrough,
type = action?.type,
),
@ -234,7 +233,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
fun create(
cryptoCurrencyStatus: CryptoCurrencyStatus,
userWallet: UserWallet,
yield: Yield,
integration: StakingIntegration,
): StakingFeeTransactionLoader
}
}

View file

@ -13,15 +13,14 @@ import com.tangem.domain.staking.GetConstructedStakingTransactionUseCase
import com.tangem.domain.staking.GetStakingTransactionsUseCase
import com.tangem.domain.staking.SaveUnsubmittedHashUseCase
import com.tangem.domain.staking.SubmitHashUseCase
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.SubmitHashData
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionStatus
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType
import com.tangem.domain.tokens.model.staking.getCurrentToken
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
@ -56,12 +55,12 @@ internal class StakingTransactionSender @AssistedInject constructor(
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
@Assisted private val userWallet: UserWallet,
@Assisted private val yield: Yield,
@Assisted private val integration: StakingIntegration,
@Assisted private val isAmountSubtractAvailable: Boolean,
) {
private val balanceUpdater: StakingBalanceUpdater
get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet, yield)
get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet, integration)
suspend fun constructAndSendTransactions(
onConstructSuccess: (List<StakingTransaction>) -> Unit,
@ -193,7 +192,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
val amountState = state.amountState as? AmountState.Data
?: error("No amount provided")
val validatorAddress = validatorState.chosenValidator.address
val validatorAddress = validatorState.chosenTarget.address
val amount = getAmount(amountState, fee, confirmationState.reduceAmountBy)
return getStakingTransactionsUseCase(
@ -201,11 +200,11 @@ internal class StakingTransactionSender @AssistedInject constructor(
network = cryptoCurrencyStatus.currency.network,
params = ActionParams(
actionCommonType = state.actionType,
integrationId = yield.id,
integrationId = integration.integrationId.value,
amount = amount,
address = defaultAddress,
validatorAddress = validatorAddress,
token = yield.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
token = integration.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
passthrough = action?.passthrough,
type = action?.type,
),
@ -306,7 +305,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
fun create(
cryptoCurrencyStatus: CryptoCurrencyStatus,
userWallet: UserWallet,
yield: Yield,
integration: StakingIntegration,
isAmountSubtractAvailable: Boolean,
): StakingTransactionSender
}

View file

@ -7,6 +7,7 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.RewardBlockType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.toStakingTarget
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
@ -81,7 +82,7 @@ internal object InitialStakingStatePreview {
fiatAmount = null,
formattedFiatAmount = stringReference("100 $"),
rawCurrencyId = null,
validator = Yield.Validator(
target = Yield.Validator(
address = "address",
status = Yield.Validator.ValidatorStatus.ACTIVE,
name = "Binance",
@ -92,13 +93,13 @@ internal object InitialStakingStatePreview {
votingPower = null,
preferred = false,
isStrategicPartner = false,
),
).toStakingTarget(),
pendingActions = persistentListOf(),
isClickable = true,
type = BalanceType.STAKED,
subtitle = null,
isPending = false,
validatorAddress = "",
targetAddress = "",
),
),
),

View file

@ -1,7 +1,11 @@
package com.tangem.features.staking.impl.presentation.state.previewdata
import com.tangem.domain.staking.model.common.RewardInfo
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.Yield.Validator.ValidatorStatus
import com.tangem.domain.staking.model.toStakingTarget
import com.tangem.features.staking.impl.presentation.state.StakingStates
import java.math.BigDecimal
@ -13,9 +17,9 @@ internal object ValidatorStatePreviewData {
status = ValidatorStatus.ACTIVE,
name = "Luganodes",
image = "https://assets.stakek.it/validators/luganodes.png",
rewardInfo = Yield.RewardInfo(
rewardInfo = RewardInfo(
rate = BigDecimal("0.054823398040640445"),
type = Yield.RewardType.APR,
type = RewardType.APR,
),
commission = 0.1,
stakedBalance = "355544384.45009977",
@ -29,9 +33,9 @@ internal object ValidatorStatePreviewData {
status = ValidatorStatus.ACTIVE,
name = "InfStones",
image = "https://assets.stakek.it/validators/infstones.png",
rewardInfo = Yield.RewardInfo(
rewardInfo = RewardInfo(
rate = BigDecimal("0.057786472172836965"),
type = Yield.RewardType.APR,
type = RewardType.APR,
),
commission = 0.05,
stakedBalance = "12495684.05643019",
@ -45,9 +49,9 @@ internal object ValidatorStatePreviewData {
status = ValidatorStatus.ACTIVE,
name = "Kiln",
image = "https://assets.stakek.it/validators/kiln.png",
rewardInfo = Yield.RewardInfo(
rewardInfo = RewardInfo(
rate = BigDecimal("0.057786472172836965"),
type = Yield.RewardType.APR,
type = RewardType.APR,
),
commission = 0.05,
stakedBalance = "85400369.96393165",
@ -58,11 +62,13 @@ internal object ValidatorStatePreviewData {
),
)
private val targetList: List<StakingTarget> = validatorList.map { it.toStakingTarget() }
val validatorState = StakingStates.ValidatorState.Data(
availableValidators = validatorList,
chosenValidator = validatorList.first(),
availableTargets = targetList,
chosenTarget = targetList.first(),
isPrimaryButtonEnabled = true,
activeValidator = null,
activeTarget = null,
isClickable = true,
isVisibleOnConfirmation = true,
)

View file

@ -3,7 +3,7 @@ package com.tangem.features.staking.impl.presentation.state.stub
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
@ -40,7 +40,7 @@ internal object StakingClickIntentsStub : StakingClickIntents {
override fun openValidators() {}
override fun onValidatorSelect(validator: Yield.Validator) {}
override fun onTargetSelect(target: StakingTarget) {}
override fun openRewardsValidators() {}

View file

@ -5,7 +5,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.PendingAction
import com.tangem.domain.models.staking.action.StakingActionType
import com.tangem.domain.staking.model.StakingApproval
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions
@ -24,7 +24,7 @@ internal class SetConfirmationStateInitTransformer(
private val stakingApproval: StakingApproval,
private val stakingAllowance: BigDecimal,
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val yieldArgs: Yield.Args,
private val integration: StakingIntegration,
private val pendingActions: ImmutableList<PendingAction>? = null,
private val pendingAction: PendingAction? = pendingActions?.firstOrNull(),
) : Transformer<StakingUiState> {
@ -66,7 +66,7 @@ internal class SetConfirmationStateInitTransformer(
}
private fun getActionType(prevState: StakingUiState): StakingActionCommonType {
val isPartialEnterAmountDisabled = yieldArgs.enter.isPartialAmountDisabled
val isPartialEnterAmountDisabled = integration.isPartialAmountDisabled
val isPartialExitAmountDisabled = isPartiallyUnstakeDisabled(prevState)
return when {
isEnter -> StakingActionCommonType.Enter(isPartialEnterAmountDisabled)
@ -87,12 +87,12 @@ internal class SetConfirmationStateInitTransformer(
private fun isPartiallyUnstakeDisabled(state: StakingUiState): Boolean {
val isSolana = BlockchainUtils.isSolana(state.cryptoCurrencyBlockchainId)
val isValidatorPreferred = balanceState?.validator?.preferred == true
val isTargetPreferred = balanceState?.target?.isPreferred == true
return if (isSolana && !isValidatorPreferred) {
return if (isSolana && !isTargetPreferred) {
true
} else {
yieldArgs.exit?.isPartialAmountDisabled == true
integration.exitArgs?.isPartialAmountDisabled == true
}
}
}

View file

@ -8,7 +8,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.FeeState
@ -18,7 +18,7 @@ import com.tangem.features.staking.impl.presentation.state.utils.getRewardSchedu
import com.tangem.utils.transformer.Transformer
internal class SetConfirmationStateLoadingTransformer(
private val yield: Yield,
private val integration: StakingIntegration,
private val appCurrency: AppCurrency,
private val cryptoCurrency: CryptoCurrency,
) : Transformer<StakingUiState> {
@ -48,7 +48,7 @@ internal class SetConfirmationStateLoadingTransformer(
)
}
val rewardSchedule = getRewardScheduleText(
rewardSchedule = yield.metadata.rewardSchedule,
rewardSchedule = integration.rewardSchedule,
networkId = cryptoCurrency.network.rawId,
decapitalize = true,
)

View file

@ -19,6 +19,9 @@ import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.BalanceItem
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
@ -41,7 +44,7 @@ import java.math.BigDecimal
@Suppress("LongParameterList")
internal class SetInitialDataStateTransformer(
private val clickIntents: StakingClickIntents,
private val yield: Yield,
private val integration: StakingIntegration,
private val isAnyTokenStaked: Boolean,
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val userWalletProvider: Provider<UserWallet>,
@ -55,7 +58,7 @@ internal class SetInitialDataStateTransformer(
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
private val rewardsValidatorStateConverter by lazy(LazyThreadSafetyMode.NONE) {
RewardsValidatorStateConverter(cryptoCurrencyStatus, appCurrencyProvider, yield)
RewardsValidatorStateConverter(cryptoCurrencyStatus, appCurrencyProvider, integration)
}
private val yieldBalancesConverter by lazy(LazyThreadSafetyMode.NONE) {
@ -63,7 +66,7 @@ internal class SetInitialDataStateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
appCurrencyProvider = appCurrencyProvider,
balancesToShowProvider = balancesToShowProvider,
yield = yield,
integration = integration,
)
}
@ -116,27 +119,27 @@ internal class SetInitialDataStateTransformer(
}
private fun createAnnualPercentageItem(): RoundedListWithDividersItemData {
val validators = yield.preferredValidators
val rateRangeInfo = getPercentageRange(validators)
val targets = integration.preferredTargets
val rateRangeInfo = getPercentageRange(targets)
return RoundedListWithDividersItemData(
id = R.string.staking_details_annual_percentage_rate,
startText = getRateStartText(rateRangeInfo.first),
endText = rateRangeInfo.second,
iconClick = {
when (rateRangeInfo.first) {
Yield.RewardType.APR -> clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_RATE)
Yield.RewardType.APY -> clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_YIELD)
Yield.RewardType.UNKNOWN -> {}
RewardType.APR -> clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_RATE)
RewardType.APY -> clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_YIELD)
RewardType.UNKNOWN -> {}
}
},
isEndTextHighlighted = false,
)
}
private fun getRateStartText(rewardType: Yield.RewardType): TextReference {
private fun getRateStartText(rewardType: RewardType): TextReference {
return when (rewardType) {
Yield.RewardType.APR -> TextReference.Res(R.string.staking_details_annual_percentage_rate)
Yield.RewardType.APY -> TextReference.Res(R.string.staking_details_annual_percentage_yield)
RewardType.APR -> TextReference.Res(R.string.staking_details_annual_percentage_rate)
RewardType.APY -> TextReference.Res(R.string.staking_details_annual_percentage_yield)
else -> TextReference.EMPTY
}
}
@ -153,7 +156,7 @@ internal class SetInitialDataStateTransformer(
}
private fun createUnbondingPeriodItem(): RoundedListWithDividersItemData? {
val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days ?: return null
val cooldownPeriodDays = integration.cooldownPeriodDays ?: return null
return RoundedListWithDividersItemData(
id = R.string.staking_details_unbonding_period,
startText = TextReference.Res(R.string.staking_details_unbonding_period),
@ -169,7 +172,7 @@ internal class SetInitialDataStateTransformer(
private fun createMinimumRequirementItem(
cryptoCurrencyStatus: CryptoCurrencyStatus,
): RoundedListWithDividersItemData? {
val minimumCryptoAmount = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum ?: return null
val minimumCryptoAmount = integration.enterMinimumAmount ?: return null
val blockchainId = cryptoCurrencyStatus.currency.network.rawId
if (!showMinimumRequirementInfo(blockchainId)) return null
@ -183,7 +186,7 @@ internal class SetInitialDataStateTransformer(
}
private fun createRewardClaimingItem(): RoundedListWithDividersItemData? {
val rewardClaiming = yield.metadata.rewardClaiming
val rewardClaiming = integration.rewardClaiming
val endTextId = rewardClaimingResources[rewardClaiming] ?: return null
return RoundedListWithDividersItemData(
@ -195,7 +198,7 @@ internal class SetInitialDataStateTransformer(
}
private fun createWarmupPeriodItem(): RoundedListWithDividersItemData? {
val warmupPeriodDays = yield.metadata.warmupPeriod.days
val warmupPeriodDays = integration.warmupPeriodDays
if (warmupPeriodDays == 0) return null
return RoundedListWithDividersItemData(
@ -212,7 +215,7 @@ internal class SetInitialDataStateTransformer(
private fun createRewardScheduleItem(): RoundedListWithDividersItemData? {
val endTextReference = getRewardScheduleText(
rewardSchedule = yield.metadata.rewardSchedule,
rewardSchedule = integration.rewardSchedule,
networkId = cryptoCurrencyStatus.currency.network.rawId,
decapitalize = false,
) ?: return null
@ -252,15 +255,19 @@ internal class SetInitialDataStateTransformer(
)
}
private fun getPercentageRange(validators: List<Yield.Validator>): Pair<Yield.RewardType, TextReference> {
if (validators.isEmpty()) {
return Yield.RewardType.APR to stringReference(DASH_SIGN)
private fun getPercentageRange(targets: List<StakingTarget>): Pair<RewardType, TextReference> {
if (targets.isEmpty()) {
return RewardType.APR to stringReference(DASH_SIGN)
}
val rewardInfos = validators
.filter { it.preferred }
val rewardInfos = targets
.filter { it.isPreferred }
.takeIf { it.isNotEmpty() }
?.mapNotNull { it.rewardInfo }
?: validators.mapNotNull { it.rewardInfo }
?: targets.mapNotNull { it.rewardInfo }
if (rewardInfos.isEmpty()) {
return RewardType.APR to stringReference(DASH_SIGN)
}
val infoWithMinRate = rewardInfos.minBy { it.rate }
val infoWithMaxRate = rewardInfos.maxBy { it.rate }

View file

@ -4,7 +4,7 @@ import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
@ -13,7 +13,7 @@ internal class AmountChangeStateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val minimumTransactionAmount: EnterAmountBoundary?,
private val value: String,
private val yield: Yield,
private val integration: StakingIntegration,
) : Transformer<StakingUiState> {
private val maxEnterAmountConverter = MaxEnterAmountConverter()
@ -41,7 +41,7 @@ internal class AmountChangeStateTransformer(
amountState = AmountRequirementStateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxAmount = maxEnterAmount,
yield = yield,
integration = integration,
actionType = prevState.actionType,
).transform(updatedAmountState),
)

View file

@ -4,7 +4,7 @@ import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmountTransformer
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
@ -13,7 +13,7 @@ internal class AmountMaxValueStateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val minimumTransactionAmount: EnterAmountBoundary?,
private val actionType: StakingActionCommonType,
private val yield: Yield,
private val integration: StakingIntegration,
) : Transformer<StakingUiState> {
private val maxEnterAmountConverter = MaxEnterAmountConverter()
@ -38,7 +38,7 @@ internal class AmountMaxValueStateTransformer(
amountState = AmountRequirementStateTransformer(
maxAmount = maxEnterAmount,
cryptoCurrencyStatus = cryptoCurrencyStatus,
yield = yield,
integration = integration,
actionType = prevState.actionType,
).transform(updatedAmountState),
)

View file

@ -12,6 +12,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.AddressArgument
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
@ -26,7 +27,7 @@ import java.math.RoundingMode
internal class AmountRequirementStateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val maxAmount: EnterAmountBoundary,
private val yield: Yield,
private val integration: StakingIntegration,
private val actionType: StakingActionCommonType,
) : Transformer<AmountState> {
override fun transform(prevState: AmountState): AmountState {
@ -96,11 +97,11 @@ internal class AmountRequirementStateTransformer(
return when (actionType) {
is StakingActionCommonType.Enter -> {
val enterRequirements = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]
val enterRequirements = integration.enterArgs?.args?.get(Yield.Args.ArgType.AMOUNT)
enterRequirements?.getError(amountDecimal, R.string.staking_amount_requirement_error)
}
is StakingActionCommonType.Exit -> {
val exitRequirements = yield.args.exit?.args?.get(Yield.Args.ArgType.AMOUNT)
val exitRequirements = integration.exitArgs?.args?.get(Yield.Args.ArgType.AMOUNT)
exitRequirements?.getError(amountDecimal, R.string.staking_unstake_amount_requirement_error)
}
else -> null

View file

@ -37,7 +37,7 @@ internal class ShowApprovalBottomSheetTransformer(
val fee = feeState.fee ?: return prevState
val walletAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value.orEmpty()
val validatorAddress = validatorState.chosenValidator.address
val targetAddress = validatorState.chosenTarget.address
val feeCryptoValue = fee.amount.value.format {
crypto(fee.amount.currencySymbol, fee.amount.decimals)
}
@ -57,7 +57,7 @@ internal class ShowApprovalBottomSheetTransformer(
amount = amountState.amountTextField.value,
approveType = ApproveType.UNLIMITED,
walletAddress = walletAddress,
spenderAddress = validatorAddress,
spenderAddress = targetAddress,
fee = resourceReference(
R.string.common_crypto_fiat_format,
wrappedList(feeCryptoValue, feeFiatValue),

View file

@ -15,9 +15,9 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.StakingErrors
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
@ -47,12 +47,12 @@ internal class AddStakingNotificationsTransformer(
private val stakingError: StakingError?,
private val currencyCheck: CryptoCurrencyCheck,
private val isSubtractAvailable: Boolean,
private val yield: Yield,
private val integration: StakingIntegration,
) : Transformer<StakingUiState> {
private val stakingInfoNotificationsFactory = StakingInfoNotificationsFactory(
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
yield = yield,
integration = integration,
isSubtractAvailable = isSubtractAvailable,
)
@ -77,7 +77,7 @@ internal class AddStakingNotificationsTransformer(
isSubtractAvailable = isSubtractAvailable,
reduceAmountBy = reduceAmountBy,
)
val minimumRequirement = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum.orZero()
val minimumRequirement = integration.enterMinimumAmount.orZero()
val sendingAmount = if (isEnterAction) {
checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isSubtractAvailable,

View file

@ -8,6 +8,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.staking.action.StakingActionType
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.R
@ -26,7 +27,7 @@ import java.math.BigDecimal
internal class StakingInfoNotificationsFactory(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val yield: Yield,
private val integration: StakingIntegration,
private val isSubtractAvailable: Boolean,
) {
@ -94,7 +95,7 @@ internal class StakingInfoNotificationsFactory(
resourceReference(R.string.staking_notification_withdraw_text)
}
StakingActionType.UNLOCK_LOCKED -> {
val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days
val cooldownPeriodDays = integration.cooldownPeriodDays
if (cooldownPeriodDays != null) {
resourceReference(R.string.staking_unlocked_locked) to resourceReference(
R.string.staking_notification_unlock_text,
@ -213,7 +214,7 @@ internal class StakingInfoNotificationsFactory(
if (prevState.actionType !is StakingActionCommonType.Exit) return
val maxAmount = prevState.balanceState?.cryptoAmount ?: return
val exitRequirements = yield.args.exit?.args?.get(Yield.Args.ArgType.AMOUNT) ?: return
val exitRequirements = integration.exitArgs?.args?.get(Yield.Args.ArgType.AMOUNT) ?: return
val amountLeft = maxAmount - actionAmount
val isNotEnoughLeft = !amountLeft.isZero() && amountLeft < exitRequirements.minimum.orZero()
@ -224,7 +225,7 @@ internal class StakingInfoNotificationsFactory(
}
private fun MutableList<NotificationUM>.addUnstakeInfoNotification() {
val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days
val cooldownPeriodDays = integration.cooldownPeriodDays
val cryptoCurrencyNetworkIdValue = cryptoCurrencyStatusProvider().currency.network.rawId
if (cooldownPeriodDays != null) {
@ -248,18 +249,17 @@ internal class StakingInfoNotificationsFactory(
val initialInfoState = prevState.initialInfoState as? StakingStates.InitialInfoState.Data
val stakingBalances = (initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data)?.balances
val validatorAddress = prevState.balanceState?.validator?.address ?: return
val targetAddress = prevState.balanceState?.target?.address ?: return
val stakesCountWithCertainValidator = stakingBalances.orEmpty()
.filter {
it.type == BalanceType.STAKED ||
it.type == BalanceType.PREPARING ||
it.type == BalanceType.UNSTAKED
val stakesCountWithCertainTarget = stakingBalances.orEmpty()
.count { state ->
(state.type == BalanceType.STAKED ||
state.type == BalanceType.PREPARING ||
state.type == BalanceType.UNSTAKED) &&
state.target?.address == targetAddress
}
.filter { it.validator?.address == validatorAddress }
.size
if (stakesCountWithCertainValidator > 1) {
if (stakesCountWithCertainTarget > 1) {
add(
StakingNotification.Info.Ordinary(
title = resourceReference(R.string.staking_notification_ton_have_to_unstake_all_title),

View file

@ -1,6 +1,7 @@
package com.tangem.features.staking.impl.presentation.state.transformers.validator
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.models.staking.action.StakingActionType
import com.tangem.features.staking.impl.presentation.state.StakingStates
@ -9,8 +10,8 @@ import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
internal class ValidatorSelectChangeTransformer(
private val yield: Yield,
private val selectedValidator: Yield.Validator?,
private val integration: StakingIntegration,
private val selectedTarget: StakingTarget?,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
@ -23,27 +24,27 @@ internal class ValidatorSelectChangeTransformer(
val isFromInfoScreen = prevState.currentStep == StakingStep.InitialInfo
val isVoteLocked = confirmationState?.pendingAction?.type == StakingActionType.VOTE_LOCKED
val activeValidator = selectedValidator.takeIf { isFromInfoScreen && isRestake }
?: validatorState?.activeValidator
val filteredValidators = yield.preferredValidators.filterNot { it == activeValidator }
val activeTarget = selectedTarget.takeIf { isFromInfoScreen && isRestake }
?: validatorState?.activeTarget
val filteredTargets = integration.preferredTargets.filterNot { it == activeTarget }
val selectedValidator = if (isRestake && isFromInfoScreen) {
filteredValidators.firstOrNull()
val selectedTarget = if (isRestake && isFromInfoScreen) {
filteredTargets.firstOrNull()
} else {
selectedValidator
selectedTarget
}
if (selectedValidator == null && yield.preferredValidators.isEmpty()) {
if (selectedTarget == null && integration.preferredTargets.isEmpty()) {
return prevState
}
return prevState.copy(
validatorState = StakingStates.ValidatorState.Data(
chosenValidator = selectedValidator ?: yield.preferredValidators.first(),
availableValidators = filteredValidators,
chosenTarget = selectedTarget ?: integration.preferredTargets.first(),
availableTargets = filteredTargets,
isPrimaryButtonEnabled = true,
isClickable = yield.preferredValidators.size > 1,
activeValidator = activeValidator,
isClickable = integration.preferredTargets.size > 1,
activeTarget = activeTarget,
isVisibleOnConfirmation = isEnter || isRestake || isVoteLocked,
),
)

View file

@ -1,6 +1,7 @@
package com.tangem.features.staking.impl.presentation.state.utils
import com.tangem.core.ui.extensions.*
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.utils.StakingRewardSchedule.COSMOS_SCHEDULE
@ -52,18 +53,18 @@ internal fun getRewardScheduleText(
}
}
internal fun getRewardTypeShortText(rewardType: Yield.RewardType): TextReference {
internal fun getRewardTypeShortText(rewardType: RewardType): TextReference {
return when (rewardType) {
Yield.RewardType.APR -> TextReference.Res(R.string.staking_details_apr)
Yield.RewardType.APY -> TextReference.Res(R.string.staking_details_apy)
RewardType.APR -> TextReference.Res(R.string.staking_details_apr)
RewardType.APY -> TextReference.Res(R.string.staking_details_apy)
else -> TextReference.EMPTY
}
}
internal fun getRewardTypeLongText(rewardType: Yield.RewardType): TextReference {
internal fun getRewardTypeLongText(rewardType: RewardType): TextReference {
return when (rewardType) {
Yield.RewardType.APR -> TextReference.Res(R.string.staking_details_annual_percentage_rate)
Yield.RewardType.APY -> TextReference.Res(R.string.staking_details_annual_percentage_yield)
RewardType.APR -> TextReference.Res(R.string.staking_details_annual_percentage_rate)
RewardType.APY -> TextReference.Res(R.string.staking_details_annual_percentage_yield)
else -> TextReference.EMPTY
}
}

View file

@ -15,7 +15,7 @@ import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.StakingStates
@ -30,7 +30,7 @@ internal fun StakingClaimRewardsValidatorContent(
) {
if (state !is StakingStates.RewardsValidatorsState.Data) return
Column(
modifier = Modifier // Do not put fillMaxSize() in here
modifier = modifier // This function shouldn't itself apply fillMaxSize(); callers should avoid passing it here
.background(TangemTheme.colors.background.secondary)
.padding(horizontal = TangemTheme.dimens.spacing12)
.verticalScroll(rememberScrollState()),
@ -42,9 +42,9 @@ internal fun StakingClaimRewardsValidatorContent(
caption = item.getAprTextNeutral(),
infoTitle = item.formattedFiatAmount,
infoSubtitle = item.formattedCryptoAmount,
imageUrl = item.validator?.image.orEmpty(),
imageUrl = item.target?.image.orEmpty(),
onImageError = { ValidatorImagePlaceholder() },
modifier = modifier
modifier = Modifier
.roundedShapeItemDecoration(index, state.rewards.lastIndex, false)
.background(TangemTheme.colors.background.action)
.clickable(
@ -65,11 +65,11 @@ internal fun StakingClaimRewardsValidatorContent(
@Suppress("UnusedPrivateMember")
@Composable
private fun BalanceState.getAprTextColored() = combinedReference(
getRewardTypeShortText(validator?.rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
getRewardTypeShortText(target?.rewardInfo?.type ?: RewardType.UNKNOWN),
annotatedReference {
appendSpace()
appendColored(
text = validator?.rewardInfo?.rate?.orZero().format { percent() },
text = target?.rewardInfo?.rate?.orZero().format { percent() },
color = TangemTheme.colors.text.accent,
)
},
@ -77,6 +77,6 @@ private fun BalanceState.getAprTextColored() = combinedReference(
@Composable
private fun BalanceState.getAprTextNeutral() = combinedReference(
getRewardTypeShortText(validator?.rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
stringReference(" " + validator?.rewardInfo?.rate?.orZero().format { percent() }),
getRewardTypeShortText(target?.rewardInfo?.type ?: RewardType.UNKNOWN),
stringReference(" " + target?.rewardInfo?.rate?.orZero().format { percent() }),
)

View file

@ -46,7 +46,7 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.StakingDetailsScreenTestTags
import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.RewardBlockType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
import com.tangem.features.staking.impl.presentation.state.BalanceState
@ -317,11 +317,11 @@ private fun StakeButtonBlock(buttonState: NavigationButtonsState) {
@Suppress("UnusedPrivateMember")
@Composable
private fun BalanceState.getAprTextColored() = combinedReference(
getRewardTypeShortText(validator?.rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
getRewardTypeShortText(target?.rewardInfo?.type ?: RewardType.UNKNOWN),
annotatedReference {
appendSpace()
appendColored(
text = validator?.rewardInfo?.rate?.orZero().format { percent() },
text = target?.rewardInfo?.rate?.orZero().format { percent() },
color = TangemTheme.colors.text.accent,
)
},
@ -329,8 +329,8 @@ private fun BalanceState.getAprTextColored() = combinedReference(
@Composable
private fun BalanceState.getAprTextNeutral() = combinedReference(
getRewardTypeShortText(validator?.rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
stringReference(" " + validator?.rewardInfo?.rate?.orZero().format { percent() }),
getRewardTypeShortText(target?.rewardInfo?.type ?: RewardType.UNKNOWN),
stringReference(" " + target?.rewardInfo?.rate?.orZero().format { percent() }),
)
@Composable
@ -347,7 +347,7 @@ private fun BalanceState.getImage() = when (type) {
BalanceType.UNSTAKED,
BalanceType.LOCKED,
-> null
else -> validator?.image
else -> target?.image
}
private val textGradientColors = listOf(

Some files were not shown because too many files have changed in this diff Show more