Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-30 11:46:31 +03:00
commit 14a7ac4f5e
431 changed files with 23077 additions and 3701 deletions

View file

@ -16,6 +16,7 @@ import java.math.BigDecimal
* @property privacyPolicy privacy policy link
* @property isRecommended flag that indicates if this provider is recommended
* @property slippage provider slippage
* @property isExchangeOnlyWithinSingleAddress flag that indicates if exchange is only allowed within a single address
*
* Uses to store transaction data in datastore, when extends - should always add default value
* to support backward compatibility
@ -40,4 +41,6 @@ data class ExpressProvider(
val isRecommended: Boolean = false,
@Json(name = "slippage")
val slippage: BigDecimal?,
@Json(name = "exchangeOnlyWithinSingleAddress")
val isExchangeOnlyWithinSingleAddress: Boolean = false,
)

View file

@ -18,6 +18,9 @@ sealed interface FeedbackEmailType {
/** User rate the app as "can be better" */
data class RateCanBeBetter(override val walletMetaInfo: WalletMetaInfo) : FeedbackEmailType
/** User has problem with backup */
data class BackupProblem(override val walletMetaInfo: WalletMetaInfo) : FeedbackEmailType
/** User has problem with scanning */
data object ScanningProblem : FeedbackEmailType {
override val walletMetaInfo: WalletMetaInfo? = null

View file

@ -97,6 +97,7 @@ class SendFeedbackEmailUseCase(
is FeedbackEmailType.Visa.FeatureIsBeta,
-> this
is FeedbackEmailType.DirectUserRequest,
is FeedbackEmailType.BackupProblem,
is FeedbackEmailType.RateCanBeBetter,
is FeedbackEmailType.StakingProblem,
is FeedbackEmailType.SwapProblem,

View file

@ -28,6 +28,7 @@ internal class EmailMessageBodyResolver(
is FeedbackEmailType.SwapProblem -> addSwapProblemBody(type)
is FeedbackEmailType.CurrencyDescriptionError -> addTokenInfo(type)
is FeedbackEmailType.PreActivatedWallet -> addUserRequestBody(type.walletMetaInfo)
is FeedbackEmailType.BackupProblem -> addUserRequestBody(type.walletMetaInfo)
is FeedbackEmailType.ScanningProblem,
is FeedbackEmailType.CardAttestationFailed,
-> addPhoneInfoBody()

View file

@ -28,6 +28,7 @@ internal class EmailMessageTitleResolver(private val resources: Resources) {
is FeedbackEmailType.Visa.Withdrawal,
is FeedbackEmailType.Visa.FeatureIsBeta,
is FeedbackEmailType.PreActivatedWallet,
is FeedbackEmailType.BackupProblem,
-> R.string.feedback_preface_support
is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative
is FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed

View file

@ -25,6 +25,7 @@ internal class EmailSubjectResolver(private val resources: Resources) {
resources.getStringSafe(R.string.feedback_subject_support_tangem)
}
}
is FeedbackEmailType.BackupProblem -> resources.getStringSafe(R.string.feedback_subject_backup_problem)
is FeedbackEmailType.RateCanBeBetter -> resources.getStringSafe(R.string.feedback_subject_rate_negative)
is FeedbackEmailType.ScanningProblem -> resources.getStringSafe(R.string.feedback_subject_scan_failed)
is FeedbackEmailType.TransactionSendingProblem,

View file

@ -8,7 +8,7 @@ import kotlinx.serialization.Serializable
[REDACTED_AUTHOR]
* @param id - unique identifier of the article
* @param title - article title
* @param sourceName - name of original article source
* @param source - object of source name and identifier
* @param locale - language of the article
* @param publishedAt - date of article publishing
* @param url - link to source of original article
@ -18,9 +18,15 @@ import kotlinx.serialization.Serializable
data class OriginalArticle(
val id: Int,
val title: String,
val sourceName: String,
val source: Source,
val locale: String,
val publishedAt: String,
val url: String,
val imageUrl: String?,
)
@Serializable
data class Source(
val id: Int,
val name: String,
)

View file

@ -0,0 +1,27 @@
package com.tangem.domain.models.staking
import com.tangem.domain.models.staking.StakingEntryType.Companion.fromBalanceType
fun BalanceItem.toStakingBalanceEntry(validatorName: String? = null): StakingBalanceEntry {
return StakingBalanceEntry(
id = groupId,
type = fromBalanceType(type),
amount = amount,
validator = validatorAddress?.let {
ValidatorInfo(address = it, name = validatorName)
},
date = date,
actions = StakingEntryActions.StakeKit(
pendingActions = pendingActions,
pendingActionsConstraints = pendingActionsConstraints,
),
isPending = isPending,
rawCurrencyId = rawCurrencyId,
)
}
fun List<BalanceItem>.toStakingBalanceEntries(
validatorNameResolver: (String?) -> String? = { null },
): List<StakingBalanceEntry> {
return map { it.toStakingBalanceEntry(validatorNameResolver(it.validatorAddress)) }
}

View file

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

View file

@ -0,0 +1,86 @@
package com.tangem.domain.models.staking
import java.math.BigDecimal
fun P2PEthPoolStakingAccount.toStakingBalanceEntries(vaultName: String? = null): List<StakingBalanceEntry> {
return buildList {
if (stake.assets > BigDecimal.ZERO) {
add(createStakedEntry(vaultAddress, stake.assets, vaultName))
}
exitQueue.requests.filter { !it.isClaimable }.forEach { add(createUnstakingEntry(vaultAddress, it, vaultName)) }
if (availableToWithdraw > BigDecimal.ZERO) {
add(createWithdrawableEntry(vaultAddress, availableToWithdraw, vaultName))
}
if (stake.totalEarnedAssets > BigDecimal.ZERO) {
add(createRewardsEntry(vaultAddress, stake.totalEarnedAssets, vaultName))
}
}
}
private fun createStakedEntry(vaultAddress: String, amount: BigDecimal, vaultName: String?): StakingBalanceEntry {
return StakingBalanceEntry(
id = vaultAddress,
type = StakingEntryType.STAKED,
amount = amount,
validator = ValidatorInfo(address = vaultAddress, name = vaultName),
date = null,
actions = StakingEntryActions.P2PEthPool(ticket = null, estimatedWithdrawalDate = null, isClaimable = false),
isPending = false,
rawCurrencyId = null,
)
}
private fun createUnstakingEntry(
vaultAddress: String,
request: P2PEthPoolExitRequest,
vaultName: String?,
): StakingBalanceEntry {
return StakingBalanceEntry(
id = "${vaultAddress}_${request.ticket}",
type = StakingEntryType.UNSTAKING,
amount = request.totalAssets,
validator = ValidatorInfo(address = vaultAddress, name = vaultName),
date = request.withdrawalTimestamp,
actions = StakingEntryActions.P2PEthPool(
ticket = request.ticket,
estimatedWithdrawalDate = request.withdrawalTimestamp,
isClaimable = false,
),
isPending = false,
rawCurrencyId = null,
)
}
private fun createWithdrawableEntry(
vaultAddress: String,
amount: BigDecimal,
vaultName: String?,
): StakingBalanceEntry {
return StakingBalanceEntry(
id = "${vaultAddress}_withdrawable",
type = StakingEntryType.WITHDRAWABLE,
amount = amount,
validator = ValidatorInfo(address = vaultAddress, name = vaultName),
date = null,
actions = StakingEntryActions.P2PEthPool(
ticket = null,
estimatedWithdrawalDate = null,
isClaimable = true,
),
isPending = false,
rawCurrencyId = null,
)
}
private fun createRewardsEntry(vaultAddress: String, amount: BigDecimal, vaultName: String?): StakingBalanceEntry {
return StakingBalanceEntry(
id = "${vaultAddress}_rewards",
type = StakingEntryType.REWARDS,
amount = amount,
validator = ValidatorInfo(address = vaultAddress, name = vaultName),
date = null,
actions = StakingEntryActions.P2PEthPool(ticket = null, estimatedWithdrawalDate = null, isClaimable = false),
isPending = false,
rawCurrencyId = null,
)
}

View file

@ -1,11 +1,12 @@
package com.tangem.domain.models.staking
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.serialization.SerializedBigDecimal
import kotlinx.serialization.Serializable
import java.math.BigDecimal
/**
* Staking balance facade covering StakeKit and P2P balances
* Staking balance facade covering StakeKit and P2PEthPool balances
*/
@Serializable
sealed interface StakingBalance {
@ -21,6 +22,9 @@ sealed interface StakingBalance {
@Serializable
sealed interface Data : StakingBalance {
/** Provider-agnostic list of balance entries for UI display */
val entries: List<StakingBalanceEntry>
@Serializable
data class StakeKit(
override val stakingId: StakingID,
@ -28,45 +32,41 @@ sealed interface StakingBalance {
val balance: YieldBalanceItem,
) : Data {
override val totalStaked: BigDecimal
get() = balance.items
.filter { it.type == BalanceType.STAKED }
.sumOf { it.amount }
override val totalStaked: SerializedBigDecimal = balance.items
.filter { it.type == BalanceType.STAKED }
.sumOf { it.amount }
override val totalRewards: BigDecimal
get() = balance.items
.filter { it.type == BalanceType.REWARDS }
.sumOf { it.amount }
override val totalRewards: SerializedBigDecimal = balance.items
.filter { it.type == BalanceType.REWARDS }
.sumOf { it.amount }
override val unstakingAmount: BigDecimal
get() = balance.items
.filter { it.type == BalanceType.UNSTAKING || it.type == BalanceType.UNLOCKING }
.sumOf { it.amount }
override val unstakingAmount: SerializedBigDecimal = balance.items
.filter { it.type == BalanceType.UNSTAKING || it.type == BalanceType.UNLOCKING }
.sumOf { it.amount }
override val withdrawableAmount: BigDecimal
get() = balance.items
.filter { it.type == BalanceType.UNSTAKED }
.sumOf { it.amount }
override val withdrawableAmount: SerializedBigDecimal = balance.items
.filter { it.type == BalanceType.UNSTAKED }
.sumOf { it.amount }
override val entries: List<StakingBalanceEntry> = balance.items.toStakingBalanceEntries()
}
@Serializable
data class P2P(
data class P2PEthPool(
override val stakingId: StakingID,
override val source: StatusSource,
val account: P2PStakingAccount,
val account: P2PEthPoolStakingAccount,
) : Data {
override val totalStaked: BigDecimal
get() = account.stake.assets
override val totalStaked: SerializedBigDecimal = account.stake.assets
override val totalRewards: BigDecimal
get() = account.stake.totalEarnedAssets
override val totalRewards: SerializedBigDecimal = account.stake.totalEarnedAssets
override val unstakingAmount: BigDecimal
get() = account.exitQueue.total
override val unstakingAmount: SerializedBigDecimal = account.exitQueue.total
override val withdrawableAmount: BigDecimal
get() = account.availableToWithdraw
override val withdrawableAmount: SerializedBigDecimal = account.availableToWithdraw
override val entries: List<StakingBalanceEntry> = account.toStakingBalanceEntries()
}
}
@ -93,7 +93,7 @@ sealed interface StakingBalance {
fun copySealed(source: StatusSource): StakingBalance {
return when (this) {
is Data.StakeKit -> copy(source = source)
is Data.P2P -> copy(source = source)
is Data.P2PEthPool -> copy(source = source)
is Empty -> copy(source = source)
is Error -> this
}

View file

@ -0,0 +1,23 @@
package com.tangem.domain.models.staking
import com.tangem.domain.models.serialization.SerializedBigDecimal
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
@Serializable
data class StakingBalanceEntry(
val id: String,
val type: StakingEntryType,
val amount: SerializedBigDecimal,
val validator: ValidatorInfo?,
val date: Instant?,
val actions: StakingEntryActions,
val isPending: Boolean,
val rawCurrencyId: String?,
)
@Serializable
data class ValidatorInfo(
val address: String,
val name: String?,
)

View file

@ -0,0 +1,23 @@
package com.tangem.domain.models.staking
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
@Serializable
sealed interface StakingEntryActions {
@Serializable
data class StakeKit(
val pendingActions: List<PendingAction>,
val pendingActionsConstraints: List<PendingActionConstraints>,
) : StakingEntryActions {
val hasPendingActions: Boolean get() = pendingActions.isNotEmpty()
}
@Serializable
data class P2PEthPool(
val ticket: String?,
val estimatedWithdrawalDate: Instant?,
val isClaimable: Boolean,
) : StakingEntryActions
}

View file

@ -0,0 +1,31 @@
package com.tangem.domain.models.staking
import kotlinx.serialization.Serializable
@Serializable
enum class StakingEntryType {
AVAILABLE,
STAKED,
PREPARING,
LOCKED,
UNSTAKING,
UNLOCKING,
WITHDRAWABLE,
REWARDS,
UNKNOWN,
;
companion object {
fun fromBalanceType(type: BalanceType): StakingEntryType = when (type) {
BalanceType.AVAILABLE -> AVAILABLE
BalanceType.STAKED -> STAKED
BalanceType.PREPARING -> PREPARING
BalanceType.LOCKED -> LOCKED
BalanceType.UNSTAKING -> UNSTAKING
BalanceType.UNLOCKING -> UNLOCKING
BalanceType.UNSTAKED -> WITHDRAWABLE // stakekit's UNSTAKED = ready to withdraw
BalanceType.REWARDS -> REWARDS
BalanceType.UNKNOWN -> UNKNOWN
}
}
}

View file

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

View file

@ -15,7 +15,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class NewsListConfig(
val language: String,
val snapshot: String,
val snapshot: String?,
val tokenIds: List<String> = emptyList(),
val categoryIds: List<Int> = emptyList(),
)

View file

@ -2,9 +2,11 @@ package com.tangem.domain.news.repository
import com.tangem.domain.models.news.ArticleCategory
import com.tangem.domain.models.news.DetailedArticle
import com.tangem.domain.models.news.ShortArticle
import com.tangem.domain.models.news.TrendingNews
import com.tangem.domain.news.model.NewsListBatchFlow
import com.tangem.domain.news.model.NewsListBatchingContext
import com.tangem.domain.news.model.NewsListConfig
import kotlinx.coroutines.flow.Flow
/**
@ -20,6 +22,13 @@ interface NewsRepository {
*/
fun getNewsListBatchFlow(context: NewsListBatchingContext, batchSize: Int): NewsListBatchFlow
/**
* Returns list of short article by config.
*
* @param config config for getting news list
*/
suspend fun getNews(config: NewsListConfig, limit: Int): List<ShortArticle>
/**
* Returns detailed article by id with locale configuration.
* @param newsId news identification
@ -50,13 +59,13 @@ interface NewsRepository {
*/
fun observeTrendingNews(): Flow<TrendingNews>
/**
* Updates viewed flag for provided trending articles.
*/
suspend fun updateTrendingNewsViewed(articleIds: Collection<Int>, viewed: Boolean)
/**
* Returns available categories.
*/
suspend fun getCategories(): List<ArticleCategory>
/**
* Updates viewed flag for provided news articles (applies to both regular and trending news).
*/
suspend fun updateNewsViewed(articleIds: Collection<Int>, viewed: Boolean)
}

View file

@ -1,5 +1,6 @@
package com.tangem.domain.news.usecase
import arrow.core.Either
import com.tangem.domain.models.news.ArticleCategory
import com.tangem.domain.news.repository.NewsRepository
@ -15,7 +16,7 @@ class GetNewsCategoriesUseCase(
/**
* Fetches categories from the repository.
*/
suspend operator fun invoke(): List<ArticleCategory> {
return repository.getCategories()
suspend operator fun invoke(): Either<Throwable, List<ArticleCategory>> = Either.catch {
repository.getCategories()
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.news.usecase
import arrow.core.Either
import com.tangem.domain.models.news.ShortArticle
import com.tangem.domain.news.model.NewsListConfig
import com.tangem.domain.news.repository.NewsRepository
class GetNewsUseCase(private val repository: NewsRepository) {
suspend fun getNews(limit: Int, newsListConfig: NewsListConfig): Either<Throwable, List<ShortArticle>> =
Either.catch {
repository.getNews(
config = newsListConfig,
limit = limit,
)
}
}

View file

@ -20,18 +20,4 @@ class ManageTrendingNewsUseCase(private val repository: NewsRepository) {
.observeTrendingNews()
.distinctUntilChanged()
}
/**
* Marks a single article as viewed/unviewed.
*/
suspend fun markAsViewed(articleId: Int, viewed: Boolean = true) {
repository.updateTrendingNewsViewed(listOf(articleId), viewed)
}
/**
* Marks multiple articles at once (useful for bulk updates).
*/
suspend fun markAsViewed(articleIds: Collection<Int>, viewed: Boolean = true) {
repository.updateTrendingNewsViewed(articleIds, viewed)
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.domain.news.usecase
import com.tangem.domain.news.repository.NewsRepository
class MarkArticleAsViewedUseCase(private val repository: NewsRepository) {
/**
* Marks a single article as viewed/unviewed.
*/
suspend fun markAsViewed(articleId: Int, viewed: Boolean = true) {
repository.updateNewsViewed(listOf(articleId), viewed)
}
/**
* Marks multiple articles at once (useful for bulk updates).
*/
suspend fun markAsViewed(articleIds: Collection<Int>, viewed: Boolean = true) {
repository.updateNewsViewed(articleIds, viewed)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.domain.news.usecase
import arrow.core.Either
import com.tangem.domain.models.news.DetailedArticle
import com.tangem.domain.news.repository.NewsRepository
import kotlinx.coroutines.flow.Flow
@ -23,7 +24,7 @@ class ObserveNewsDetailsUseCase(
/**
* Prefetches the given article ids (can be called with current + next ids for pager preloading).
*/
suspend fun prefetch(newsIds: Collection<Int>, language: String?) {
suspend fun prefetch(newsIds: Collection<Int>, language: String?): Either<Throwable, Unit> = Either.catch {
repository.fetchDetailedArticles(newsIds, language)
}
}

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 kotlinx.serialization.Serializable
@Serializable
enum class RewardClaiming {
AUTO,
MANUAL,
UNKNOWN,
}

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,15 @@
package com.tangem.domain.staking.model.common
import kotlinx.serialization.Serializable
@Serializable
enum class RewardSchedule {
BLOCK,
HOUR,
DAY,
WEEK,
MONTH,
ERA,
EPOCH,
UNKNOWN,
}

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

@ -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,
)

View file

@ -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,
)

View file

@ -1,44 +0,0 @@
package com.tangem.domain.staking.model.ethpool
import com.tangem.domain.models.serialization.SerializedBigDecimal
import org.joda.time.Instant
/**
* P2P.org account staking information
* Contains detailed balance and exit queue information
*/
data class P2PEthPoolAccount(
val delegatorAddress: String,
val vaultAddress: String,
val stake: P2PEthPoolStake,
val availableToUnstake: SerializedBigDecimal,
val availableToWithdraw: SerializedBigDecimal,
val exitQueue: P2PEthPoolExitQueue,
)
/**
* Current stake information
*/
data class P2PEthPoolStake(
val assets: SerializedBigDecimal,
val totalEarnedAssets: SerializedBigDecimal,
)
/**
* Exit queue information
*/
data class P2PEthPoolExitQueue(
val total: SerializedBigDecimal,
val requests: List<P2PEthPoolExitRequest>,
)
/**
* Individual exit request in the queue
*/
data class P2PEthPoolExitRequest(
val ticket: String,
val totalAssets: SerializedBigDecimal,
val timestamp: Instant,
val withdrawalTimestamp: Instant,
val isClaimable: Boolean,
)

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,8 @@
package com.tangem.domain.staking.model
sealed class CooldownPeriod {
data class Fixed(val days: Int) : CooldownPeriod()
data class Range(val minDays: Int, val maxDays: Int) : CooldownPeriod()
}

View file

@ -0,0 +1,92 @@
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,
private val 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 = calculateMaximumStakeAmount(),
),
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 cooldownPeriod: CooldownPeriod = CooldownPeriod.Range(
minDays = MIN_COOLDOWN_DAYS,
maxDays = MAX_COOLDOWN_DAYS,
)
override val rewardSchedule: RewardSchedule = RewardSchedule.DAY
override val rewardClaiming: RewardClaiming = RewardClaiming.AUTO
override fun getCurrentToken(rawCurrencyId: CryptoCurrency.RawID?): YieldToken = token
private fun calculateMaximumStakeAmount(): BigDecimal? {
return vaults
.mapNotNull { vault ->
val availableCapacity = vault.capacity - vault.totalAssets
if (availableCapacity > BigDecimal.ZERO) availableCapacity else null
}
.maxOrNull()
}
companion object {
private const val MIN_COOLDOWN_DAYS = 1
private const val MAX_COOLDOWN_DAYS = 4
private val DEFAULT_MINIMUM_STAKE = BigDecimal("0.01")
}
}

View file

@ -0,0 +1,101 @@
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 cooldownPeriod: CooldownPeriod? = yield.metadata.cooldownPeriod?.days?.let {
CooldownPeriod.Fixed(it)
}
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
}
}
}

View file

@ -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.
*/
sealed 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 cooldownPeriod: CooldownPeriod?
val rewardSchedule: RewardSchedule
val rewardClaiming: 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

@ -1,14 +1,14 @@
package com.tangem.domain.staking.repositories
import arrow.core.Either
import com.tangem.domain.models.staking.P2PEthPoolStakingAccount
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.ethpool.P2PEthPoolAccount
import com.tangem.domain.staking.model.ethpool.P2PEthPoolBroadcastResult
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,15 +53,17 @@ 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 stakerPublicKey Staker's public key (note: API doc may have Bitcoin terminology)
* @param stakeTransactionHash Original stake transaction hash
* @param network P2PEthPool network (MAINNET or TESTNET)
* @param delegatorAddress User's wallet address
* @param vaultAddress Vault contract address
* @param amount Amount of ETH to unstake
* @return Either error or unsigned transaction
*/
suspend fun createUnstakeTransaction(
network: P2PEthPoolNetwork,
stakerPublicKey: String,
stakeTransactionHash: String,
delegatorAddress: String,
vaultAddress: String,
amount: String,
): Either<StakingError, P2PEthPoolUnsignedTx>
/**
@ -69,19 +71,23 @@ interface P2PEthPoolRepository {
*
* Only works when funds are available (after exit queue wait period).
*
* @param network P2P network (MAINNET or TESTNET)
* @param stakerAddress User's wallet address
* @param network P2PEthPool network (MAINNET or TESTNET)
* @param delegatorAddress User's wallet address
* @param vaultAddress Vault contract address
* @param amount Amount of ETH to withdraw
* @return Either error or unsigned transaction with withdrawal tickets
*/
suspend fun createWithdrawTransaction(
network: P2PEthPoolNetwork,
stakerAddress: String,
delegatorAddress: String,
vaultAddress: String,
amount: String,
): Either<StakingError, P2PEthPoolUnsignedTx>
/**
* 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 +101,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
@ -104,12 +110,12 @@ interface P2PEthPoolRepository {
network: P2PEthPoolNetwork,
delegatorAddress: String,
vaultAddress: String,
): Either<StakingError, P2PEthPoolAccount>
): Either<StakingError, P2PEthPoolStakingAccount>
/**
* 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 +129,37 @@ 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>>
/**
* Get cached vaults synchronously from local store.
*
* This returns vaults from the local cache/store without network call.
* Call [fetchVaults] first to populate the cache from the network.
*
* @return List of cached vaults (empty if cache is not populated)
*/
suspend fun getVaultsSync(): 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

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

View file

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

View file

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

View file

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

View file

@ -428,10 +428,10 @@ class CryptoCurrencyStatusFactoryTest {
source = StatusSource.ACTUAL,
balance = YieldBalanceItem(
items = listOf(
mockk<BalanceItem> {
mockk<BalanceItem>(relaxed = true) {
every { this@mockk.token.coinGeckoId } returns currency.id.rawCurrencyId?.value
},
mockk<BalanceItem> {
mockk<BalanceItem>(relaxed = true) {
every { this@mockk.token.coinGeckoId } returns "unknown"
},
),
@ -525,10 +525,10 @@ class CryptoCurrencyStatusFactoryTest {
source = StatusSource.ACTUAL,
balance = YieldBalanceItem(
items = listOf(
mockk<BalanceItem> {
mockk<BalanceItem>(relaxed = true) {
every { this@mockk.token.coinGeckoId } returns currency.id.rawCurrencyId?.value
},
mockk<BalanceItem> {
mockk<BalanceItem>(relaxed = true) {
every { this@mockk.token.coinGeckoId } returns "unknown"
},
),
@ -614,10 +614,10 @@ class CryptoCurrencyStatusFactoryTest {
source = StatusSource.ACTUAL,
balance = YieldBalanceItem(
items = listOf(
mockk<BalanceItem> {
mockk<BalanceItem>(relaxed = true) {
every { this@mockk.token.coinGeckoId } returns currency.id.rawCurrencyId?.value
},
mockk<BalanceItem> {
mockk<BalanceItem>(relaxed = true) {
every { this@mockk.token.coinGeckoId } returns "unknown"
},
),

View file

@ -589,11 +589,11 @@ class TotalFiatBalanceCalculatorTest {
private fun createStakeKitBalance(amount: BigDecimal, balanceType: BalanceType): StakingBalance.Data.StakeKit {
return StakingBalance.Data.StakeKit(
stakingId = mockk(),
stakingId = mockk(relaxed = true),
source = StatusSource.ACTUAL,
balance = YieldBalanceItem(
items = listOf(
mockk<BalanceItem> {
mockk<BalanceItem>(relaxed = true) {
every { this@mockk.amount } returns amount
every { this@mockk.type } returns balanceType
},