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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,19 +1,18 @@
package com.tangem.domain.staking.model
import com.tangem.domain.models.serialization.SerializedBigDecimal
import com.tangem.domain.models.staking.NetworkType
import com.tangem.domain.models.staking.YieldToken
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
import com.tangem.domain.staking.model.stakekit.Yield
/**
* Represents a staking option from any provider
* Unified abstraction over StakeKit and P2P staking integrations
* Unified abstraction over StakeKit and P2PEthPool staking integrations
*/
sealed interface StakingOption {
/** Unique identifier for the staking option */
val integrationId: String
val integrationId: StakingIntegrationID
/** Annual Percentage Yield */
val apy: SerializedBigDecimal
@ -28,34 +27,23 @@ sealed interface StakingOption {
* StakeKit staking option
* Wraps StakeKit Yield with all validator and metadata information
*/
data class StakeKit(val yield: Yield) : StakingOption {
override val integrationId: String = yield.id
data class StakeKit(
override val integrationId: StakingIntegrationID.StakeKit,
val yield: Yield,
) : StakingOption {
override val apy: SerializedBigDecimal = yield.apy
override val token: YieldToken = yield.token
override val isAvailable: Boolean = yield.isAvailable
}
/**
* P2P pooled staking option
* Wraps P2P ETH Pool vault information
* P2PEthPool staking option
* Wraps P2PEthPool vault information
*/
data class P2P(val vaults: List<P2PEthPoolVault>) : StakingOption {
override val integrationId: String = "p2p-ethereum-pooled"
data class P2PEthPool(val vaults: List<P2PEthPoolVault>) : StakingOption {
override val integrationId: StakingIntegrationID = StakingIntegrationID.P2PEthPool
override val apy: SerializedBigDecimal = vaults.maxOf { it.apy }
override val token: YieldToken = createEthToken()
override val token: YieldToken = YieldToken.ETH
override val isAvailable: Boolean = vaults.isNotEmpty()
private fun createEthToken(): YieldToken { // TODO
return YieldToken(
name = "Ethereum",
network = NetworkType.ETHEREUM,
symbol = "ETH",
decimals = 18,
address = null, // Native token
coinGeckoId = "ethereum",
logoURI = null,
isPoints = false,
)
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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