Updated on 2026-08-14
This commit is contained in:
parent
994186039e
commit
259953954a
13 changed files with 303 additions and 41 deletions
|
|
@ -25,6 +25,7 @@ import com.tangem.domain.models.currency.yieldSupplyKey
|
|||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.staking.model.StakingOption
|
||||
import com.tangem.domain.staking.model.optionOrNull
|
||||
import com.tangem.domain.staking.model.common.RewardInfo
|
||||
import com.tangem.domain.staking.model.common.RewardType
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
|
|
@ -244,14 +245,21 @@ class TokenItemStateConverter(
|
|||
currencyStatus: CryptoCurrencyStatus,
|
||||
stakingApyMap: Map<CryptoCurrency, StakingAvailability>,
|
||||
): StakingLocalInfo {
|
||||
val stakingAvailability = stakingApyMap[currencyStatus.currency] as? StakingAvailability.Available
|
||||
val availability = stakingApyMap[currencyStatus.currency]
|
||||
val option = availability?.optionOrNull
|
||||
?: return StakingLocalInfo(rate = null, isActive = false, rewardType = null)
|
||||
|
||||
val stakingBalance = currencyStatus.value.stakingBalance as? StakingBalance.Data
|
||||
val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit
|
||||
val p2pEthPoolBalance = stakingBalance as? StakingBalance.Data.P2PEthPool
|
||||
val isActive = stakeKitBalance != null || p2pEthPoolBalance != null
|
||||
|
||||
val rateInfo = when (val stakingOptions = stakingAvailability.option) {
|
||||
// Full = no free capacity: show the badge only for tokens that already have a stake.
|
||||
if (availability is StakingAvailability.Full && !isActive) {
|
||||
return StakingLocalInfo(rate = null, isActive = false, rewardType = null)
|
||||
}
|
||||
|
||||
val rateInfo = when (val stakingOptions = option) {
|
||||
is StakingOption.P2PEthPool -> {
|
||||
RewardInfo(
|
||||
rate = stakingOptions.apy,
|
||||
|
|
@ -282,7 +290,7 @@ class TokenItemStateConverter(
|
|||
|
||||
return StakingLocalInfo(
|
||||
rate = rateInfo?.rate,
|
||||
isActive = stakeKitBalance != null || p2pEthPoolBalance != null,
|
||||
isActive = isActive,
|
||||
rewardType = rateInfo?.type,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ internal class DefaultP2PEthPoolRepository(
|
|||
else -> {
|
||||
val integration = P2PEthPoolIntegration(StakingIntegrationID.P2PEthPool, vaults, limits)
|
||||
if (integration.areAllTargetsFull) {
|
||||
StakingAvailability.Unavailable
|
||||
StakingAvailability.Full(StakingOption.P2PEthPool(vaults))
|
||||
} else {
|
||||
StakingAvailability.Available(StakingOption.P2PEthPool(vaults))
|
||||
}
|
||||
|
|
@ -216,7 +216,7 @@ internal class DefaultP2PEthPoolRepository(
|
|||
val limits = getVaultLimitsSyncOrNull() ?: return StakingAvailability.TemporaryUnavailable
|
||||
val integration = P2PEthPoolIntegration(StakingIntegrationID.P2PEthPool, vaults, limits)
|
||||
return if (integration.areAllTargetsFull) {
|
||||
StakingAvailability.Unavailable
|
||||
StakingAvailability.Full(StakingOption.P2PEthPool(vaults))
|
||||
} else {
|
||||
StakingAvailability.Available(StakingOption.P2PEthPool(vaults))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
package com.tangem.data.staking
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.token.P2PEthPoolVaultsStore
|
||||
import com.tangem.datasource.local.token.P2PVaultLimitsStore
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
import com.tangem.domain.staking.model.ethpool.VaultLimitInfo
|
||||
import com.tangem.domain.staking.toggles.StakingFeatureToggles
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultP2PEthPoolRepositoryAvailabilityTest {
|
||||
|
||||
private val api = mockk<P2PEthPoolApi>(relaxed = true)
|
||||
private val vaultsStore = mockk<P2PEthPoolVaultsStore>(relaxed = true)
|
||||
private val limitsStore = mockk<P2PVaultLimitsStore>(relaxed = true)
|
||||
private val tangemTechApi = mockk<TangemTechApi>(relaxed = true)
|
||||
private val featureToggles = mockk<StakingFeatureToggles>(relaxed = true)
|
||||
|
||||
private val repository = DefaultP2PEthPoolRepository(
|
||||
p2pEthPoolApi = api,
|
||||
p2pEthPoolVaultsStore = vaultsStore,
|
||||
p2pVaultLimitsStore = limitsStore,
|
||||
tangemTechApi = tangemTechApi,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
stakingFeatureToggles = featureToggles,
|
||||
)
|
||||
|
||||
private fun buildVault(address: String, totalAssets: String) = P2PEthPoolVault(
|
||||
vaultAddress = address,
|
||||
displayName = "Vault",
|
||||
apy = BigDecimal("4.5"),
|
||||
baseApy = BigDecimal("4.0"),
|
||||
capacity = BigDecimal("1000"),
|
||||
totalAssets = BigDecimal(totalAssets),
|
||||
feePercent = BigDecimal("10"),
|
||||
isPrivate = false,
|
||||
isGenesis = false,
|
||||
isSmoothingPool = true,
|
||||
isErc20 = false,
|
||||
tokenName = null,
|
||||
tokenSymbol = null,
|
||||
createdAt = 0L,
|
||||
)
|
||||
|
||||
private fun limits(address: String, limit: String) =
|
||||
mapOf(address.lowercase() to VaultLimitInfo(limit = BigDecimal(limit), coefficient = null))
|
||||
|
||||
@Test
|
||||
fun `all vaults full - emits Full with option`() = runTest {
|
||||
every { vaultsStore.get() } returns flowOf(listOf(buildVault("0xABC", totalAssets = "999.95")))
|
||||
every { limitsStore.get() } returns MutableStateFlow(limits("0xABC", limit = "1000")) // remaining 0.05 <= 0.1
|
||||
|
||||
val result = repository.getStakingAvailability().first()
|
||||
|
||||
assertThat(result).isInstanceOf(StakingAvailability.Full::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `capacity available - emits Available`() = runTest {
|
||||
every { vaultsStore.get() } returns flowOf(listOf(buildVault("0xABC", totalAssets = "100")))
|
||||
every { limitsStore.get() } returns MutableStateFlow(limits("0xABC", limit = "1000")) // remaining 900 > 0.1
|
||||
|
||||
val result = repository.getStakingAvailability().first()
|
||||
|
||||
assertThat(result).isInstanceOf(StakingAvailability.Available::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync - all vaults full - returns Full with option`() = runTest {
|
||||
coEvery { vaultsStore.getSync() } returns listOf(buildVault("0xABC", totalAssets = "999.95"))
|
||||
coEvery { limitsStore.getSyncOrNull() } returns limits("0xABC", limit = "1000") // remaining 0.05 <= 0.1
|
||||
|
||||
val result = repository.getStakingAvailabilitySync()
|
||||
|
||||
assertThat(result).isInstanceOf(StakingAvailability.Full::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync - capacity available - returns Available`() = runTest {
|
||||
coEvery { vaultsStore.getSync() } returns listOf(buildVault("0xABC", totalAssets = "100"))
|
||||
coEvery { limitsStore.getSyncOrNull() } returns limits("0xABC", limit = "1000") // remaining 900 > 0.1
|
||||
|
||||
val result = repository.getStakingAvailabilitySync()
|
||||
|
||||
assertThat(result).isInstanceOf(StakingAvailability.Available::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync - empty vaults - returns TemporaryUnavailable`() = runTest {
|
||||
coEvery { vaultsStore.getSync() } returns emptyList()
|
||||
|
||||
val result = repository.getStakingAvailabilitySync()
|
||||
|
||||
assertThat(result).isInstanceOf(StakingAvailability.TemporaryUnavailable::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync - limits not loaded - returns TemporaryUnavailable`() = runTest {
|
||||
coEvery { vaultsStore.getSync() } returns listOf(buildVault("0xABC", totalAssets = "100"))
|
||||
coEvery { limitsStore.getSyncOrNull() } returns null
|
||||
|
||||
val result = repository.getStakingAvailabilitySync()
|
||||
|
||||
assertThat(result).isInstanceOf(StakingAvailability.TemporaryUnavailable::class.java)
|
||||
}
|
||||
}
|
||||
|
|
@ -108,7 +108,7 @@ class P2PEthPoolIntegration(
|
|||
private const val MAX_AMOUNT_SCALE = 1
|
||||
private val DEFAULT_MINIMUM_STAKE = BigDecimal("0.01")
|
||||
private val DEFAULT_MINIMUM_UNSTAKE = BigDecimal("0.01")
|
||||
private val AVAILABILITY_THRESHOLD = BigDecimal("2")
|
||||
private val AVAILABILITY_THRESHOLD = BigDecimal("0.1")
|
||||
|
||||
private const val TERMS_OF_SERVICE_URL = "https://www.p2p.org/terms-of-use"
|
||||
private const val PRIVACY_POLICY_URL = "https://www.p2p.org/privacy-policy"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,23 @@ sealed class StakingAvailability {
|
|||
|
||||
data class Available(val option: StakingOption) : StakingAvailability()
|
||||
|
||||
/**
|
||||
* Integration exists and APY is known, but there is no free capacity (all vaults full).
|
||||
* Existing stakes stay visible; new stakes are not offered. P2P ETH only.
|
||||
*/
|
||||
data class Full(val option: StakingOption) : StakingAvailability()
|
||||
|
||||
data object Unavailable : StakingAvailability()
|
||||
|
||||
data object TemporaryUnavailable : StakingAvailability()
|
||||
}
|
||||
}
|
||||
|
||||
/** Staking option if the integration is known (Available or Full), else null. */
|
||||
val StakingAvailability.optionOrNull: StakingOption?
|
||||
get() = when (this) {
|
||||
is StakingAvailability.Available -> option
|
||||
is StakingAvailability.Full -> option
|
||||
StakingAvailability.Unavailable,
|
||||
StakingAvailability.TemporaryUnavailable,
|
||||
-> null
|
||||
}
|
||||
|
|
@ -67,24 +67,6 @@ internal class P2PEthPoolIntegrationTest {
|
|||
assertThat(integration.enterArgs!!.amountRequirement!!.maximum).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vault with exactly 2 ETH remaining - not available, max is null`() {
|
||||
val vaults = listOf(buildVault("0xABC", capacity = "100", totalAssets = "48"))
|
||||
val limits = buildLimits("0xABC" to BigDecimal("50"))
|
||||
val integration = P2PEthPoolIntegration(StakingIntegrationID.P2PEthPool, vaults, limits)
|
||||
|
||||
assertThat(integration.enterArgs!!.amountRequirement!!.maximum).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vault with less than 2 ETH remaining - not available, max is null`() {
|
||||
val vaults = listOf(buildVault("0xABC", capacity = "100", totalAssets = "48.5"))
|
||||
val limits = buildLimits("0xABC" to BigDecimal("50"))
|
||||
val integration = P2PEthPoolIntegration(StakingIntegrationID.P2PEthPool, vaults, limits)
|
||||
|
||||
assertThat(integration.enterArgs!!.amountRequirement!!.maximum).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multiple available vaults - uses minimum remaining space`() {
|
||||
val vault1 = buildVault("0xA", capacity = "100", totalAssets = "10")
|
||||
|
|
@ -99,27 +81,36 @@ internal class P2PEthPoolIntegrationTest {
|
|||
@Nested
|
||||
inner class Availability {
|
||||
@Test
|
||||
fun `all vaults full - areAllTargetsFull is true`() {
|
||||
fun `vault absent from limits - areAllTargetsFull is true`() {
|
||||
val vaults = listOf(buildVault("0xABC", capacity = "100", totalAssets = "48"))
|
||||
val limits = buildLimits("0xABC" to BigDecimal("50"))
|
||||
val limits = emptyMap<String, VaultLimitInfo>()
|
||||
val integration = P2PEthPoolIntegration(StakingIntegrationID.P2PEthPool, vaults, limits)
|
||||
|
||||
assertThat(integration.areAllTargetsFull).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vault with remaining between 0_1 and 2 ETH - also considered full`() {
|
||||
val vaults = listOf(buildVault("0xABC", capacity = "100", totalAssets = "48.5"))
|
||||
val limits = buildLimits("0xABC" to BigDecimal("50"))
|
||||
fun `vault with exactly 0_1 ETH remaining - not available`() {
|
||||
val vaults = listOf(buildVault("0xABC", capacity = "100", totalAssets = "49.9"))
|
||||
val limits = buildLimits("0xABC" to BigDecimal("50")) // remaining = 0.1
|
||||
val integration = P2PEthPoolIntegration(StakingIntegrationID.P2PEthPool, vaults, limits)
|
||||
|
||||
assertThat(integration.areAllTargetsFull).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vault with remaining just above 0_1 ETH - available (regression [REDACTED_TASK_KEY])`() {
|
||||
val vaults = listOf(buildVault("0xABC", capacity = "400", totalAssets = "321.895202388313423922"))
|
||||
val limits = buildLimits("0xABC" to BigDecimal("322.1")) // remaining ≈ 0.2048 > 0.1
|
||||
val integration = P2PEthPoolIntegration(StakingIntegrationID.P2PEthPool, vaults, limits)
|
||||
|
||||
assertThat(integration.areAllTargetsFull).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `at least one vault available - areAllTargetsFull is false`() {
|
||||
val vault1 = buildVault("0xA", capacity = "100", totalAssets = "48.5") // full (1.5 remaining < 2)
|
||||
val vault2 = buildVault("0xB", capacity = "100", totalAssets = "10") // available (40 remaining > 2)
|
||||
val vault1 = buildVault("0xA", capacity = "100", totalAssets = "49.95") // full (0.05 remaining < 0.1)
|
||||
val vault2 = buildVault("0xB", capacity = "100", totalAssets = "10") // available (40 remaining > 0.1)
|
||||
val limits = buildLimits("0xa" to BigDecimal("50"), "0xb" to BigDecimal("50"))
|
||||
val integration = P2PEthPoolIntegration(StakingIntegrationID.P2PEthPool, listOf(vault1, vault2), limits)
|
||||
|
||||
|
|
@ -128,8 +119,8 @@ internal class P2PEthPoolIntegrationTest {
|
|||
|
||||
@Test
|
||||
fun `preferred targets only contains available vaults`() {
|
||||
val vault1 = buildVault("0xA", capacity = "100", totalAssets = "48.5") // full
|
||||
val vault2 = buildVault("0xB", capacity = "100", totalAssets = "10") // available
|
||||
val vault1 = buildVault("0xA", capacity = "100", totalAssets = "49.95") // full (0.05 remaining ≤ 0.1)
|
||||
val vault2 = buildVault("0xB", capacity = "100", totalAssets = "10") // available (40 remaining > 0.1)
|
||||
val limits = buildLimits("0xa" to BigDecimal("50"), "0xb" to BigDecimal("50"))
|
||||
val integration = P2PEthPoolIntegration(StakingIntegrationID.P2PEthPool, listOf(vault1, vault2), limits)
|
||||
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ internal open class BaseActionsFactory(
|
|||
unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(currency.name),
|
||||
option = null,
|
||||
)
|
||||
is StakingAvailability.Full -> null
|
||||
StakingAvailability.Unavailable -> null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY
|
|||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.staking.model.optionOrNull
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
|
|
@ -70,12 +71,12 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
|
|||
return@launch
|
||||
}
|
||||
|
||||
val availability = getStakingAvailabilityUseCase.invokeSync(
|
||||
val availability: StakingAvailability? = getStakingAvailabilityUseCase.invokeSync(
|
||||
userWalletId = selectedUserWalletId,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
).getOrNull()
|
||||
|
||||
val option = (availability as? StakingAvailability.Available)?.option
|
||||
val option = availability?.optionOrNull
|
||||
if (option == null) {
|
||||
TangemLogger.e("Staking is unavailable for ${cryptoCurrency.name}")
|
||||
return@launch
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ import com.tangem.domain.onramp.model.OnrampSource
|
|||
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
|
||||
import com.tangem.domain.staking.GetStakingEntryInfoUseCase
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.staking.model.optionOrNull
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
|
||||
import com.tangem.domain.tokens.model.TokenActionsState
|
||||
|
|
@ -1148,7 +1149,7 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
getStakingAvailabilityUseCase.invokeSync(userWalletId, cryptoCurrency)
|
||||
.onRight { availability ->
|
||||
val option = (availability as? StakingAvailability.Available)?.option
|
||||
val option = availability.optionOrNull
|
||||
if (option != null) {
|
||||
router.openStaking(
|
||||
userWalletId = userWalletId,
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ internal class TokenDetailsStakingInfoConverter(
|
|||
return when (stakingAvailability) {
|
||||
StakingAvailability.TemporaryUnavailable -> StakingBlockUM.TemporaryUnavailable
|
||||
StakingAvailability.Unavailable -> null
|
||||
is StakingAvailability.Full -> getStakedBlockOrNull(status)
|
||||
is StakingAvailability.Available -> getStakingInfoBlock(status, state)
|
||||
}
|
||||
}
|
||||
|
|
@ -107,6 +108,39 @@ internal class TokenDetailsStakingInfoConverter(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getStakedBlockOrNull(status: CryptoCurrencyStatus): StakingBlockUM? {
|
||||
val stakingBalance = status.value.stakingBalance as? StakingBalance.Data
|
||||
val stakingCryptoAmount = stakingBalance?.getTotalStakingBalance(status.currency.network.rawId)
|
||||
val hasPendingBalances = when (stakingBalance) {
|
||||
is StakingBalance.Data.StakeKit -> stakingBalance.balance.items.isNotEmpty()
|
||||
is StakingBalance.Data.P2PEthPool -> !stakingBalance.unstakingAmount.isNullOrZero()
|
||||
null -> false
|
||||
}
|
||||
return when {
|
||||
!stakingCryptoAmount.isNullOrZero() -> getStakedBlockWithFiatAmount(
|
||||
status = status,
|
||||
stakingAmount = stakingCryptoAmount,
|
||||
rewardAmount = when (stakingBalance) {
|
||||
is StakingBalance.Data.StakeKit -> stakingBalance.getRewardStakingBalance()
|
||||
is StakingBalance.Data.P2PEthPool -> stakingBalance.totalRewards
|
||||
else -> BigDecimal.ZERO
|
||||
},
|
||||
)
|
||||
// Pending-only path: reachable for StakeKit (pending items are not part of the total);
|
||||
// for P2PEthPool the unstaking amount is already included in getTotalStakingBalance above.
|
||||
hasPendingBalances -> getStakedBlockWithFiatAmount(
|
||||
status = status,
|
||||
stakingAmount = when (stakingBalance) {
|
||||
is StakingBalance.Data.StakeKit -> stakingBalance.balance.items.sumOf { it.amount }
|
||||
is StakingBalance.Data.P2PEthPool -> stakingBalance.unstakingAmount
|
||||
null -> BigDecimal.ZERO
|
||||
},
|
||||
rewardAmount = null,
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isStakingButtonEnabled(status: CryptoCurrencyStatus): Boolean {
|
||||
return status.value is CryptoCurrencyStatus.Loaded ||
|
||||
status.value is CryptoCurrencyStatus.NoQuote ||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ internal class UpdateStakingNotificationTransformer(
|
|||
return when (val availability = stakingAvailability) {
|
||||
StakingAvailability.TemporaryUnavailable -> buildTemporaryUnavailable()
|
||||
StakingAvailability.Unavailable -> null
|
||||
is StakingAvailability.Full -> buildActiveBlockOrNull(isBalanceHidden)
|
||||
is StakingAvailability.Available -> getStakingInfoBlock(availability, isBalanceHidden)
|
||||
}
|
||||
}
|
||||
|
|
@ -106,6 +107,27 @@ internal class UpdateStakingNotificationTransformer(
|
|||
}
|
||||
}
|
||||
|
||||
private fun buildActiveBlockOrNull(isBalanceHidden: Boolean): EarnBlockUM? {
|
||||
val status = cryptoCurrencyStatus
|
||||
val stakingBalance = status.value.stakingBalance as? StakingBalance.Data
|
||||
val stakingCryptoAmount = stakingBalance?.getTotalStakingBalance(status.currency.network.rawId)
|
||||
return when {
|
||||
!stakingCryptoAmount.isNullOrZero() -> buildActiveBlock(
|
||||
stakingAmount = stakingCryptoAmount,
|
||||
rewardAmount = stakingBalance.getRewardAmount(),
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
// Pending-only path: reachable for StakeKit (pending items are not part of the total);
|
||||
// for P2PEthPool the unstaking amount is already included in getTotalStakingBalance above.
|
||||
stakingBalance.hasPendingBalances() -> buildActiveBlock(
|
||||
stakingAmount = stakingBalance.getPendingAmount(),
|
||||
rewardAmount = null,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isStakingButtonEnabled(status: CryptoCurrencyStatus): Boolean {
|
||||
return status.value is CryptoCurrencyStatus.Loaded ||
|
||||
status.value is CryptoCurrencyStatus.NoQuote ||
|
||||
|
|
|
|||
|
|
@ -84,11 +84,39 @@ class UpdateStakingNotificationTransformerTest {
|
|||
assertThat(content.trailingUM).isInstanceOf(EarnBlockUM.TrailingUM.Button::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN Full AND no active stake WHEN transform THEN earnBlockState is null`() {
|
||||
val transformer = createTransformer(
|
||||
availability = fullOption(BigDecimal("4.2")),
|
||||
entryInfo = null,
|
||||
)
|
||||
|
||||
val result = transformer.transform(initialState())
|
||||
|
||||
assertThat(result.earnBlockState).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN Full AND active stake WHEN transform THEN active balance block`() {
|
||||
val transformer = createTransformer(
|
||||
availability = fullOption(BigDecimal("4.2")),
|
||||
entryInfo = null,
|
||||
status = buildStatusWithStake(stakedAmount = BigDecimal("5")),
|
||||
)
|
||||
|
||||
val result = transformer.transform(initialState())
|
||||
|
||||
assertThat(result.earnBlockState).isInstanceOf(EarnBlockUM.Content::class.java)
|
||||
val content = result.earnBlockState as EarnBlockUM.Content
|
||||
assertThat(content.trailingUM).isInstanceOf(EarnBlockUM.TrailingUM.Balance::class.java)
|
||||
}
|
||||
|
||||
private fun createTransformer(
|
||||
availability: StakingAvailability,
|
||||
entryInfo: StakingEntryInfo?,
|
||||
status: CryptoCurrencyStatus = buildStatus(),
|
||||
) = UpdateStakingNotificationTransformer(
|
||||
cryptoCurrencyStatus = buildStatus(),
|
||||
cryptoCurrencyStatus = status,
|
||||
stakingAvailability = availability,
|
||||
stakingEntryInfo = entryInfo,
|
||||
appCurrency = AppCurrency.Default,
|
||||
|
|
@ -122,6 +150,38 @@ class UpdateStakingNotificationTransformerTest {
|
|||
return StakingAvailability.Available(option = option)
|
||||
}
|
||||
|
||||
private fun fullOption(apy: BigDecimal): StakingAvailability.Full {
|
||||
val option = mockk<StakingOption>(relaxed = true) {
|
||||
every { this@mockk.apy } returns apy
|
||||
}
|
||||
return StakingAvailability.Full(option = option)
|
||||
}
|
||||
|
||||
private fun buildStatusWithStake(stakedAmount: BigDecimal): CryptoCurrencyStatus {
|
||||
val network = mockk<Network>(relaxed = true) {
|
||||
every { rawId } returns "solana"
|
||||
every { isTestnet } returns false
|
||||
}
|
||||
val currency = mockk<CryptoCurrency.Coin>(relaxed = true) {
|
||||
every { symbol } returns "SOL"
|
||||
every { decimals } returns 9
|
||||
every { this@mockk.network } returns network
|
||||
every { id.isCoin } returns true
|
||||
}
|
||||
val stakingBalance = mockk<StakingBalance.Data.P2PEthPool>(relaxed = true) {
|
||||
every { totalStaked } returns stakedAmount
|
||||
every { unstakingAmount } returns BigDecimal.ZERO
|
||||
every { withdrawableAmount } returns BigDecimal.ZERO
|
||||
every { totalRewards } returns BigDecimal.ZERO
|
||||
}
|
||||
val value = mockk<CryptoCurrencyStatus.Value>(relaxed = true) {
|
||||
every { this@mockk.stakingBalance } returns stakingBalance
|
||||
every { fiatRate } returns BigDecimal.ONE
|
||||
every { yieldSupplyStatus } returns null
|
||||
}
|
||||
return CryptoCurrencyStatus(currency = currency, value = value)
|
||||
}
|
||||
|
||||
private fun initialState(): TokenDetailsUM = TokenDetailsUM(
|
||||
topAppBarUM = TokenDetailsTopAppBarUM(
|
||||
titleState = TitleState.Simple(tokenName = "Solana"),
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import com.tangem.domain.models.currency.yieldSupplyKey
|
|||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.staking.model.StakingOption
|
||||
import com.tangem.domain.staking.model.optionOrNull
|
||||
import com.tangem.domain.staking.model.common.RewardInfo
|
||||
import com.tangem.domain.staking.model.common.RewardType
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
|
|
@ -77,14 +78,21 @@ internal class EarnApyConverter(
|
|||
currencyStatus: CryptoCurrencyStatus,
|
||||
stakingApyMap: Map<CryptoCurrency, StakingAvailability>,
|
||||
): StakingLocalInfo {
|
||||
val stakingAvailability = stakingApyMap[currencyStatus.currency] as? StakingAvailability.Available
|
||||
val availability = stakingApyMap[currencyStatus.currency]
|
||||
val option = availability?.optionOrNull
|
||||
?: return StakingLocalInfo(rate = null, isActive = false, rewardType = null)
|
||||
|
||||
val stakingBalance = currencyStatus.value.stakingBalance as? StakingBalance.Data
|
||||
val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit
|
||||
val p2pEthPoolBalance = stakingBalance as? StakingBalance.Data.P2PEthPool
|
||||
val isActive = stakeKitBalance != null || p2pEthPoolBalance != null
|
||||
|
||||
val rateInfo = when (val stakingOptions = stakingAvailability.option) {
|
||||
// Full = no free capacity: show the badge only for tokens that already have a stake.
|
||||
if (availability is StakingAvailability.Full && !isActive) {
|
||||
return StakingLocalInfo(rate = null, isActive = false, rewardType = null)
|
||||
}
|
||||
|
||||
val rateInfo = when (val stakingOptions = option) {
|
||||
is StakingOption.P2PEthPool -> {
|
||||
RewardInfo(
|
||||
rate = stakingOptions.apy,
|
||||
|
|
@ -115,7 +123,7 @@ internal class EarnApyConverter(
|
|||
|
||||
return StakingLocalInfo(
|
||||
rate = rateInfo?.rate,
|
||||
isActive = stakeKitBalance != null || p2pEthPoolBalance != null,
|
||||
isActive = isActive,
|
||||
rewardType = rateInfo?.type,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue