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 @Singleton
fun provideFetchStakingOptionsUseCase( fun provideFetchStakingOptionsUseCase(
stakeKitRepository: StakeKitRepository, stakeKitRepository: StakeKitRepository,
p2pRepository: P2PEthPoolRepository, p2pEthPoolRepository: P2PEthPoolRepository,
stakingErrorResolver: StakingErrorResolver, stakingErrorResolver: StakingErrorResolver,
): FetchStakingOptionsUseCase { ): FetchStakingOptionsUseCase {
return FetchStakingOptionsUseCase( return FetchStakingOptionsUseCase(
stakeKitRepository = stakeKitRepository, stakeKitRepository = stakeKitRepository,
p2pRepository = p2pRepository, p2pEthPoolRepository = p2pEthPoolRepository,
stakingErrorResolver = stakingErrorResolver, stakingErrorResolver = stakingErrorResolver,
) )
} }
@ -240,8 +240,9 @@ internal object StakingDomainModule {
@Singleton @Singleton
fun provideStakingApyFlowUseCase( fun provideStakingApyFlowUseCase(
stakeKitRepository: StakeKitRepository, stakeKitRepository: StakeKitRepository,
p2pEthPoolRepository: P2PEthPoolRepository,
stakingFeatureToggles: StakingFeatureToggles, stakingFeatureToggles: StakingFeatureToggles,
): StakingApyFlowUseCase { ): StakingApyFlowUseCase {
return StakingApyFlowUseCase(stakeKitRepository, stakingFeatureToggles) return StakingApyFlowUseCase(stakeKitRepository, p2pEthPoolRepository, stakingFeatureToggles)
} }
} }

View file

@ -1,7 +1,7 @@
package com.tangem.tap.network.auth package com.tangem.tap.network.auth
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage 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 import com.tangem.lib.auth.P2PEthPoolAuthProvider
internal class DefaultP2PEthPoolAuthProvider( internal class DefaultP2PEthPoolAuthProvider(
@ -12,6 +12,6 @@ internal class DefaultP2PEthPoolAuthProvider(
val keys = environmentConfigStorage.getConfigSync().p2pApiKey val keys = environmentConfigStorage.getConfigSync().p2pApiKey
?: error("No P2P api keys provided") ?: 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( params = StakingComponent.Params(
userWalletId = route.userWalletId, userWalletId = route.userWalletId,
cryptoCurrency = route.cryptoCurrency, cryptoCurrency = route.cryptoCurrency,
yieldId = route.yieldId, integrationId = route.integrationId,
), ),
componentFactory = stakingComponentFactory, componentFactory = stakingComponentFactory,
) )

View file

@ -19,7 +19,7 @@ dependencies {
implementation(projects.domain.models) implementation(projects.domain.models)
implementation(projects.domain.tokens.models) implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models) implementation(projects.domain.wallets.models)
implementation(projects.domain.staking.models) implementation(projects.domain.staking)
implementation(projects.domain.markets.models) implementation(projects.domain.markets.models)
implementation(projects.domain.onramp.models) implementation(projects.domain.onramp.models)
implementation(projects.domain.appCurrency.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.RouteBundleParams
import com.tangem.common.routing.bundle.bundle import com.tangem.common.routing.bundle.bundle
import com.tangem.common.routing.entity.InitScreenLaunchMode import com.tangem.common.routing.entity.InitScreenLaunchMode
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.core.decompose.navigation.Route import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.feedback.models.WalletMetaInfo import com.tangem.domain.feedback.models.WalletMetaInfo
@ -208,8 +209,8 @@ sealed class AppRoute(val path: String) : Route {
data class Staking( data class Staking(
val userWalletId: UserWalletId, val userWalletId: UserWalletId,
val cryptoCurrency: CryptoCurrency, val cryptoCurrency: CryptoCurrency,
val yieldId: String, val integrationId: StakingIntegrationID,
) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrency.id.value}/$yieldId") ) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrency.id.value}/${integrationId.value}")
@Serializable @Serializable
data class PushNotification( data class PushNotification(

View file

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

View file

@ -1,7 +1,7 @@
package com.tangem.datasource.api.common.config package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig 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.lib.auth.P2PEthPoolAuthProvider
import com.tangem.utils.ProviderSuspend import com.tangem.utils.ProviderSuspend
@ -23,7 +23,7 @@ internal class P2PEthPool(
private fun getInitialEnvironment(): ApiEnvironment { private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) { return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK 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) @JsonClass(generateAdapter = false)
enum class P2PEthPoolTxStatusDTO { enum class P2PEthPoolTxStatusDTO {

View file

@ -4,9 +4,9 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass 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) @JsonClass(generateAdapter = true)
data class P2PEthPoolErrorResponse( data class P2PEthPoolErrorResponse(

View file

@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass
/** /**
* Unified response wrapper for all P2P.org API responses * 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 * ```json
* { * {
* "error": null | { code, message, name, errors }, * "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) @JsonClass(generateAdapter = false)
enum class P2PEthPoolNetworkDTO { enum class P2PEthPoolNetworkDTO {

View file

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

View file

@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow
* (similar to StakingYieldsStore for StakeKit yields) * (similar to StakingYieldsStore for StakeKit yields)
* *
* Vault is ETH-specific concept for pooled staking. * 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 { interface P2PEthPoolVaultsStore {
@ -23,7 +23,7 @@ interface P2PEthPoolVaultsStore {
suspend fun getSync(): List<P2PEthPoolVault> suspend fun getSync(): List<P2PEthPoolVault>
/** /**
* Store vaults from P2P API * Store vaults from P2PEthPool API
*/ */
suspend fun store(vaults: List<P2PEthPoolVault>) 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.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.BLOCK_AID_API_KEY
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_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.ExpressAuthProvider
import com.tangem.lib.auth.P2PEthPoolAuthProvider import com.tangem.lib.auth.P2PEthPoolAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider
@ -300,7 +300,7 @@ internal class ProdApiConfigsManagerTest {
} }
private fun createP2PModel(): TestModel { 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/" ApiEnvironment.DEV to "https://api-test.p2p.org/"
} else { } else {
ApiEnvironment.PROD to "https://api.p2p.org/" ApiEnvironment.PROD to "https://api.p2p.org/"

View file

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

View file

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

View file

@ -29,12 +29,12 @@ internal class DefaultStakingErrorResolver(
is StakingError.DomainError -> { is StakingError.DomainError -> {
analyticsEventHandler.send(StakingAnalyticsEvent.DomainError(error)) analyticsEventHandler.send(StakingAnalyticsEvent.DomainError(error))
} }
// P2P errors // P2PEthPool errors
is StakingError.InvalidAmount, is StakingError.InvalidAmount,
is StakingError.DataError, is StakingError.DataError,
is StakingError.UnknownError, 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 stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id)
val availabilityFlow = when (stakingIntegration) { val availabilityFlow = when (stakingIntegration) {
is StakingIntegrationID.P2P -> p2pEthPoolRepository.getStakingAvailability() StakingIntegrationID.P2PEthPool -> p2pEthPoolRepository.getStakingAvailability()
is StakingIntegrationID.StakeKit -> stakeKitRepository.getStakingAvailability( is StakingIntegrationID.StakeKit -> stakeKitRepository.getStakingAvailability(
stakingIntegration,
rawCurrencyId, rawCurrencyId,
cryptoCurrency.symbol, cryptoCurrency.symbol,
) )
@ -94,8 +95,9 @@ internal class DefaultStakingRepository(
?: return StakingAvailability.Unavailable ?: return StakingAvailability.Unavailable
return when (stakingIntegration) { return when (stakingIntegration) {
is StakingIntegrationID.P2P -> p2pEthPoolRepository.getStakingAvailabilitySync() StakingIntegrationID.P2PEthPool -> p2pEthPoolRepository.getStakingAvailabilitySync()
is StakingIntegrationID.StakeKit -> stakeKitRepository.getStakingAvailabilitySync( is StakingIntegrationID.StakeKit -> stakeKitRepository.getStakingAvailabilitySync(
stakingIntegration,
rawCurrencyId, rawCurrencyId,
cryptoCurrency.symbol, 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.MetadataDTO.RewardScheduleDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.ValidatorDTO.ValidatorStatusDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.ValidatorDTO.ValidatorStatusDTO
import com.tangem.datasource.local.token.converter.YieldTokenConverter 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.AddressArgument
import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.Yield.Metadata.RewardSchedule 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") val address = validatorDTO.address.asMandatory("address")
return Yield.Validator( 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 aprOrApy = validatorDTO.apr
val commission = validatorDTO.commission val commission = validatorDTO.commission
// gross = net / (1 - commission) // gross = net / (1 - commission)
@ -162,17 +164,17 @@ internal object YieldConverter : Converter<YieldDTO, Yield> {
} else { } else {
netApy netApy
} }
grossAprOrApy?.let { Yield.RewardInfo(rate = it, type = rewardType) } grossAprOrApy?.let { RewardInfo(rate = it, type = rewardType) }
} catch (_: Exception) { } 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) { return when (rewardTypeDTO) {
YieldDTO.RewardTypeDTO.APY -> Yield.RewardType.APY YieldDTO.RewardTypeDTO.APY -> RewardType.APY
YieldDTO.RewardTypeDTO.APR -> Yield.RewardType.APR YieldDTO.RewardTypeDTO.APR -> RewardType.APR
else -> Yield.RewardType.UNKNOWN else -> RewardType.UNKNOWN
} }
} }

View file

@ -9,7 +9,7 @@ import com.tangem.utils.converter.Converter
import org.joda.time.Instant 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> { internal object P2PEthPoolAccountConverter : Converter<P2PEthPoolAccountResponse, P2PEthPoolAccount> {

View file

@ -8,7 +8,7 @@ import com.tangem.utils.converter.Converter
import java.math.BigDecimal 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> { 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 import com.tangem.utils.converter.Converter
/** /**
* Converter from P2P Error Response to Domain StakingError * Converter from P2PEthPool Error Response to Domain StakingError
*/ */
@Suppress("MagicNumber") @Suppress("MagicNumber")
internal object P2PEthPoolErrorConverter : Converter<P2PEthPoolErrorResponse, StakingError> { 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 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> { 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 com.tangem.domain.staking.model.StakingIntegrationID
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
/** Converts P2P ETH Pool API response to [StakingBalance.Data.P2P] */ /** Converts P2PEthPool API response to [StakingBalance.Data.P2PEthPool] */
internal object P2PStakingBalanceConverter { internal object P2PEthPoolStakingBalanceConverter {
fun convert(response: P2PEthPoolAccountResponse, source: StatusSource): StakingBalance.Data.P2P { fun convert(response: P2PEthPoolAccountResponse, source: StatusSource): StakingBalance.Data.P2PEthPool {
val stakingId = StakingID( val stakingId = StakingID(
integrationId = StakingIntegrationID.P2P.EthereumPooled.value, integrationId = StakingIntegrationID.P2PEthPool.value,
address = response.delegatorAddress, address = response.delegatorAddress,
) )
val account = P2PStakingAccount( val account = P2PEthPoolStakingAccount(
delegatorAddress = response.delegatorAddress, delegatorAddress = response.delegatorAddress,
vaultAddress = response.vaultAddress, vaultAddress = response.vaultAddress,
stake = convertStake(response.stake), stake = convertStake(response.stake),
@ -27,29 +27,29 @@ internal object P2PStakingBalanceConverter {
exitQueue = convertExitQueue(response.exitQueue), exitQueue = convertExitQueue(response.exitQueue),
) )
return StakingBalance.Data.P2P( return StakingBalance.Data.P2PEthPool(
stakingId = stakingId, stakingId = stakingId,
source = source, source = source,
account = account, account = account,
) )
} }
private fun convertStake(dto: P2PEthPoolStakeDTO): P2PStake { private fun convertStake(dto: P2PEthPoolStakeDTO): P2PEthPoolStake {
return P2PStake( return P2PEthPoolStake(
assets = dto.assets, assets = dto.assets,
totalEarnedAssets = dto.totalEarnedAssets, totalEarnedAssets = dto.totalEarnedAssets,
) )
} }
private fun convertExitQueue(dto: P2PEthPoolExitQueueDTO): P2PExitQueue { private fun convertExitQueue(dto: P2PEthPoolExitQueueDTO): P2PEthPoolExitQueue {
return P2PExitQueue( return P2PEthPoolExitQueue(
total = dto.total.toBigDecimal(), total = dto.total.toBigDecimal(),
requests = dto.requests.map(::convertExitRequest), requests = dto.requests.map(::convertExitRequest),
) )
} }
private fun convertExitRequest(dto: P2PEthPoolExitRequestDTO): P2PExitRequest { private fun convertExitRequest(dto: P2PEthPoolExitRequestDTO): P2PEthPoolExitRequest {
return P2PExitRequest( return P2PEthPoolExitRequest(
ticket = dto.ticket, ticket = dto.ticket,
totalAssets = dto.totalAssets.toBigDecimal(), totalAssets = dto.totalAssets.toBigDecimal(),
timestamp = Instant.fromEpochSeconds(dto.timestamp), timestamp = Instant.fromEpochSeconds(dto.timestamp),

View file

@ -6,7 +6,7 @@ import com.tangem.utils.converter.Converter
import java.math.BigDecimal 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> { 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 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> { 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 package com.tangem.data.staking.di
import androidx.datastore.core.DataStore 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.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.data.staking.store.StakingBalancesStore
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
@ -38,11 +38,11 @@ internal object StakingBalanceSupplierModule {
@Provides @Provides
@Singleton @Singleton
fun provideP2PBalancesStore( fun provideP2PEthPoolBalancesStore(
persistenceStore: DataStore<Map<String, Set<P2PEthPoolAccountResponse>>>, persistenceStore: DataStore<Map<String, Set<P2PEthPoolAccountResponse>>>,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
): P2PBalancesStore { ): P2PEthPoolBalancesStore {
return DefaultP2PBalancesStore( return DefaultP2PEthPoolBalancesStore(
runtimeStore = RuntimeSharedStore(), runtimeStore = RuntimeSharedStore(),
persistenceStore = persistenceStore, persistenceStore = persistenceStore,
dispatchers = dispatchers, dispatchers = dispatchers,

View file

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

View file

@ -5,7 +5,7 @@ import arrow.core.left
import arrow.core.right import arrow.core.right
import arrow.core.toOption import arrow.core.toOption
import com.tangem.data.common.api.safeApiCall 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.store.StakingBalancesStore
import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory
import com.tangem.datasource.api.common.response.ApiResponse 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.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.staking.model.StakingIntegrationID 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.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.coroutines.runSuspendCatching
@ -38,16 +39,16 @@ import javax.inject.Inject
/** /**
* Default implementation of [MultiStakingBalanceFetcher] * Default implementation of [MultiStakingBalanceFetcher]
* *
* Supports both StakeKit and P2P staking providers. * Supports both StakeKit and P2PEthPool staking providers.
* *
* @property userWalletsStore user wallets store * @property userWalletsStore user wallets store
* @property stakingYieldsStore staking yields store * @property stakingYieldsStore staking yields store
* @property stakingBalancesStore staking balances store (StakeKit) * @property stakingBalancesStore staking balances store (StakeKit)
* @property p2pBalancesStore P2P balances store * @property p2PEthPoolBalancesStore P2PEthPool balances store
* @property stakeKitApi stake kit API * @property stakeKitApi stake kit API
* @property p2pApi P2P ETH Pool API * @property p2pEthPoolApi P2PEthPool API
* @property p2pVaultsStore P2P vaults store * @property p2pEthPoolVaultsStore P2PEthPool vaults store
* @property dispatchers dispatchers * @property dispatchers dispatchers
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
@ -56,10 +57,10 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
private val userWalletsStore: UserWalletsStore, private val userWalletsStore: UserWalletsStore,
private val stakingYieldsStore: StakingYieldsStore, private val stakingYieldsStore: StakingYieldsStore,
private val stakingBalancesStore: StakingBalancesStore, private val stakingBalancesStore: StakingBalancesStore,
private val p2pBalancesStore: P2PBalancesStore, private val p2PEthPoolBalancesStore: P2PEthPoolBalancesStore,
private val stakeKitApi: StakeKitApi, private val stakeKitApi: StakeKitApi,
private val p2pApi: P2PEthPoolApi, private val p2pEthPoolApi: P2PEthPoolApi,
private val p2pVaultsStore: P2PEthPoolVaultsStore, private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
) : MultiStakingBalanceFetcher { ) : MultiStakingBalanceFetcher {
@ -75,7 +76,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
return it.left() return it.left()
} }
val (stakeKitIds, p2pIds) = stakingIds.partition { stakingId -> val (stakeKitIds, p2pEthPoolIds) = stakingIds.partition { stakingId ->
val stakingIntegrationID = StakingIntegrationID.entries.find { val stakingIntegrationID = StakingIntegrationID.entries.find {
it.value == stakingId.integrationId it.value == stakingId.integrationId
} }
@ -86,7 +87,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
""" """
Staking IDs to fetch: Staking IDs to fetch:
- StakeKit: ${stakeKitIds.joinToString()} - StakeKit: ${stakeKitIds.joinToString()}
- P2P: ${p2pIds.joinToString()} - P2PEthPool: ${p2pEthPoolIds.joinToString()}
""".trimIndent(), """.trimIndent(),
) )
@ -96,8 +97,8 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
launch { fetchStakeKitBalances(params.userWalletId, stakeKitIds.toSet()) } launch { fetchStakeKitBalances(params.userWalletId, stakeKitIds.toSet()) }
} }
if (p2pIds.isNotEmpty()) { if (p2pEthPoolIds.isNotEmpty()) {
launch { fetchP2PBalances(params.userWalletId, p2pIds.toSet()) } launch { fetchP2PBalances(params.userWalletId, p2pEthPoolIds.toSet()) }
} }
} }
} }
@ -110,8 +111,11 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
stakingIds = stakeKitIds.toSet(), stakingIds = stakeKitIds.toSet(),
) )
} }
if (p2pIds.isNotEmpty()) { if (p2pEthPoolIds.isNotEmpty()) {
p2pBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = p2pIds.toSet()) 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>) { 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()) { if (vaults.isEmpty()) {
Timber.w("No P2P vaults available for $userWalletId") Timber.w("No P2PEthPool vaults available for $userWalletId")
p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds) p2PEthPoolBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
return return
} }
@ -143,59 +147,17 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
private suspend fun fetchFromP2P( private suspend fun fetchFromP2P(
userWalletId: UserWalletId, userWalletId: UserWalletId,
stakingIds: Set<StakingID>, stakingIds: Set<StakingID>,
vaults: List<com.tangem.domain.staking.model.ethpool.P2PEthPoolVault>, vaults: List<P2PEthPoolVault>,
) { ) {
safeApiCall( safeApiCall(
call = { call = {
val addresses = stakingIds.map { it.address }.toSet() val addresses = stakingIds.map { it.address }.toSet()
val responses = fetchP2PAccountResponses(vaults = vaults, addresses = addresses)
val responses = mutableSetOf<P2PEthPoolAccountResponse>() Timber.i("Successfully fetched ${responses.size} P2PEthPool balances for $userWalletId")
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")
if (responses.isNotEmpty()) { if (responses.isNotEmpty()) {
p2pBalancesStore.storeActual(userWalletId = userWalletId, values = responses) p2PEthPoolBalancesStore.storeActual(userWalletId = userWalletId, values = responses)
val missingStakingIds = stakingIds.filter { stakingId -> val missingStakingIds = stakingIds.filter { stakingId ->
responses.none { response -> responses.none { response ->
@ -205,23 +167,76 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
if (missingStakingIds.isNotEmpty()) { if (missingStakingIds.isNotEmpty()) {
Timber.i("Missing responses for ${missingStakingIds.size} staking IDs: $missingStakingIds") 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 { } else {
Timber.i("No P2P responses received for $userWalletId") Timber.i("No P2PEthPool responses received for $userWalletId")
p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds) p2PEthPoolBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
} }
}, },
onError = { throwable -> 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 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) { private inline fun checkIsSupportedByWalletOrElse(userWalletId: UserWalletId, ifNotSupported: (Throwable) -> Unit) {
val maybeUserWallet = userWalletsStore.getSyncOrNull(key = userWalletId).toOption() 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.Option
import arrow.core.some 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.data.staking.store.StakingBalancesStore
import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.staking.multi.MultiStakingBalanceProducer import com.tangem.domain.staking.multi.MultiStakingBalanceProducer
@ -19,11 +19,11 @@ import kotlinx.coroutines.flow.onEmpty
/** /**
* Default implementation of [MultiStakingBalanceProducer] * 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 params params
* @property stakingBalancesStore StakeKit staking balances store * @property stakingBalancesStore StakeKit staking balances store
* @property p2pBalancesStore P2P balances store * @property p2PEthPoolBalancesStore P2PEthPool balances store
* @property dispatchers dispatchers * @property dispatchers dispatchers
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.onEmpty
internal class DefaultMultiStakingBalanceProducer @AssistedInject constructor( internal class DefaultMultiStakingBalanceProducer @AssistedInject constructor(
@Assisted val params: MultiStakingBalanceProducer.Params, @Assisted val params: MultiStakingBalanceProducer.Params,
private val stakingBalancesStore: StakingBalancesStore, private val stakingBalancesStore: StakingBalancesStore,
private val p2pBalancesStore: P2PBalancesStore, private val p2PEthPoolBalancesStore: P2PEthPoolBalancesStore,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
) : MultiStakingBalanceProducer { ) : MultiStakingBalanceProducer {
@ -39,10 +39,10 @@ internal class DefaultMultiStakingBalanceProducer @AssistedInject constructor(
override fun produce(): Flow<Set<StakingBalance>> { override fun produce(): Flow<Set<StakingBalance>> {
val stakeKitFlow = stakingBalancesStore.get(userWalletId = params.userWalletId) 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 -> return combine(stakeKitFlow, p2pEthPoolFlow) { stakeKitBalances, p2pEthPoolBalances ->
stakeKitBalances + p2pBalances stakeKitBalances + p2pEthPoolBalances
} }
.distinctUntilChanged() .distinctUntilChanged()
.onEmpty { emit(value = hashSetOf()) } .onEmpty { emit(value = hashSetOf()) }

View file

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

View file

@ -7,9 +7,9 @@ import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow 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>> fun get(userWalletId: UserWalletId): Flow<Set<StakingBalance>>

View file

@ -1,6 +1,6 @@
package com.tangem.data.staking 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.ethpool.models.response.P2PEthPoolAccountResponse
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.local.token.converter.StakingBalanceConverter 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)!! return StakingBalanceConverter(isCached = source == StatusSource.CACHE).convert(this)!!
} }
internal fun P2PEthPoolAccountResponse.toDomain(source: StatusSource = StatusSource.CACHE): StakingBalance.Data.P2P { internal fun P2PEthPoolAccountResponse.toDomain(
return P2PStakingBalanceConverter.convert( source: StatusSource = StatusSource.CACHE,
): StakingBalance.Data.P2PEthPool {
return P2PEthPoolStakingBalanceConverter.convert(
response = this, response = this,
source = source, 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.MockYieldBalanceWrapperDTOFactory
import com.tangem.common.test.data.staking.MockYieldDTOFactory import com.tangem.common.test.data.staking.MockYieldDTOFactory
import com.tangem.common.test.domain.wallet.MockUserWalletFactory 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.store.StakingBalancesStore
import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory
import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponse
@ -36,19 +36,19 @@ internal class DefaultMultiStakingBalanceFetcherTest {
private val userWalletsStore: UserWalletsStore = mockk() private val userWalletsStore: UserWalletsStore = mockk()
private val stakingYieldsStore: StakingYieldsStore = mockk() private val stakingYieldsStore: StakingYieldsStore = mockk()
private val stakingBalancesStore: StakingBalancesStore = mockk(relaxUnitFun = true) 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 stakeKitApi: StakeKitApi = mockk()
private val p2pApi: P2PEthPoolApi = mockk() private val p2pEthPoolApi: P2PEthPoolApi = mockk()
private val p2pVaultsStore: P2PEthPoolVaultsStore = mockk() private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore = mockk()
private val fetcher = DefaultMultiStakingBalanceFetcher( private val fetcher = DefaultMultiStakingBalanceFetcher(
userWalletsStore = userWalletsStore, userWalletsStore = userWalletsStore,
stakingYieldsStore = stakingYieldsStore, stakingYieldsStore = stakingYieldsStore,
stakingBalancesStore = stakingBalancesStore, stakingBalancesStore = stakingBalancesStore,
p2pBalancesStore = p2pBalancesStore, p2PEthPoolBalancesStore = p2PEthPoolBalancesStore,
stakeKitApi = stakeKitApi, stakeKitApi = stakeKitApi,
p2pApi = p2pApi, p2pEthPoolApi = p2pEthPoolApi,
p2pVaultsStore = p2pVaultsStore, p2pEthPoolVaultsStore = p2pEthPoolVaultsStore,
dispatchers = TestingCoroutineDispatcherProvider(), dispatchers = TestingCoroutineDispatcherProvider(),
) )

View file

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

View file

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

View file

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

View file

@ -12,4 +12,17 @@ data class YieldToken(
val coinGeckoId: String?, val coinGeckoId: String?,
val logoURI: String?, val logoURI: String?,
val isPoints: Boolean?, 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.tokens.models)
implementation(projects.domain.wallets.models) implementation(projects.domain.wallets.models)
implementation(projects.features.staking.api)
implementation(tangemDeps.blockchain) { implementation(tangemDeps.blockchain) {
exclude(module = "joda-time") 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 import org.joda.time.Instant
/** /**
* P2P.org account staking information * Account staking information
* Contains detailed balance and exit queue information * Contains detailed balance and exit queue information
*/ */
data class P2PEthPoolAccount( data class P2PEthPoolAccount(

View file

@ -22,7 +22,7 @@ data class P2PEthPoolAction(
) )
/** /**
* Types of P2P staking actions * Types of P2PEthPool staking actions
*/ */
@Serializable @Serializable
enum class P2PEthPoolActionType { enum class P2PEthPoolActionType {
@ -33,7 +33,7 @@ enum class P2PEthPoolActionType {
} }
/** /**
* Status of P2P staking action * Status of P2PEthPool staking action
*/ */
@Serializable @Serializable
enum class P2PEthPoolActionStatus { 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( data class P2PEthPoolStakingTransaction(
val id: String?, val id: String?,

View file

@ -4,7 +4,7 @@ import com.tangem.domain.models.serialization.SerializedBigDecimal
import org.joda.time.Instant 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 * Contains staked amounts, rewards, and pending actions
*/ */
data class P2PEthPoolStakingBalance( 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( data class P2PEthPoolBalanceItem(
val type: P2PEthPoolBalanceType, 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 { enum class P2PEthPoolBalanceType {
STAKED, 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 * @param chainId Ethereum chain ID
* @return true if supported, false otherwise * @return true if supported, false otherwise

View file

@ -4,7 +4,7 @@ import com.tangem.domain.models.serialization.SerializedBigDecimal
import org.joda.time.DateTime import org.joda.time.DateTime
/** /**
* P2P.org rewards history entry * Rewards history entry
* Historical reward information for account * Historical reward information for account
*/ */
data class P2PEthPoolReward( 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( data class P2PEthPoolVaultDetails(
val vaultAddress: String, val vaultAddress: String,

View file

@ -1,11 +1,11 @@
package com.tangem.domain.staking.model.ethpool 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. * Change [USE_TESTNET] to switch between testnet and mainnet.
*/ */
object P2PStakingConfig { object P2PEthPoolStakingConfig {
const val USE_TESTNET: Boolean = true 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 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 * Contains all necessary data for transaction signing
*/ */
data class P2PEthPoolUnsignedTx( 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.serialization.SerializedBigDecimal
import com.tangem.domain.models.staking.YieldToken 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 import kotlinx.serialization.Serializable
@Serializable @Serializable
@ -138,18 +140,6 @@ data class Yield(
UNKNOWN, UNKNOWN,
} }
} }
enum class RewardType {
APY, // compound rate
APR, // simple rate
UNKNOWN,
}
@Serializable
data class RewardInfo(
val rate: SerializedBigDecimal,
val type: RewardType,
)
} }
@Serializable @Serializable

View file

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

View file

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

View file

@ -1,19 +1,18 @@
package com.tangem.domain.staking.model package com.tangem.domain.staking.model
import com.tangem.domain.models.serialization.SerializedBigDecimal 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.models.staking.YieldToken
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.Yield
/** /**
* Represents a staking option from any provider * 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 { sealed interface StakingOption {
/** Unique identifier for the staking option */ /** Unique identifier for the staking option */
val integrationId: String val integrationId: StakingIntegrationID
/** Annual Percentage Yield */ /** Annual Percentage Yield */
val apy: SerializedBigDecimal val apy: SerializedBigDecimal
@ -28,34 +27,23 @@ sealed interface StakingOption {
* StakeKit staking option * StakeKit staking option
* Wraps StakeKit Yield with all validator and metadata information * Wraps StakeKit Yield with all validator and metadata information
*/ */
data class StakeKit(val yield: Yield) : StakingOption { data class StakeKit(
override val integrationId: String = yield.id override val integrationId: StakingIntegrationID.StakeKit,
val yield: Yield,
) : StakingOption {
override val apy: SerializedBigDecimal = yield.apy override val apy: SerializedBigDecimal = yield.apy
override val token: YieldToken = yield.token override val token: YieldToken = yield.token
override val isAvailable: Boolean = yield.isAvailable override val isAvailable: Boolean = yield.isAvailable
} }
/** /**
* P2P pooled staking option * P2PEthPool staking option
* Wraps P2P ETH Pool vault information * Wraps P2PEthPool vault information
*/ */
data class P2P(val vaults: List<P2PEthPoolVault>) : StakingOption { data class P2PEthPool(val vaults: List<P2PEthPoolVault>) : StakingOption {
override val integrationId: String = "p2p-ethereum-pooled" override val integrationId: StakingIntegrationID = StakingIntegrationID.P2PEthPool
override val apy: SerializedBigDecimal = vaults.maxOf { it.apy } 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() 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.P2PEthPoolReward
import com.tangem.domain.staking.model.ethpool.P2PEthPoolUnsignedTx import com.tangem.domain.staking.model.ethpool.P2PEthPoolUnsignedTx
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault 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 com.tangem.domain.staking.model.stakekit.StakingError
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -17,24 +17,24 @@ interface P2PEthPoolRepository {
/** /**
* Fetch and store available staking vaults * 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 * 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 * @return Either error or list of vaults with APY, capacity, fees
*/ */
suspend fun getVaults( suspend fun getVaults(
network: P2PEthPoolNetwork = P2PStakingConfig.activeNetwork, network: P2PEthPoolNetwork = P2PEthPoolStakingConfig.activeNetwork,
): Either<StakingError, List<P2PEthPoolVault>> ): Either<StakingError, List<P2PEthPoolVault>>
/** /**
* Create unsigned transaction for depositing ETH into a vault * 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 delegatorAddress User's wallet address
* @param vaultAddress Vault contract address * @param vaultAddress Vault contract address
* @param amount Amount of ETH to deposit * @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] * Unstaking adds funds to exit queue. After ~1-4 days, use [createWithdrawTransaction]
* to withdraw the funds. * 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 stakerPublicKey Staker's public key (note: API doc may have Bitcoin terminology)
* @param stakeTransactionHash Original stake transaction hash * @param stakeTransactionHash Original stake transaction hash
* @return Either error or unsigned transaction * @return Either error or unsigned transaction
@ -69,7 +69,7 @@ interface P2PEthPoolRepository {
* *
* Only works when funds are available (after exit queue wait period). * 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 * @param stakerAddress User's wallet address
* @return Either error or unsigned transaction with withdrawal tickets * @return Either error or unsigned transaction with withdrawal tickets
*/ */
@ -81,7 +81,7 @@ interface P2PEthPoolRepository {
/** /**
* Broadcast signed transaction to blockchain * 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) * @param signedTransaction Signed transaction in hex format (with 0x prefix)
* @return Either error or broadcast result with transaction hash * @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 * 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 delegatorAddress User's wallet address
* @param vaultAddress Vault contract address * @param vaultAddress Vault contract address
* @return Either error or account info * @return Either error or account info
@ -109,7 +109,7 @@ interface P2PEthPoolRepository {
/** /**
* Get rewards history for account and vault * 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 delegatorAddress User's wallet address
* @param vaultAddress Vault contract address * @param vaultAddress Vault contract address
* @param period Optional period filter in days (30, 60, or 90) * @param period Optional period filter in days (30, 60, or 90)
@ -123,17 +123,27 @@ interface P2PEthPoolRepository {
): Either<StakingError, List<P2PEthPoolReward>> ): 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 * TemporaryUnavailable if not found or vaults empty
*/ */
fun getStakingAvailability(): Flow<StakingAvailability> 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 * TemporaryUnavailable if not found or vaults empty
*/ */
suspend fun getStakingAvailabilitySync(): StakingAvailability 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.models.wallet.UserWalletId
import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo 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.models.staking.NetworkType
import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingAction
@ -24,9 +25,17 @@ interface StakeKitRepository {
fun getEnabledYields(): Flow<List<Yield>> 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 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.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toCoinId 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.repositories.StakeKitRepository
import com.tangem.domain.staking.toggles.StakingFeatureToggles import com.tangem.domain.staking.toggles.StakingFeatureToggles
import kotlinx.coroutines.flow.Flow 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: * Return map:
* - key: currency staking key (network.backendId + "_" + symbol) * - key: currency staking key (coinGeckoId + "_" + symbol)
* - value: validators * - value: list of staking targets (validators or vaults)
*/ */
class StakingApyFlowUseCase( class StakingApyFlowUseCase(
private val stakeKitRepository: StakeKitRepository, private val stakeKitRepository: StakeKitRepository,
private val p2pEthPoolRepository: P2PEthPoolRepository,
private val stakingFeatureToggles: StakingFeatureToggles, private val stakingFeatureToggles: StakingFeatureToggles,
) { ) {
operator fun invoke(): Flow<Map<String, List<Yield.Validator>>> { operator fun invoke(): Flow<Map<String, List<StakingTarget>>> {
return stakeKitRepository.getEnabledYields() return combine(
.map { yields -> stakeKitRepository.getEnabledYields(),
yields.filterNot { yield -> p2pEthPoolRepository.getVaultsFlow(),
val isCardanoYield = yield.token.coinGeckoId == Blockchain.Cardano.toCoinId() ) { yields, p2pVaults ->
isCardanoYield && !stakingFeatureToggles.isCardanoStakingEnabled val stakeKitMap = yields.filterNot { yield ->
}.associate { yield -> val isCardanoYield = yield.token.coinGeckoId == Blockchain.Cardano.toCoinId()
val key = "${yield.token.coinGeckoId}_${yield.token.symbol}" isCardanoYield && !stakingFeatureToggles.isCardanoStakingEnabled
val apy = yield.validators }.associate { yield ->
key to apy 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. * 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 * Returns sum of all staking-related balances including rewards
* (staked + unstaking + withdrawable + rewards). * (staked + unstaking + withdrawable + rewards).
@ -18,7 +18,7 @@ import java.math.BigDecimal
fun StakingBalance.Data.getTotalWithRewardsStakingBalance(blockchainId: String): BigDecimal { fun StakingBalance.Data.getTotalWithRewardsStakingBalance(blockchainId: String): BigDecimal {
return when (this) { return when (this) {
is StakingBalance.Data.StakeKit -> getTotalWithRewardsStakingBalanceStakeKit(blockchainId) is StakingBalance.Data.StakeKit -> getTotalWithRewardsStakingBalanceStakeKit(blockchainId)
is StakingBalance.Data.P2P -> { is StakingBalance.Data.P2PEthPool -> {
val rewards = totalRewards val rewards = totalRewards
if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId)) { if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId)) {
totalStaked + unstakingAmount + withdrawableAmount + rewards totalStaked + unstakingAmount + withdrawableAmount + rewards
@ -31,7 +31,7 @@ fun StakingBalance.Data.getTotalWithRewardsStakingBalance(blockchainId: String):
/** /**
* Provider-agnostic extension to get total staking balance excluding rewards. * 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) * Returns sum of all staking-related balances (staked + unstaking + withdrawable)
* excluding rewards. * excluding rewards.
@ -39,7 +39,7 @@ fun StakingBalance.Data.getTotalWithRewardsStakingBalance(blockchainId: String):
fun StakingBalance.Data.getTotalStakingBalance(blockchainId: String): BigDecimal { fun StakingBalance.Data.getTotalStakingBalance(blockchainId: String): BigDecimal {
return when (this) { return when (this) {
is StakingBalance.Data.StakeKit -> getTotalStakingBalanceStakeKit(blockchainId) 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), expected = createStakingId(integrationId = StakingIntegrationID.StakeKit.Coin.Cardano),
), ),
CreateModel( CreateModel(
currencyId = createCurrencyId(blockchain = StakingIntegrationID.P2P.EthereumPooled.blockchain), currencyId = createCurrencyId(blockchain = StakingIntegrationID.P2PEthPool.blockchain),
expected = createStakingId(integrationId = StakingIntegrationID.P2P.EthereumPooled), expected = createStakingId(integrationId = StakingIntegrationID.P2PEthPool),
), ),
CreateModel( CreateModel(
currencyId = CryptoCurrency.ID.fromValue( currencyId = CryptoCurrency.ID.fromValue(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
package com.tangem.features.staking.impl.deeplink package com.tangem.features.staking.impl.deeplink
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY 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.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingAvailabilityUseCase
import com.tangem.domain.staking.GetYieldUseCase
import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
@ -28,7 +26,6 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
private val appRouter: AppRouter, private val appRouter: AppRouter,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val getYieldUseCase: GetYieldUseCase,
private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase, private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase,
) : StakingDeepLinkHandler { ) : StakingDeepLinkHandler {
@ -73,19 +70,13 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
return@launch return@launch
} }
val isStakingEnabled = getStakingAvailabilityUseCase.invokeSync( val availability = getStakingAvailabilityUseCase.invokeSync(
userWalletId = selectedUserWalletId, userWalletId = selectedUserWalletId,
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
).getOrNull() ).getOrNull()
if (isStakingEnabled !is StakingAvailability.Available) { val option = (availability as? StakingAvailability.Available)?.option
return@launch if (option == null) {
}
val yield = getYieldUseCase.invoke(
cryptoCurrencyId = cryptoCurrency.id,
symbol = cryptoCurrency.symbol,
).getOrElse {
Timber.e("Staking is unavailable for ${cryptoCurrency.name}") Timber.e("Staking is unavailable for ${cryptoCurrency.name}")
return@launch return@launch
} }
@ -94,7 +85,7 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
AppRoute.Staking( AppRoute.Staking(
userWalletId = selectedUserWalletId, userWalletId = selectedUserWalletId,
cryptoCurrency = cryptoCurrency, 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.bottomsheet.permission.state.ApproveType
import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.domain.models.currency.CryptoCurrency 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.BalanceState
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
import java.math.BigDecimal import java.math.BigDecimal
@ -35,7 +35,7 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
fun openValidators() fun openValidators()
fun onValidatorSelect(validator: Yield.Validator) fun onTargetSelect(target: StakingTarget)
fun openRewardsValidators() 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.*
import com.tangem.domain.staking.analytics.StakeScreenSource import com.tangem.domain.staking.analytics.StakeScreenSource
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent 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.StakingApproval
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.StakingIntegrationID 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.StakingError
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.staking.utils.getValidatorsCount import com.tangem.domain.staking.utils.getValidatorsCount
@ -134,6 +138,7 @@ internal class StakingModel @Inject constructor(
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val getActionsUseCase: GetActionsUseCase, private val getActionsUseCase: GetActionsUseCase,
private val getYieldUseCase: GetYieldUseCase, private val getYieldUseCase: GetYieldUseCase,
private val p2pEthPoolRepository: P2PEthPoolRepository,
private val checkAccountInitializedUseCase: CheckAccountInitializedUseCase, private val checkAccountInitializedUseCase: CheckAccountInitializedUseCase,
private val createTransferTransactionUseCase: CreateTransferTransactionUseCase, private val createTransferTransactionUseCase: CreateTransferTransactionUseCase,
private val getFeeUseCase: GetFeeUseCase, private val getFeeUseCase: GetFeeUseCase,
@ -164,9 +169,18 @@ internal class StakingModel @Inject constructor(
private val cryptoCurrencyId: CryptoCurrency.ID = params.cryptoCurrency.id private val cryptoCurrencyId: CryptoCurrency.ID = params.cryptoCurrency.id
private val userWalletId: UserWalletId = params.userWalletId private val userWalletId: UserWalletId = params.userWalletId
private val yield: Yield = runBlocking { private val integration: StakingIntegration = runBlocking {
getYieldUseCase(params.yieldId).getOrElse { when (val integrationId = params.integrationId) {
error("yield must be not null") 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( return invalidatePendingTransactionsUseCase(
balanceItems = stakeKitBalance?.balance?.items.orEmpty(), balanceItems = stakeKitBalance?.balance?.items.orEmpty(),
stakingActions = stakingActions, stakingActions = stakingActions,
token = yield.token, token = integration.token,
).getOrElse { emptyList() } ).getOrElse { emptyList() }
} }
@ -205,7 +219,7 @@ internal class StakingModel @Inject constructor(
stakingBalanceUpdater.create( stakingBalanceUpdater.create(
cryptoCurrencyStatus, cryptoCurrencyStatus,
userWallet, userWallet,
yield, integration,
) )
} }
@ -213,7 +227,7 @@ internal class StakingModel @Inject constructor(
stakingFeeTransactionLoader.create( stakingFeeTransactionLoader.create(
cryptoCurrencyStatus = cryptoCurrencyStatus, cryptoCurrencyStatus = cryptoCurrencyStatus,
userWallet = userWallet, userWallet = userWallet,
yield = yield, integration = integration,
) )
} }
@ -221,7 +235,7 @@ internal class StakingModel @Inject constructor(
stakingTransactionLoader.create( stakingTransactionLoader.create(
cryptoCurrencyStatus = cryptoCurrencyStatus, cryptoCurrencyStatus = cryptoCurrencyStatus,
userWallet = userWallet, userWallet = userWallet,
yield = yield, integration = integration,
isAmountSubtractAvailable = isAmountSubtractAvailable, isAmountSubtractAvailable = isAmountSubtractAvailable,
) )
} }
@ -280,7 +294,7 @@ internal class StakingModel @Inject constructor(
val hasNoYieldBalanceData = cryptoCurrencyStatus.value.stakingBalance !is StakingBalance.Data.StakeKit val hasNoYieldBalanceData = cryptoCurrencyStatus.value.stakingBalance !is StakingBalance.Data.StakeKit
when { when {
isInitialInfoStep && noBalanceState && yield.allValidatorsFull && hasNoYieldBalanceData -> { isInitialInfoStep && noBalanceState && integration.areAllTargetsFull && hasNoYieldBalanceData -> {
stakingEventFactory.createStakingValidatorsUnavailableAlert() stakingEventFactory.createStakingValidatorsUnavailableAlert()
return@launch return@launch
} }
@ -293,12 +307,12 @@ internal class StakingModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus, cryptoCurrencyStatus = cryptoCurrencyStatus,
stakingApproval = stakingApproval, stakingApproval = stakingApproval,
stakingAllowance = stakingAllowance, stakingAllowance = stakingAllowance,
yieldArgs = yield.args, integration = integration,
).let(::add) ).let(::add)
if (yield.args.enter.isPartialAmountDisabled) { if (integration.isPartialAmountDisabled) {
ValidatorSelectChangeTransformer( ValidatorSelectChangeTransformer(
selectedValidator = yield.preferredValidators.firstOrNull(), selectedTarget = integration.preferredTargets.firstOrNull(),
yield = yield, integration = integration,
).let(::add) ).let(::add)
SetAmountDataTransformer( SetAmountDataTransformer(
clickIntents = this@StakingModel, clickIntents = this@StakingModel,
@ -313,7 +327,7 @@ internal class StakingModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus, cryptoCurrencyStatus = cryptoCurrencyStatus,
minimumTransactionAmount = minimumTransactionAmount, minimumTransactionAmount = minimumTransactionAmount,
actionType = uiState.value.actionType, actionType = uiState.value.actionType,
yield = yield, integration = integration,
).let(::add) ).let(::add)
} }
} }
@ -327,7 +341,7 @@ internal class StakingModel @Inject constructor(
override fun getFee() { override fun getFee() {
stateController.update( stateController.update(
SetConfirmationStateLoadingTransformer( SetConfirmationStateLoadingTransformer(
yield = yield, integration = integration,
appCurrency = appCurrency, appCurrency = appCurrency,
cryptoCurrency = cryptoCurrencyStatus.currency, cryptoCurrency = cryptoCurrencyStatus.currency,
), ),
@ -479,7 +493,7 @@ internal class StakingModel @Inject constructor(
} }
override fun onAmountEnterClick() { override fun onAmountEnterClick() {
if (yield.preferredValidators.isEmpty()) { if (integration.preferredTargets.isEmpty()) {
stateController.updateEvent( stateController.updateEvent(
StakingEvent.ShowAlert(StakingAlertUM.NoAvailableValidators), StakingEvent.ShowAlert(StakingAlertUM.NoAvailableValidators),
) )
@ -487,8 +501,8 @@ internal class StakingModel @Inject constructor(
if (uiState.value.actionType is StakingActionCommonType.Enter) { if (uiState.value.actionType is StakingActionCommonType.Enter) {
stateController.updateAll( stateController.updateAll(
ValidatorSelectChangeTransformer( ValidatorSelectChangeTransformer(
selectedValidator = null, selectedTarget = null,
yield = yield, integration = integration,
), ),
) )
} }
@ -502,7 +516,7 @@ internal class StakingModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus, cryptoCurrencyStatus = cryptoCurrencyStatus,
minimumTransactionAmount = minimumTransactionAmount, minimumTransactionAmount = minimumTransactionAmount,
value = value, value = value,
yield = yield, integration = integration,
), ),
) )
} }
@ -518,7 +532,7 @@ internal class StakingModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus, cryptoCurrencyStatus = cryptoCurrencyStatus,
minimumTransactionAmount = minimumTransactionAmount, minimumTransactionAmount = minimumTransactionAmount,
actionType = uiState.value.actionType, actionType = uiState.value.actionType,
yield = yield, integration = integration,
), ),
) )
} }
@ -539,16 +553,16 @@ internal class StakingModel @Inject constructor(
stakingStateRouter.showValidators() stakingStateRouter.showValidators()
} }
override fun onValidatorSelect(validator: Yield.Validator) { override fun onTargetSelect(target: StakingTarget) {
analyticsEventHandler.send( analyticsEventHandler.send(
StakingAnalyticsEvent.ValidatorChosen( StakingAnalyticsEvent.ValidatorChosen(
validator = validator.name, validator = target.name,
), ),
) )
stateController.update( stateController.update(
ValidatorSelectChangeTransformer( ValidatorSelectChangeTransformer(
selectedValidator = validator, selectedTarget = target,
yield = yield, integration = integration,
), ),
) )
} }
@ -606,7 +620,7 @@ internal class StakingModel @Inject constructor(
balanceType = activeStake.type, balanceType = activeStake.type,
pendingActions = activeStake.pendingActions, pendingActions = activeStake.pendingActions,
balanceState = activeStake, balanceState = activeStake,
validator = activeStake.validator, target = activeStake.target,
amountValue = activeStake.cryptoValue, amountValue = activeStake.cryptoValue,
) )
onNextClick(activeStake) onNextClick(activeStake)
@ -619,7 +633,7 @@ internal class StakingModel @Inject constructor(
balanceType = activeStake.type, balanceType = activeStake.type,
pendingAction = action, pendingAction = action,
balanceState = activeStake, balanceState = activeStake,
validator = activeStake.validator, target = activeStake.target,
amountValue = activeStake.cryptoValue, amountValue = activeStake.cryptoValue,
) )
stateController.update(DismissBottomSheetStateTransformer) stateController.update(DismissBottomSheetStateTransformer)
@ -788,7 +802,7 @@ internal class StakingModel @Inject constructor(
isSubtractAvailable = isAmountSubtractAvailable, isSubtractAvailable = isAmountSubtractAvailable,
feeError = feeError, feeError = feeError,
stakingError = stakingError, 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 validatorState = uiState.value.validatorState as? StakingStates.ValidatorState.Data
val feeState = confirmationState?.feeState as? FeeState.Content val feeState = confirmationState?.feeState as? FeeState.Content
val validator = validatorState?.chosenValidator val target = validatorState?.chosenTarget
val feeAmount = feeState?.fee?.amount val feeAmount = feeState?.fee?.amount
val amount = amountState?.amountTextField?.cryptoAmount val amount = amountState?.amountTextField?.cryptoAmount
saveBlockchainErrorUseCase( saveBlockchainErrorUseCase(
@ -919,7 +933,7 @@ internal class StakingModel @Inject constructor(
errorMessage = errorMessage, errorMessage = errorMessage,
blockchainId = network.rawId, blockchainId = network.rawId,
derivationPath = network.derivationPath.value, derivationPath = network.derivationPath.value,
destinationAddress = validator?.address.orEmpty(), destinationAddress = target?.address.orEmpty(),
tokenSymbol = (cryptoCurrencyStatus.currency as? CryptoCurrency.Token)?.symbol, tokenSymbol = (cryptoCurrencyStatus.currency as? CryptoCurrency.Token)?.symbol,
amount = amount?.run { value?.toPlainString() + currencySymbol }.orEmpty(), amount = amount?.run { value?.toPlainString() + currencySymbol }.orEmpty(),
fee = feeAmount?.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( val email = FeedbackEmailType.StakingProblem(
walletMetaInfo = metaInfo, walletMetaInfo = metaInfo,
validatorName = validator?.name, validatorName = target?.name,
transactionTypes = transactionsInProgress.map { it.type.name }, transactionTypes = transactionsInProgress.map { it.type.name },
unsignedTransactions = transactionsInProgress.map { it.unsignedTransaction }, unsignedTransactions = transactionsInProgress.map { it.unsignedTransaction },
) )
@ -1229,7 +1243,7 @@ internal class StakingModel @Inject constructor(
stateController.updateAll( stateController.updateAll(
SetInitialDataStateTransformer( SetInitialDataStateTransformer(
clickIntents = this@StakingModel, clickIntents = this@StakingModel,
yield = yield, integration = integration,
isAnyTokenStaked = isAnyTokenStaked, isAnyTokenStaked = isAnyTokenStaked,
cryptoCurrencyStatus = status, cryptoCurrencyStatus = status,
userWalletProvider = Provider { userWallet }, userWalletProvider = Provider { userWallet },
@ -1248,7 +1262,7 @@ internal class StakingModel @Inject constructor(
balanceState: BalanceState, balanceState: BalanceState,
pendingActions: ImmutableList<PendingAction> = persistentListOf(), pendingActions: ImmutableList<PendingAction> = persistentListOf(),
pendingAction: PendingAction? = pendingActions.firstOrNull(), pendingAction: PendingAction? = pendingActions.firstOrNull(),
validator: Yield.Validator?, target: StakingTarget?,
amountValue: String, amountValue: String,
) { ) {
stateController.updateAll( stateController.updateAll(
@ -1261,11 +1275,11 @@ internal class StakingModel @Inject constructor(
pendingActions = pendingActions, pendingActions = pendingActions,
pendingAction = pendingAction, pendingAction = pendingAction,
stakingAllowance = stakingAllowance, stakingAllowance = stakingAllowance,
yieldArgs = yield.args, integration = integration,
), ),
ValidatorSelectChangeTransformer( ValidatorSelectChangeTransformer(
selectedValidator = validator, selectedTarget = target,
yield = yield, integration = integration,
), ),
SetAmountDataTransformer( SetAmountDataTransformer(
clickIntents = this, clickIntents = this,
@ -1280,7 +1294,7 @@ internal class StakingModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus, cryptoCurrencyStatus = cryptoCurrencyStatus,
value = amountValue, value = amountValue,
minimumTransactionAmount = minimumTransactionAmount, 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.PendingAction
import com.tangem.domain.models.staking.PendingActionConstraints import com.tangem.domain.models.staking.PendingActionConstraints
import com.tangem.domain.models.staking.RewardBlockType 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 kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal import java.math.BigDecimal
@ -35,10 +35,10 @@ internal data class BalanceState(
val fiatAmount: BigDecimal?, val fiatAmount: BigDecimal?,
val formattedFiatAmount: TextReference, val formattedFiatAmount: TextReference,
val rawCurrencyId: String?, val rawCurrencyId: String?,
val validator: Yield.Validator?, val target: StakingTarget?,
val pendingActions: ImmutableList<PendingAction>, val pendingActions: ImmutableList<PendingAction>,
val isPending: Boolean, val isPending: Boolean,
val validatorAddress: String?, val targetAddress: String?,
) )
@Immutable @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.extensions.TextReference
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.domain.models.staking.PendingAction 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.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
import com.tangem.features.staking.impl.presentation.state.events.StakingEvent import com.tangem.features.staking.impl.presentation.state.events.StakingEvent
@ -97,9 +97,9 @@ internal sealed class StakingStates {
override val isPrimaryButtonEnabled: Boolean, override val isPrimaryButtonEnabled: Boolean,
override val isClickable: Boolean, override val isClickable: Boolean,
val isVisibleOnConfirmation: Boolean, val isVisibleOnConfirmation: Boolean,
val chosenValidator: Yield.Validator, val chosenTarget: StakingTarget,
val activeValidator: Yield.Validator?, val activeTarget: StakingTarget?,
val availableValidators: List<Yield.Validator>, val availableTargets: List<StakingTarget>,
) : ValidatorState() ) : ValidatorState()
data class Empty( 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.BalanceType.Companion.isClickable
import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.staking.action.StakingActionType 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.domain.staking.utils.getRewardStakingBalance
import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.BalanceState
@ -29,24 +29,24 @@ import java.util.Calendar
internal class BalanceItemConverter( internal class BalanceItemConverter(
private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrencyProvider: Provider<AppCurrency>, private val appCurrencyProvider: Provider<AppCurrency>,
private val yield: Yield, private val integration: StakingIntegration,
) : Converter<BalanceItem, BalanceState?> { ) : Converter<BalanceItem, BalanceState?> {
override fun convert(value: BalanceItem): BalanceState? { override fun convert(value: BalanceItem): BalanceState? {
val appCurrency = appCurrencyProvider() val appCurrency = appCurrencyProvider()
val cryptoCurrency = cryptoCurrencyStatus.currency val cryptoCurrency = cryptoCurrencyStatus.currency
val validator = yield.validators.firstOrNull { val target = integration.targets.firstOrNull {
value.validatorAddress?.contains(it.address, ignoreCase = true) == true value.validatorAddress?.contains(it.address, ignoreCase = true) == true
} }
val cryptoAmount = value.getBalanceValue() val cryptoAmount = value.getBalanceValue()
val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount) val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount)
val title = value.type.getTitle(validator?.name) val title = value.type.getTitle(target?.name)
return title?.let { return title?.let {
BalanceState( BalanceState(
groupId = value.groupId, groupId = value.groupId,
validator = validator, target = target,
title = title, title = title,
subtitle = getSubtitle(value), subtitle = getSubtitle(value),
type = value.type, type = value.type,
@ -68,7 +68,7 @@ internal class BalanceItemConverter(
pendingActions = value.pendingActions.toPersistentList(), pendingActions = value.pendingActions.toPersistentList(),
isClickable = value.isClickable(), isClickable = value.isClickable(),
isPending = value.isPending, isPending = value.isPending,
validatorAddress = value.validatorAddress, targetAddress = value.validatorAddress,
) )
} }
} }
@ -108,7 +108,7 @@ internal class BalanceItemConverter(
resourceReference(R.string.staking_tap_to_unlock) resourceReference(R.string.staking_tap_to_unlock)
} }
BalanceType.PREPARING -> { BalanceType.PREPARING -> {
val warmupPeriod = yield.metadata.warmupPeriod.days val warmupPeriod = integration.warmupPeriodDays
combinedReference( combinedReference(
resourceReference(R.string.staking_details_warmup_period), resourceReference(R.string.staking_details_warmup_period),
stringReference(" "), stringReference(" "),
@ -124,7 +124,7 @@ internal class BalanceItemConverter(
} }
private fun getUnbondingDate(date: Instant?): TextReference? { private fun getUnbondingDate(date: Instant?): TextReference? {
val unbondingPeriod = yield.metadata.cooldownPeriod?.days ?: return null val unbondingPeriod = integration.cooldownPeriodDays ?: return null
if (date == null) { if (date == null) {
return combinedReference( return combinedReference(
resourceReference(R.string.staking_details_unbonding_period), 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.BalanceItem
import com.tangem.domain.models.staking.BalanceType import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.StakingBalance 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.BalanceState
import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.utils.Provider import com.tangem.utils.Provider
@ -21,7 +22,7 @@ import java.math.BigDecimal
internal class RewardsValidatorStateConverter( internal class RewardsValidatorStateConverter(
private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrencyProvider: Provider<AppCurrency>, private val appCurrencyProvider: Provider<AppCurrency>,
private val yield: Yield, private val integration: StakingIntegration,
) : Converter<Unit, StakingStates.RewardsValidatorsState> { ) : Converter<Unit, StakingStates.RewardsValidatorsState> {
override fun convert(value: Unit): StakingStates.RewardsValidatorsState { override fun convert(value: Unit): StakingStates.RewardsValidatorsState {
val stakingBalance = cryptoCurrencyStatus.value.stakingBalance val stakingBalance = cryptoCurrencyStatus.value.stakingBalance
@ -42,13 +43,13 @@ internal class RewardsValidatorStateConverter(
private fun List<BalanceItem>.mapRewardBalances(cryptoCurrencyStatus: CryptoCurrencyStatus) = private fun List<BalanceItem>.mapRewardBalances(cryptoCurrencyStatus: CryptoCurrencyStatus) =
this.mapNotNull { balance -> this.mapNotNull { balance ->
val validator = yield.validators.firstOrNull { val target = integration.targets.firstOrNull {
it.address.contains(balance.validatorAddress.orEmpty(), ignoreCase = true) it.address.contains(balance.validatorAddress.orEmpty(), ignoreCase = true)
} }
val cryptoValue = balance.amount val cryptoValue = balance.amount
val fiatValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoValue) val fiatValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoValue)
validator?.toBalanceState( target?.toBalanceState(
balance = balance, balance = balance,
cryptoCurrencyStatus = cryptoCurrencyStatus, cryptoCurrencyStatus = cryptoCurrencyStatus,
cryptoValue = cryptoValue, cryptoValue = cryptoValue,
@ -56,7 +57,7 @@ internal class RewardsValidatorStateConverter(
) )
} }
private fun Yield.Validator.toBalanceState( private fun StakingTarget.toBalanceState(
balance: BalanceItem, balance: BalanceItem,
cryptoCurrencyStatus: CryptoCurrencyStatus, cryptoCurrencyStatus: CryptoCurrencyStatus,
cryptoValue: BigDecimal, cryptoValue: BigDecimal,
@ -80,7 +81,7 @@ internal class RewardsValidatorStateConverter(
return BalanceState( return BalanceState(
groupId = balance.groupId, groupId = balance.groupId,
validator = this, target = this,
title = stringReference(this.name), title = stringReference(this.name),
subtitle = null, subtitle = null,
cryptoValue = cryptoValue.parseBigDecimal(cryptoCurrency.decimals), cryptoValue = cryptoValue.parseBigDecimal(cryptoCurrency.decimals),
@ -93,7 +94,7 @@ internal class RewardsValidatorStateConverter(
isClickable = true, isClickable = true,
type = balance.type, type = balance.type,
isPending = balance.isPending, 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.RewardBlockType
import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.staking.action.StakingActionType 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.domain.staking.utils.getRewardStakingBalance
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
import com.tangem.features.staking.impl.presentation.state.YieldReward import com.tangem.features.staking.impl.presentation.state.YieldReward
@ -25,11 +25,11 @@ internal class YieldBalancesConverter(
private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrencyProvider: Provider<AppCurrency>, private val appCurrencyProvider: Provider<AppCurrency>,
private val balancesToShowProvider: Provider<List<BalanceItem>>, private val balancesToShowProvider: Provider<List<BalanceItem>>,
private val yield: Yield, private val integration: StakingIntegration,
) : Converter<Unit, InnerYieldBalanceState> { ) : Converter<Unit, InnerYieldBalanceState> {
private val balanceItemConverter by lazy(LazyThreadSafetyMode.NONE) { private val balanceItemConverter by lazy(LazyThreadSafetyMode.NONE) {
BalanceItemConverter(cryptoCurrencyStatus, appCurrencyProvider, yield) BalanceItemConverter(cryptoCurrencyStatus, appCurrencyProvider, integration)
} }
override fun convert(value: Unit): InnerYieldBalanceState { 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.models.wallet.UserWallet
import com.tangem.domain.staking.FetchActionsUseCase import com.tangem.domain.staking.FetchActionsUseCase
import com.tangem.domain.staking.FetchStakingYieldBalanceUseCase 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.staking.model.stakekit.action.StakingActionStatus
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.FetchPendingTransactionsUseCase import com.tangem.domain.tokens.FetchPendingTransactionsUseCase
@ -27,7 +27,7 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
@DelayedWork private val coroutineScope: CoroutineScope, @DelayedWork private val coroutineScope: CoroutineScope,
@Assisted private val userWallet: UserWallet, @Assisted private val userWallet: UserWallet,
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus, @Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
@Assisted private val yield: Yield, @Assisted private val integration: StakingIntegration,
) { ) {
fun updateAfterTransaction() { fun updateAfterTransaction() {
coroutineScope.launch { coroutineScope.launch {
@ -105,7 +105,7 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
fetchActionsUseCase( fetchActionsUseCase(
userWalletId = userWallet.walletId, userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrencyStatus.currency, cryptoCurrency = cryptoCurrencyStatus.currency,
networkType = yield.token.network, networkType = integration.token.network,
stakingActionStatus = StakingActionStatus.PROCESSING, stakingActionStatus = StakingActionStatus.PROCESSING,
) )
} }
@ -115,7 +115,7 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
fun create( fun create(
cryptoCurrencyStatus: CryptoCurrencyStatus, cryptoCurrencyStatus: CryptoCurrencyStatus,
userWallet: UserWallet, userWallet: UserWallet,
yield: Yield, integration: StakingIntegration,
): StakingBalanceUpdater ): 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.staking.PendingAction
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.EstimateGasUseCase 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.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.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate 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.error.GetFeeError
import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase
@ -45,7 +44,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase, private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase,
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus, @Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
@Assisted private val userWallet: UserWallet, @Assisted private val userWallet: UserWallet,
@Assisted private val yield: Yield, @Assisted private val integration: StakingIntegration,
) { ) {
suspend fun getFee( suspend fun getFee(
@ -58,9 +57,9 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data
?: error("Illegal state") ?: error("Illegal state")
val validatorAddress = (state.validatorState as? StakingStates.ValidatorState.Data)?.chosenValidator?.address val validatorAddress = (state.validatorState as? StakingStates.ValidatorState.Data)?.chosenTarget?.address
?: state.balanceState?.validatorAddress ?: state.balanceState?.targetAddress
?: error("No validator address provided") ?: error("No target address provided")
val amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value val amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided") ?: error("No amount provided")
@ -170,11 +169,11 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
network = cryptoCurrencyStatus.currency.network, network = cryptoCurrencyStatus.currency.network,
params = ActionParams( params = ActionParams(
actionCommonType = stateController.value.actionType, actionCommonType = stateController.value.actionType,
integrationId = yield.id, integrationId = integration.integrationId.value,
amount = amount, amount = amount,
address = sourceAddress, address = sourceAddress,
validatorAddress = validatorAddress, validatorAddress = validatorAddress,
token = yield.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId), token = integration.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
passthrough = action?.passthrough, passthrough = action?.passthrough,
type = action?.type, type = action?.type,
), ),
@ -234,7 +233,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
fun create( fun create(
cryptoCurrencyStatus: CryptoCurrencyStatus, cryptoCurrencyStatus: CryptoCurrencyStatus,
userWallet: UserWallet, userWallet: UserWallet,
yield: Yield, integration: StakingIntegration,
): StakingFeeTransactionLoader ): StakingFeeTransactionLoader
} }
} }

View file

@ -13,15 +13,14 @@ import com.tangem.domain.staking.GetConstructedStakingTransactionUseCase
import com.tangem.domain.staking.GetStakingTransactionsUseCase import com.tangem.domain.staking.GetStakingTransactionsUseCase
import com.tangem.domain.staking.SaveUnsubmittedHashUseCase import com.tangem.domain.staking.SaveUnsubmittedHashUseCase
import com.tangem.domain.staking.SubmitHashUseCase 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.SubmitHashData
import com.tangem.domain.staking.model.stakekit.StakingError 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.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams 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.StakingTransaction
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionStatus import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionStatus
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType 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.error.SendTransactionError
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase
@ -56,12 +55,12 @@ internal class StakingTransactionSender @AssistedInject constructor(
private val isFeeApproximateUseCase: IsFeeApproximateUseCase, private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus, @Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
@Assisted private val userWallet: UserWallet, @Assisted private val userWallet: UserWallet,
@Assisted private val yield: Yield, @Assisted private val integration: StakingIntegration,
@Assisted private val isAmountSubtractAvailable: Boolean, @Assisted private val isAmountSubtractAvailable: Boolean,
) { ) {
private val balanceUpdater: StakingBalanceUpdater private val balanceUpdater: StakingBalanceUpdater
get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet, yield) get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet, integration)
suspend fun constructAndSendTransactions( suspend fun constructAndSendTransactions(
onConstructSuccess: (List<StakingTransaction>) -> Unit, onConstructSuccess: (List<StakingTransaction>) -> Unit,
@ -193,7 +192,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
val amountState = state.amountState as? AmountState.Data val amountState = state.amountState as? AmountState.Data
?: error("No amount provided") ?: error("No amount provided")
val validatorAddress = validatorState.chosenValidator.address val validatorAddress = validatorState.chosenTarget.address
val amount = getAmount(amountState, fee, confirmationState.reduceAmountBy) val amount = getAmount(amountState, fee, confirmationState.reduceAmountBy)
return getStakingTransactionsUseCase( return getStakingTransactionsUseCase(
@ -201,11 +200,11 @@ internal class StakingTransactionSender @AssistedInject constructor(
network = cryptoCurrencyStatus.currency.network, network = cryptoCurrencyStatus.currency.network,
params = ActionParams( params = ActionParams(
actionCommonType = state.actionType, actionCommonType = state.actionType,
integrationId = yield.id, integrationId = integration.integrationId.value,
amount = amount, amount = amount,
address = defaultAddress, address = defaultAddress,
validatorAddress = validatorAddress, validatorAddress = validatorAddress,
token = yield.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId), token = integration.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
passthrough = action?.passthrough, passthrough = action?.passthrough,
type = action?.type, type = action?.type,
), ),
@ -306,7 +305,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
fun create( fun create(
cryptoCurrencyStatus: CryptoCurrencyStatus, cryptoCurrencyStatus: CryptoCurrencyStatus,
userWallet: UserWallet, userWallet: UserWallet,
yield: Yield, integration: StakingIntegration,
isAmountSubtractAvailable: Boolean, isAmountSubtractAvailable: Boolean,
): StakingTransactionSender ): 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.BalanceType
import com.tangem.domain.models.staking.RewardBlockType import com.tangem.domain.models.staking.RewardBlockType
import com.tangem.domain.staking.model.stakekit.Yield 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.R
import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
@ -81,7 +82,7 @@ internal object InitialStakingStatePreview {
fiatAmount = null, fiatAmount = null,
formattedFiatAmount = stringReference("100 $"), formattedFiatAmount = stringReference("100 $"),
rawCurrencyId = null, rawCurrencyId = null,
validator = Yield.Validator( target = Yield.Validator(
address = "address", address = "address",
status = Yield.Validator.ValidatorStatus.ACTIVE, status = Yield.Validator.ValidatorStatus.ACTIVE,
name = "Binance", name = "Binance",
@ -92,13 +93,13 @@ internal object InitialStakingStatePreview {
votingPower = null, votingPower = null,
preferred = false, preferred = false,
isStrategicPartner = false, isStrategicPartner = false,
), ).toStakingTarget(),
pendingActions = persistentListOf(), pendingActions = persistentListOf(),
isClickable = true, isClickable = true,
type = BalanceType.STAKED, type = BalanceType.STAKED,
subtitle = null, subtitle = null,
isPending = false, isPending = false,
validatorAddress = "", targetAddress = "",
), ),
), ),
), ),

View file

@ -1,7 +1,11 @@
package com.tangem.features.staking.impl.presentation.state.previewdata 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
import com.tangem.domain.staking.model.stakekit.Yield.Validator.ValidatorStatus 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 com.tangem.features.staking.impl.presentation.state.StakingStates
import java.math.BigDecimal import java.math.BigDecimal
@ -13,9 +17,9 @@ internal object ValidatorStatePreviewData {
status = ValidatorStatus.ACTIVE, status = ValidatorStatus.ACTIVE,
name = "Luganodes", name = "Luganodes",
image = "https://assets.stakek.it/validators/luganodes.png", image = "https://assets.stakek.it/validators/luganodes.png",
rewardInfo = Yield.RewardInfo( rewardInfo = RewardInfo(
rate = BigDecimal("0.054823398040640445"), rate = BigDecimal("0.054823398040640445"),
type = Yield.RewardType.APR, type = RewardType.APR,
), ),
commission = 0.1, commission = 0.1,
stakedBalance = "355544384.45009977", stakedBalance = "355544384.45009977",
@ -29,9 +33,9 @@ internal object ValidatorStatePreviewData {
status = ValidatorStatus.ACTIVE, status = ValidatorStatus.ACTIVE,
name = "InfStones", name = "InfStones",
image = "https://assets.stakek.it/validators/infstones.png", image = "https://assets.stakek.it/validators/infstones.png",
rewardInfo = Yield.RewardInfo( rewardInfo = RewardInfo(
rate = BigDecimal("0.057786472172836965"), rate = BigDecimal("0.057786472172836965"),
type = Yield.RewardType.APR, type = RewardType.APR,
), ),
commission = 0.05, commission = 0.05,
stakedBalance = "12495684.05643019", stakedBalance = "12495684.05643019",
@ -45,9 +49,9 @@ internal object ValidatorStatePreviewData {
status = ValidatorStatus.ACTIVE, status = ValidatorStatus.ACTIVE,
name = "Kiln", name = "Kiln",
image = "https://assets.stakek.it/validators/kiln.png", image = "https://assets.stakek.it/validators/kiln.png",
rewardInfo = Yield.RewardInfo( rewardInfo = RewardInfo(
rate = BigDecimal("0.057786472172836965"), rate = BigDecimal("0.057786472172836965"),
type = Yield.RewardType.APR, type = RewardType.APR,
), ),
commission = 0.05, commission = 0.05,
stakedBalance = "85400369.96393165", stakedBalance = "85400369.96393165",
@ -58,11 +62,13 @@ internal object ValidatorStatePreviewData {
), ),
) )
private val targetList: List<StakingTarget> = validatorList.map { it.toStakingTarget() }
val validatorState = StakingStates.ValidatorState.Data( val validatorState = StakingStates.ValidatorState.Data(
availableValidators = validatorList, availableTargets = targetList,
chosenValidator = validatorList.first(), chosenTarget = targetList.first(),
isPrimaryButtonEnabled = true, isPrimaryButtonEnabled = true,
activeValidator = null, activeTarget = null,
isClickable = true, isClickable = true,
isVisibleOnConfirmation = 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.bottomsheet.permission.state.ApproveType
import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.domain.models.currency.CryptoCurrency 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.model.StakingClickIntents
import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
@ -40,7 +40,7 @@ internal object StakingClickIntentsStub : StakingClickIntents {
override fun openValidators() {} override fun openValidators() {}
override fun onValidatorSelect(validator: Yield.Validator) {} override fun onTargetSelect(target: StakingTarget) {}
override fun openRewardsValidators() {} 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.PendingAction
import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.models.staking.action.StakingActionType
import com.tangem.domain.staking.model.StakingApproval 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.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions
@ -24,7 +24,7 @@ internal class SetConfirmationStateInitTransformer(
private val stakingApproval: StakingApproval, private val stakingApproval: StakingApproval,
private val stakingAllowance: BigDecimal, private val stakingAllowance: BigDecimal,
private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val yieldArgs: Yield.Args, private val integration: StakingIntegration,
private val pendingActions: ImmutableList<PendingAction>? = null, private val pendingActions: ImmutableList<PendingAction>? = null,
private val pendingAction: PendingAction? = pendingActions?.firstOrNull(), private val pendingAction: PendingAction? = pendingActions?.firstOrNull(),
) : Transformer<StakingUiState> { ) : Transformer<StakingUiState> {
@ -66,7 +66,7 @@ internal class SetConfirmationStateInitTransformer(
} }
private fun getActionType(prevState: StakingUiState): StakingActionCommonType { private fun getActionType(prevState: StakingUiState): StakingActionCommonType {
val isPartialEnterAmountDisabled = yieldArgs.enter.isPartialAmountDisabled val isPartialEnterAmountDisabled = integration.isPartialAmountDisabled
val isPartialExitAmountDisabled = isPartiallyUnstakeDisabled(prevState) val isPartialExitAmountDisabled = isPartiallyUnstakeDisabled(prevState)
return when { return when {
isEnter -> StakingActionCommonType.Enter(isPartialEnterAmountDisabled) isEnter -> StakingActionCommonType.Enter(isPartialEnterAmountDisabled)
@ -87,12 +87,12 @@ internal class SetConfirmationStateInitTransformer(
private fun isPartiallyUnstakeDisabled(state: StakingUiState): Boolean { private fun isPartiallyUnstakeDisabled(state: StakingUiState): Boolean {
val isSolana = BlockchainUtils.isSolana(state.cryptoCurrencyBlockchainId) 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 true
} else { } 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.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency 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.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.FeeState 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 import com.tangem.utils.transformer.Transformer
internal class SetConfirmationStateLoadingTransformer( internal class SetConfirmationStateLoadingTransformer(
private val yield: Yield, private val integration: StakingIntegration,
private val appCurrency: AppCurrency, private val appCurrency: AppCurrency,
private val cryptoCurrency: CryptoCurrency, private val cryptoCurrency: CryptoCurrency,
) : Transformer<StakingUiState> { ) : Transformer<StakingUiState> {
@ -48,7 +48,7 @@ internal class SetConfirmationStateLoadingTransformer(
) )
} }
val rewardSchedule = getRewardScheduleText( val rewardSchedule = getRewardScheduleText(
rewardSchedule = yield.metadata.rewardSchedule, rewardSchedule = integration.rewardSchedule,
networkId = cryptoCurrency.network.rawId, networkId = cryptoCurrency.network.rawId,
decapitalize = true, 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.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.BalanceItem import com.tangem.domain.models.staking.BalanceItem
import com.tangem.domain.models.wallet.UserWallet 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.domain.staking.model.stakekit.Yield
import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
@ -41,7 +44,7 @@ import java.math.BigDecimal
@Suppress("LongParameterList") @Suppress("LongParameterList")
internal class SetInitialDataStateTransformer( internal class SetInitialDataStateTransformer(
private val clickIntents: StakingClickIntents, private val clickIntents: StakingClickIntents,
private val yield: Yield, private val integration: StakingIntegration,
private val isAnyTokenStaked: Boolean, private val isAnyTokenStaked: Boolean,
private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val userWalletProvider: Provider<UserWallet>, private val userWalletProvider: Provider<UserWallet>,
@ -55,7 +58,7 @@ internal class SetInitialDataStateTransformer(
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
private val rewardsValidatorStateConverter by lazy(LazyThreadSafetyMode.NONE) { private val rewardsValidatorStateConverter by lazy(LazyThreadSafetyMode.NONE) {
RewardsValidatorStateConverter(cryptoCurrencyStatus, appCurrencyProvider, yield) RewardsValidatorStateConverter(cryptoCurrencyStatus, appCurrencyProvider, integration)
} }
private val yieldBalancesConverter by lazy(LazyThreadSafetyMode.NONE) { private val yieldBalancesConverter by lazy(LazyThreadSafetyMode.NONE) {
@ -63,7 +66,7 @@ internal class SetInitialDataStateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus, cryptoCurrencyStatus = cryptoCurrencyStatus,
appCurrencyProvider = appCurrencyProvider, appCurrencyProvider = appCurrencyProvider,
balancesToShowProvider = balancesToShowProvider, balancesToShowProvider = balancesToShowProvider,
yield = yield, integration = integration,
) )
} }
@ -116,27 +119,27 @@ internal class SetInitialDataStateTransformer(
} }
private fun createAnnualPercentageItem(): RoundedListWithDividersItemData { private fun createAnnualPercentageItem(): RoundedListWithDividersItemData {
val validators = yield.preferredValidators val targets = integration.preferredTargets
val rateRangeInfo = getPercentageRange(validators) val rateRangeInfo = getPercentageRange(targets)
return RoundedListWithDividersItemData( return RoundedListWithDividersItemData(
id = R.string.staking_details_annual_percentage_rate, id = R.string.staking_details_annual_percentage_rate,
startText = getRateStartText(rateRangeInfo.first), startText = getRateStartText(rateRangeInfo.first),
endText = rateRangeInfo.second, endText = rateRangeInfo.second,
iconClick = { iconClick = {
when (rateRangeInfo.first) { when (rateRangeInfo.first) {
Yield.RewardType.APR -> clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_RATE) RewardType.APR -> clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_RATE)
Yield.RewardType.APY -> clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_YIELD) RewardType.APY -> clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_YIELD)
Yield.RewardType.UNKNOWN -> {} RewardType.UNKNOWN -> {}
} }
}, },
isEndTextHighlighted = false, isEndTextHighlighted = false,
) )
} }
private fun getRateStartText(rewardType: Yield.RewardType): TextReference { private fun getRateStartText(rewardType: RewardType): TextReference {
return when (rewardType) { return when (rewardType) {
Yield.RewardType.APR -> TextReference.Res(R.string.staking_details_annual_percentage_rate) RewardType.APR -> TextReference.Res(R.string.staking_details_annual_percentage_rate)
Yield.RewardType.APY -> TextReference.Res(R.string.staking_details_annual_percentage_yield) RewardType.APY -> TextReference.Res(R.string.staking_details_annual_percentage_yield)
else -> TextReference.EMPTY else -> TextReference.EMPTY
} }
} }
@ -153,7 +156,7 @@ internal class SetInitialDataStateTransformer(
} }
private fun createUnbondingPeriodItem(): RoundedListWithDividersItemData? { private fun createUnbondingPeriodItem(): RoundedListWithDividersItemData? {
val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days ?: return null val cooldownPeriodDays = integration.cooldownPeriodDays ?: return null
return RoundedListWithDividersItemData( return RoundedListWithDividersItemData(
id = R.string.staking_details_unbonding_period, id = R.string.staking_details_unbonding_period,
startText = TextReference.Res(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( private fun createMinimumRequirementItem(
cryptoCurrencyStatus: CryptoCurrencyStatus, cryptoCurrencyStatus: CryptoCurrencyStatus,
): RoundedListWithDividersItemData? { ): 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 val blockchainId = cryptoCurrencyStatus.currency.network.rawId
if (!showMinimumRequirementInfo(blockchainId)) return null if (!showMinimumRequirementInfo(blockchainId)) return null
@ -183,7 +186,7 @@ internal class SetInitialDataStateTransformer(
} }
private fun createRewardClaimingItem(): RoundedListWithDividersItemData? { private fun createRewardClaimingItem(): RoundedListWithDividersItemData? {
val rewardClaiming = yield.metadata.rewardClaiming val rewardClaiming = integration.rewardClaiming
val endTextId = rewardClaimingResources[rewardClaiming] ?: return null val endTextId = rewardClaimingResources[rewardClaiming] ?: return null
return RoundedListWithDividersItemData( return RoundedListWithDividersItemData(
@ -195,7 +198,7 @@ internal class SetInitialDataStateTransformer(
} }
private fun createWarmupPeriodItem(): RoundedListWithDividersItemData? { private fun createWarmupPeriodItem(): RoundedListWithDividersItemData? {
val warmupPeriodDays = yield.metadata.warmupPeriod.days val warmupPeriodDays = integration.warmupPeriodDays
if (warmupPeriodDays == 0) return null if (warmupPeriodDays == 0) return null
return RoundedListWithDividersItemData( return RoundedListWithDividersItemData(
@ -212,7 +215,7 @@ internal class SetInitialDataStateTransformer(
private fun createRewardScheduleItem(): RoundedListWithDividersItemData? { private fun createRewardScheduleItem(): RoundedListWithDividersItemData? {
val endTextReference = getRewardScheduleText( val endTextReference = getRewardScheduleText(
rewardSchedule = yield.metadata.rewardSchedule, rewardSchedule = integration.rewardSchedule,
networkId = cryptoCurrencyStatus.currency.network.rawId, networkId = cryptoCurrencyStatus.currency.network.rawId,
decapitalize = false, decapitalize = false,
) ?: return null ) ?: return null
@ -252,15 +255,19 @@ internal class SetInitialDataStateTransformer(
) )
} }
private fun getPercentageRange(validators: List<Yield.Validator>): Pair<Yield.RewardType, TextReference> { private fun getPercentageRange(targets: List<StakingTarget>): Pair<RewardType, TextReference> {
if (validators.isEmpty()) { if (targets.isEmpty()) {
return Yield.RewardType.APR to stringReference(DASH_SIGN) return RewardType.APR to stringReference(DASH_SIGN)
} }
val rewardInfos = validators val rewardInfos = targets
.filter { it.preferred } .filter { it.isPreferred }
.takeIf { it.isNotEmpty() } .takeIf { it.isNotEmpty() }
?.mapNotNull { it.rewardInfo } ?.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 infoWithMinRate = rewardInfos.minBy { it.rate }
val infoWithMaxRate = rewardInfos.maxBy { 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.converters.field.AmountFieldChangeTransformer
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.domain.models.currency.CryptoCurrencyStatus 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.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer import com.tangem.utils.transformer.Transformer
@ -13,7 +13,7 @@ internal class AmountChangeStateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val minimumTransactionAmount: EnterAmountBoundary?, private val minimumTransactionAmount: EnterAmountBoundary?,
private val value: String, private val value: String,
private val yield: Yield, private val integration: StakingIntegration,
) : Transformer<StakingUiState> { ) : Transformer<StakingUiState> {
private val maxEnterAmountConverter = MaxEnterAmountConverter() private val maxEnterAmountConverter = MaxEnterAmountConverter()
@ -41,7 +41,7 @@ internal class AmountChangeStateTransformer(
amountState = AmountRequirementStateTransformer( amountState = AmountRequirementStateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus, cryptoCurrencyStatus = cryptoCurrencyStatus,
maxAmount = maxEnterAmount, maxAmount = maxEnterAmount,
yield = yield, integration = integration,
actionType = prevState.actionType, actionType = prevState.actionType,
).transform(updatedAmountState), ).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.converters.field.AmountFieldSetMaxAmountTransformer
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.domain.models.currency.CryptoCurrencyStatus 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.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer import com.tangem.utils.transformer.Transformer
@ -13,7 +13,7 @@ internal class AmountMaxValueStateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val minimumTransactionAmount: EnterAmountBoundary?, private val minimumTransactionAmount: EnterAmountBoundary?,
private val actionType: StakingActionCommonType, private val actionType: StakingActionCommonType,
private val yield: Yield, private val integration: StakingIntegration,
) : Transformer<StakingUiState> { ) : Transformer<StakingUiState> {
private val maxEnterAmountConverter = MaxEnterAmountConverter() private val maxEnterAmountConverter = MaxEnterAmountConverter()
@ -38,7 +38,7 @@ internal class AmountMaxValueStateTransformer(
amountState = AmountRequirementStateTransformer( amountState = AmountRequirementStateTransformer(
maxAmount = maxEnterAmount, maxAmount = maxEnterAmount,
cryptoCurrencyStatus = cryptoCurrencyStatus, cryptoCurrencyStatus = cryptoCurrencyStatus,
yield = yield, integration = integration,
actionType = prevState.actionType, actionType = prevState.actionType,
).transform(updatedAmountState), ).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.format.bigdecimal.format
import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.models.currency.CryptoCurrencyStatus 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.AddressArgument
import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
@ -26,7 +27,7 @@ import java.math.RoundingMode
internal class AmountRequirementStateTransformer( internal class AmountRequirementStateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val maxAmount: EnterAmountBoundary, private val maxAmount: EnterAmountBoundary,
private val yield: Yield, private val integration: StakingIntegration,
private val actionType: StakingActionCommonType, private val actionType: StakingActionCommonType,
) : Transformer<AmountState> { ) : Transformer<AmountState> {
override fun transform(prevState: AmountState): AmountState { override fun transform(prevState: AmountState): AmountState {
@ -96,11 +97,11 @@ internal class AmountRequirementStateTransformer(
return when (actionType) { return when (actionType) {
is StakingActionCommonType.Enter -> { 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) enterRequirements?.getError(amountDecimal, R.string.staking_amount_requirement_error)
} }
is StakingActionCommonType.Exit -> { 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) exitRequirements?.getError(amountDecimal, R.string.staking_unstake_amount_requirement_error)
} }
else -> null else -> null

View file

@ -37,7 +37,7 @@ internal class ShowApprovalBottomSheetTransformer(
val fee = feeState.fee ?: return prevState val fee = feeState.fee ?: return prevState
val walletAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value.orEmpty() val walletAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value.orEmpty()
val validatorAddress = validatorState.chosenValidator.address val targetAddress = validatorState.chosenTarget.address
val feeCryptoValue = fee.amount.value.format { val feeCryptoValue = fee.amount.value.format {
crypto(fee.amount.currencySymbol, fee.amount.decimals) crypto(fee.amount.currencySymbol, fee.amount.decimals)
} }
@ -57,7 +57,7 @@ internal class ShowApprovalBottomSheetTransformer(
amount = amountState.amountTextField.value, amount = amountState.amountTextField.value,
approveType = ApproveType.UNLIMITED, approveType = ApproveType.UNLIMITED,
walletAddress = walletAddress, walletAddress = walletAddress,
spenderAddress = validatorAddress, spenderAddress = targetAddress,
fee = resourceReference( fee = resourceReference(
R.string.common_crypto_fiat_format, R.string.common_crypto_fiat_format,
wrappedList(feeCryptoValue, feeFiatValue), 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.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus 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.StakingError
import com.tangem.domain.staking.model.stakekit.StakingErrors 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.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
@ -47,12 +47,12 @@ internal class AddStakingNotificationsTransformer(
private val stakingError: StakingError?, private val stakingError: StakingError?,
private val currencyCheck: CryptoCurrencyCheck, private val currencyCheck: CryptoCurrencyCheck,
private val isSubtractAvailable: Boolean, private val isSubtractAvailable: Boolean,
private val yield: Yield, private val integration: StakingIntegration,
) : Transformer<StakingUiState> { ) : Transformer<StakingUiState> {
private val stakingInfoNotificationsFactory = StakingInfoNotificationsFactory( private val stakingInfoNotificationsFactory = StakingInfoNotificationsFactory(
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
yield = yield, integration = integration,
isSubtractAvailable = isSubtractAvailable, isSubtractAvailable = isSubtractAvailable,
) )
@ -77,7 +77,7 @@ internal class AddStakingNotificationsTransformer(
isSubtractAvailable = isSubtractAvailable, isSubtractAvailable = isSubtractAvailable,
reduceAmountBy = reduceAmountBy, reduceAmountBy = reduceAmountBy,
) )
val minimumRequirement = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum.orZero() val minimumRequirement = integration.enterMinimumAmount.orZero()
val sendingAmount = if (isEnterAction) { val sendingAmount = if (isEnterAction) {
checkAndCalculateSubtractedAmount( checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isSubtractAvailable, 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.BalanceType
import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.staking.action.StakingActionType 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.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.R
@ -26,7 +27,7 @@ import java.math.BigDecimal
internal class StakingInfoNotificationsFactory( internal class StakingInfoNotificationsFactory(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>, private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val yield: Yield, private val integration: StakingIntegration,
private val isSubtractAvailable: Boolean, private val isSubtractAvailable: Boolean,
) { ) {
@ -94,7 +95,7 @@ internal class StakingInfoNotificationsFactory(
resourceReference(R.string.staking_notification_withdraw_text) resourceReference(R.string.staking_notification_withdraw_text)
} }
StakingActionType.UNLOCK_LOCKED -> { StakingActionType.UNLOCK_LOCKED -> {
val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days val cooldownPeriodDays = integration.cooldownPeriodDays
if (cooldownPeriodDays != null) { if (cooldownPeriodDays != null) {
resourceReference(R.string.staking_unlocked_locked) to resourceReference( resourceReference(R.string.staking_unlocked_locked) to resourceReference(
R.string.staking_notification_unlock_text, R.string.staking_notification_unlock_text,
@ -213,7 +214,7 @@ internal class StakingInfoNotificationsFactory(
if (prevState.actionType !is StakingActionCommonType.Exit) return if (prevState.actionType !is StakingActionCommonType.Exit) return
val maxAmount = prevState.balanceState?.cryptoAmount ?: 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 amountLeft = maxAmount - actionAmount
val isNotEnoughLeft = !amountLeft.isZero() && amountLeft < exitRequirements.minimum.orZero() val isNotEnoughLeft = !amountLeft.isZero() && amountLeft < exitRequirements.minimum.orZero()
@ -224,7 +225,7 @@ internal class StakingInfoNotificationsFactory(
} }
private fun MutableList<NotificationUM>.addUnstakeInfoNotification() { private fun MutableList<NotificationUM>.addUnstakeInfoNotification() {
val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days val cooldownPeriodDays = integration.cooldownPeriodDays
val cryptoCurrencyNetworkIdValue = cryptoCurrencyStatusProvider().currency.network.rawId val cryptoCurrencyNetworkIdValue = cryptoCurrencyStatusProvider().currency.network.rawId
if (cooldownPeriodDays != null) { if (cooldownPeriodDays != null) {
@ -248,18 +249,17 @@ internal class StakingInfoNotificationsFactory(
val initialInfoState = prevState.initialInfoState as? StakingStates.InitialInfoState.Data val initialInfoState = prevState.initialInfoState as? StakingStates.InitialInfoState.Data
val stakingBalances = (initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data)?.balances 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() val stakesCountWithCertainTarget = stakingBalances.orEmpty()
.filter { .count { state ->
it.type == BalanceType.STAKED || (state.type == BalanceType.STAKED ||
it.type == BalanceType.PREPARING || state.type == BalanceType.PREPARING ||
it.type == BalanceType.UNSTAKED state.type == BalanceType.UNSTAKED) &&
state.target?.address == targetAddress
} }
.filter { it.validator?.address == validatorAddress }
.size
if (stakesCountWithCertainValidator > 1) { if (stakesCountWithCertainTarget > 1) {
add( add(
StakingNotification.Info.Ordinary( StakingNotification.Info.Ordinary(
title = resourceReference(R.string.staking_notification_ton_have_to_unstake_all_title), 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 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.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.models.staking.action.StakingActionType
import com.tangem.features.staking.impl.presentation.state.StakingStates 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 import com.tangem.utils.transformer.Transformer
internal class ValidatorSelectChangeTransformer( internal class ValidatorSelectChangeTransformer(
private val yield: Yield, private val integration: StakingIntegration,
private val selectedValidator: Yield.Validator?, private val selectedTarget: StakingTarget?,
) : Transformer<StakingUiState> { ) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState { override fun transform(prevState: StakingUiState): StakingUiState {
@ -23,27 +24,27 @@ internal class ValidatorSelectChangeTransformer(
val isFromInfoScreen = prevState.currentStep == StakingStep.InitialInfo val isFromInfoScreen = prevState.currentStep == StakingStep.InitialInfo
val isVoteLocked = confirmationState?.pendingAction?.type == StakingActionType.VOTE_LOCKED val isVoteLocked = confirmationState?.pendingAction?.type == StakingActionType.VOTE_LOCKED
val activeValidator = selectedValidator.takeIf { isFromInfoScreen && isRestake } val activeTarget = selectedTarget.takeIf { isFromInfoScreen && isRestake }
?: validatorState?.activeValidator ?: validatorState?.activeTarget
val filteredValidators = yield.preferredValidators.filterNot { it == activeValidator } val filteredTargets = integration.preferredTargets.filterNot { it == activeTarget }
val selectedValidator = if (isRestake && isFromInfoScreen) { val selectedTarget = if (isRestake && isFromInfoScreen) {
filteredValidators.firstOrNull() filteredTargets.firstOrNull()
} else { } else {
selectedValidator selectedTarget
} }
if (selectedValidator == null && yield.preferredValidators.isEmpty()) { if (selectedTarget == null && integration.preferredTargets.isEmpty()) {
return prevState return prevState
} }
return prevState.copy( return prevState.copy(
validatorState = StakingStates.ValidatorState.Data( validatorState = StakingStates.ValidatorState.Data(
chosenValidator = selectedValidator ?: yield.preferredValidators.first(), chosenTarget = selectedTarget ?: integration.preferredTargets.first(),
availableValidators = filteredValidators, availableTargets = filteredTargets,
isPrimaryButtonEnabled = true, isPrimaryButtonEnabled = true,
isClickable = yield.preferredValidators.size > 1, isClickable = integration.preferredTargets.size > 1,
activeValidator = activeValidator, activeTarget = activeTarget,
isVisibleOnConfirmation = isEnter || isRestake || isVoteLocked, isVisibleOnConfirmation = isEnter || isRestake || isVoteLocked,
), ),
) )

View file

@ -1,6 +1,7 @@
package com.tangem.features.staking.impl.presentation.state.utils package com.tangem.features.staking.impl.presentation.state.utils
import com.tangem.core.ui.extensions.* 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.domain.staking.model.stakekit.Yield
import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.utils.StakingRewardSchedule.COSMOS_SCHEDULE 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) { return when (rewardType) {
Yield.RewardType.APR -> TextReference.Res(R.string.staking_details_apr) RewardType.APR -> TextReference.Res(R.string.staking_details_apr)
Yield.RewardType.APY -> TextReference.Res(R.string.staking_details_apy) RewardType.APY -> TextReference.Res(R.string.staking_details_apy)
else -> TextReference.EMPTY else -> TextReference.EMPTY
} }
} }
internal fun getRewardTypeLongText(rewardType: Yield.RewardType): TextReference { internal fun getRewardTypeLongText(rewardType: RewardType): TextReference {
return when (rewardType) { return when (rewardType) {
Yield.RewardType.APR -> TextReference.Res(R.string.staking_details_annual_percentage_rate) RewardType.APR -> TextReference.Res(R.string.staking_details_annual_percentage_rate)
Yield.RewardType.APY -> TextReference.Res(R.string.staking_details_annual_percentage_yield) RewardType.APY -> TextReference.Res(R.string.staking_details_annual_percentage_yield)
else -> TextReference.EMPTY 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.format
import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.res.TangemTheme 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.model.StakingClickIntents
import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingStates
@ -30,7 +30,7 @@ internal fun StakingClaimRewardsValidatorContent(
) { ) {
if (state !is StakingStates.RewardsValidatorsState.Data) return if (state !is StakingStates.RewardsValidatorsState.Data) return
Column( 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) .background(TangemTheme.colors.background.secondary)
.padding(horizontal = TangemTheme.dimens.spacing12) .padding(horizontal = TangemTheme.dimens.spacing12)
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
@ -42,9 +42,9 @@ internal fun StakingClaimRewardsValidatorContent(
caption = item.getAprTextNeutral(), caption = item.getAprTextNeutral(),
infoTitle = item.formattedFiatAmount, infoTitle = item.formattedFiatAmount,
infoSubtitle = item.formattedCryptoAmount, infoSubtitle = item.formattedCryptoAmount,
imageUrl = item.validator?.image.orEmpty(), imageUrl = item.target?.image.orEmpty(),
onImageError = { ValidatorImagePlaceholder() }, onImageError = { ValidatorImagePlaceholder() },
modifier = modifier modifier = Modifier
.roundedShapeItemDecoration(index, state.rewards.lastIndex, false) .roundedShapeItemDecoration(index, state.rewards.lastIndex, false)
.background(TangemTheme.colors.background.action) .background(TangemTheme.colors.background.action)
.clickable( .clickable(
@ -65,11 +65,11 @@ internal fun StakingClaimRewardsValidatorContent(
@Suppress("UnusedPrivateMember") @Suppress("UnusedPrivateMember")
@Composable @Composable
private fun BalanceState.getAprTextColored() = combinedReference( private fun BalanceState.getAprTextColored() = combinedReference(
getRewardTypeShortText(validator?.rewardInfo?.type ?: Yield.RewardType.UNKNOWN), getRewardTypeShortText(target?.rewardInfo?.type ?: RewardType.UNKNOWN),
annotatedReference { annotatedReference {
appendSpace() appendSpace()
appendColored( appendColored(
text = validator?.rewardInfo?.rate?.orZero().format { percent() }, text = target?.rewardInfo?.rate?.orZero().format { percent() },
color = TangemTheme.colors.text.accent, color = TangemTheme.colors.text.accent,
) )
}, },
@ -77,6 +77,6 @@ private fun BalanceState.getAprTextColored() = combinedReference(
@Composable @Composable
private fun BalanceState.getAprTextNeutral() = combinedReference( private fun BalanceState.getAprTextNeutral() = combinedReference(
getRewardTypeShortText(validator?.rewardInfo?.type ?: Yield.RewardType.UNKNOWN), getRewardTypeShortText(target?.rewardInfo?.type ?: RewardType.UNKNOWN),
stringReference(" " + validator?.rewardInfo?.rate?.orZero().format { percent() }), 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.core.ui.test.StakingDetailsScreenTestTags
import com.tangem.domain.models.staking.BalanceType import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.RewardBlockType 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.R
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents 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.BalanceState
@ -317,11 +317,11 @@ private fun StakeButtonBlock(buttonState: NavigationButtonsState) {
@Suppress("UnusedPrivateMember") @Suppress("UnusedPrivateMember")
@Composable @Composable
private fun BalanceState.getAprTextColored() = combinedReference( private fun BalanceState.getAprTextColored() = combinedReference(
getRewardTypeShortText(validator?.rewardInfo?.type ?: Yield.RewardType.UNKNOWN), getRewardTypeShortText(target?.rewardInfo?.type ?: RewardType.UNKNOWN),
annotatedReference { annotatedReference {
appendSpace() appendSpace()
appendColored( appendColored(
text = validator?.rewardInfo?.rate?.orZero().format { percent() }, text = target?.rewardInfo?.rate?.orZero().format { percent() },
color = TangemTheme.colors.text.accent, color = TangemTheme.colors.text.accent,
) )
}, },
@ -329,8 +329,8 @@ private fun BalanceState.getAprTextColored() = combinedReference(
@Composable @Composable
private fun BalanceState.getAprTextNeutral() = combinedReference( private fun BalanceState.getAprTextNeutral() = combinedReference(
getRewardTypeShortText(validator?.rewardInfo?.type ?: Yield.RewardType.UNKNOWN), getRewardTypeShortText(target?.rewardInfo?.type ?: RewardType.UNKNOWN),
stringReference(" " + validator?.rewardInfo?.rate?.orZero().format { percent() }), stringReference(" " + target?.rewardInfo?.rate?.orZero().format { percent() }),
) )
@Composable @Composable
@ -347,7 +347,7 @@ private fun BalanceState.getImage() = when (type) {
BalanceType.UNSTAKED, BalanceType.UNSTAKED,
BalanceType.LOCKED, BalanceType.LOCKED,
-> null -> null
else -> validator?.image else -> target?.image
} }
private val textGradientColors = listOf( private val textGradientColors = listOf(

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