Updated on 2026-08-14
This commit is contained in:
commit
e388c14eb9
184 changed files with 4338 additions and 1298 deletions
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.domain.staking.model.common
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class RewardClaiming {
|
||||
AUTO,
|
||||
MANUAL,
|
||||
UNKNOWN,
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.staking.model.common
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class RewardSchedule {
|
||||
BLOCK,
|
||||
HOUR,
|
||||
DAY,
|
||||
WEEK,
|
||||
MONTH,
|
||||
ERA,
|
||||
EPOCH,
|
||||
UNKNOWN,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.domain.staking.model.common
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Provider-agnostic representation of staking action arguments.
|
||||
* Contains amount requirements and constraints for enter/exit operations.
|
||||
*
|
||||
* Maps from:
|
||||
* - StakeKit: Yield.Args.Enter
|
||||
* - P2PEthPool: P2PEthPoolStaking.Metadata
|
||||
*/
|
||||
@Serializable
|
||||
data class StakingActionArgs(
|
||||
val amountRequirement: StakingAmountRequirement?,
|
||||
val isPartialAmountDisabled: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.domain.staking.model.common
|
||||
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Provider-agnostic representation of staking amount requirements.
|
||||
* Contains validation constraints for stake/unstake amounts.
|
||||
*
|
||||
* Maps from:
|
||||
* - StakeKit: AddressArgument with ArgType.AMOUNT
|
||||
* - P2PEthPool: P2PEthPoolStaking.Metadata minimumStake/maximumStake
|
||||
*/
|
||||
@Serializable
|
||||
data class StakingAmountRequirement(
|
||||
val isRequired: Boolean,
|
||||
val minimum: SerializedBigDecimal? = null,
|
||||
val maximum: SerializedBigDecimal? = null,
|
||||
)
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class GetStakingEntryInfoUseCase(
|
|||
symbol = symbol,
|
||||
)
|
||||
}
|
||||
is StakingOption.P2P -> {
|
||||
is StakingOption.P2PEthPool -> {
|
||||
StakingEntryInfo(
|
||||
tokenSymbol = "ETH",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
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.common.RewardClaiming
|
||||
import com.tangem.domain.staking.model.common.RewardSchedule
|
||||
import com.tangem.domain.staking.model.common.StakingActionArgs
|
||||
import com.tangem.domain.staking.model.common.StakingAmountRequirement
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
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: StakingActionArgs = StakingActionArgs(
|
||||
amountRequirement = StakingAmountRequirement(
|
||||
isRequired = true,
|
||||
minimum = DEFAULT_MINIMUM_STAKE,
|
||||
maximum = null,
|
||||
),
|
||||
isPartialAmountDisabled = false,
|
||||
)
|
||||
|
||||
override val exitArgs: StakingActionArgs = StakingActionArgs(
|
||||
amountRequirement = StakingAmountRequirement(
|
||||
isRequired = true,
|
||||
minimum = null,
|
||||
maximum = null,
|
||||
),
|
||||
isPartialAmountDisabled = false,
|
||||
)
|
||||
|
||||
// Metadata
|
||||
|
||||
override val warmupPeriodDays: Int = 0
|
||||
|
||||
override val cooldownPeriodDays: Int = DEFAULT_COOLDOWN_DAYS
|
||||
|
||||
override val rewardSchedule: RewardSchedule = RewardSchedule.DAY
|
||||
|
||||
override val rewardClaiming: RewardClaiming = 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
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.common.RewardClaiming
|
||||
import com.tangem.domain.staking.model.common.RewardSchedule
|
||||
import com.tangem.domain.staking.model.common.StakingActionArgs
|
||||
import com.tangem.domain.staking.model.common.StakingAmountRequirement
|
||||
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: StakingActionArgs = yield.args.enter.toStakingActionArgs()
|
||||
|
||||
override val exitArgs: StakingActionArgs? = yield.args.exit?.toStakingActionArgs()
|
||||
|
||||
// Metadata
|
||||
|
||||
override val warmupPeriodDays: Int = yield.metadata.warmupPeriod.days
|
||||
|
||||
override val cooldownPeriodDays: Int? = yield.metadata.cooldownPeriod?.days
|
||||
|
||||
override val rewardSchedule: RewardSchedule = yield.metadata.rewardSchedule.toRewardSchedule()
|
||||
|
||||
override val rewardClaiming: RewardClaiming = yield.metadata.rewardClaiming.toRewardClaiming()
|
||||
|
||||
// Basic
|
||||
|
||||
override fun getCurrentToken(rawCurrencyId: CryptoCurrency.RawID?): YieldToken =
|
||||
tokens.firstOrNull { rawCurrencyId?.value == it.coinGeckoId } ?: token
|
||||
|
||||
private fun Yield.Args.Enter.toStakingActionArgs(): StakingActionArgs {
|
||||
val amountArg = args[Yield.Args.ArgType.AMOUNT]
|
||||
return StakingActionArgs(
|
||||
amountRequirement = amountArg?.let { arg ->
|
||||
StakingAmountRequirement(
|
||||
isRequired = arg.required,
|
||||
minimum = arg.minimum,
|
||||
maximum = arg.maximum,
|
||||
)
|
||||
},
|
||||
isPartialAmountDisabled = isPartialAmountDisabled,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Yield.Metadata.RewardSchedule.toRewardSchedule(): RewardSchedule {
|
||||
return when (this) {
|
||||
Yield.Metadata.RewardSchedule.BLOCK -> RewardSchedule.BLOCK
|
||||
Yield.Metadata.RewardSchedule.HOUR -> RewardSchedule.HOUR
|
||||
Yield.Metadata.RewardSchedule.DAY -> RewardSchedule.DAY
|
||||
Yield.Metadata.RewardSchedule.WEEK -> RewardSchedule.WEEK
|
||||
Yield.Metadata.RewardSchedule.MONTH -> RewardSchedule.MONTH
|
||||
Yield.Metadata.RewardSchedule.ERA -> RewardSchedule.ERA
|
||||
Yield.Metadata.RewardSchedule.EPOCH -> RewardSchedule.EPOCH
|
||||
Yield.Metadata.RewardSchedule.UNKNOWN -> RewardSchedule.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
private fun Yield.Metadata.RewardClaiming.toRewardClaiming(): RewardClaiming {
|
||||
return when (this) {
|
||||
Yield.Metadata.RewardClaiming.AUTO -> RewardClaiming.AUTO
|
||||
Yield.Metadata.RewardClaiming.MANUAL -> RewardClaiming.MANUAL
|
||||
Yield.Metadata.RewardClaiming.UNKNOWN -> RewardClaiming.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
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.common.RewardClaiming
|
||||
import com.tangem.domain.staking.model.common.RewardSchedule
|
||||
import com.tangem.domain.staking.model.common.StakingActionArgs
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Strategy interface for staking integrations.
|
||||
* Abstracts over StakeKit and P2PEthPool staking providers.
|
||||
*/
|
||||
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: StakingActionArgs?
|
||||
|
||||
val exitArgs: StakingActionArgs?
|
||||
|
||||
// Metadata
|
||||
|
||||
val warmupPeriodDays: Int
|
||||
|
||||
val cooldownPeriodDays: Int?
|
||||
|
||||
val rewardSchedule: RewardSchedule
|
||||
|
||||
val rewardClaiming: RewardClaiming
|
||||
|
||||
// Basic
|
||||
|
||||
fun getCurrentToken(rawCurrencyId: CryptoCurrency.RawID?): YieldToken
|
||||
}
|
||||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue