Updated on 2026-08-14
This commit is contained in:
parent
e04ac1fb2c
commit
99d32963b5
20 changed files with 336 additions and 27 deletions
|
|
@ -219,6 +219,9 @@ interface TangemTechApi {
|
|||
@POST("v2/transaction-events")
|
||||
suspend fun transactionEvents(@Body name: TransactionEventBody): ApiResponse<Unit>
|
||||
|
||||
@GET("v1/coins/settings")
|
||||
suspend fun getCoinsSettings(): ApiResponse<CoinsSettingsResponse>
|
||||
|
||||
// region Earn
|
||||
@GET("v1/earn/markets")
|
||||
suspend fun getEarnTokens(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CoinsSettingsResponse(
|
||||
@Json(name = "staking") val staking: StakingSettingsDTO?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StakingSettingsDTO(
|
||||
@Json(name = "vaults") val vaults: List<VaultSettingsDTO> = emptyList(),
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class VaultSettingsDTO(
|
||||
@Json(name = "vaultAddress") val vaultAddress: String,
|
||||
@Json(name = "limit") val limit: BigDecimal?,
|
||||
@Json(name = "coefficient") val coefficient: BigDecimal?,
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.datasource.local.token
|
||||
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.domain.staking.model.ethpool.VaultLimitInfo
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* In-memory store for P2P vault limits from Tangem API /v1/coins/settings.
|
||||
* Map key is vaultAddress.lowercase(). Null map value means limits not yet fetched.
|
||||
* A missing key means the vault is full (null-limit vaults are excluded at fetch time).
|
||||
*/
|
||||
@Singleton
|
||||
class P2PVaultLimitsStore @Inject constructor() :
|
||||
RuntimeStateStore<Map<String, VaultLimitInfo>?> by RuntimeStateStore(defaultValue = null)
|
||||
|
|
@ -7,29 +7,35 @@ import arrow.core.raise.either
|
|||
import arrow.core.raise.ensure
|
||||
import com.tangem.data.staking.converters.ethpool.*
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
|
||||
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolBroadcastRequest
|
||||
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolTransactionRequest
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolResponse
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolTransactionResponse
|
||||
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.models.staking.P2PEthPoolStakingAccount
|
||||
import com.tangem.domain.staking.model.P2PEthPoolIntegration
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.domain.staking.model.StakingOption
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolBroadcastResult
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolNetwork
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolUnsignedTx
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
import com.tangem.domain.staking.model.ethpool.VaultLimitInfo
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.domain.staking.toggles.StakingFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
|
|
@ -38,6 +44,8 @@ import kotlinx.coroutines.withContext
|
|||
internal class DefaultP2PEthPoolRepository(
|
||||
private val p2pEthPoolApi: P2PEthPoolApi,
|
||||
private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
|
||||
private val p2pVaultLimitsStore: P2PVaultLimitsStore,
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val stakingFeatureToggles: StakingFeatureToggles,
|
||||
) : P2PEthPoolRepository {
|
||||
|
|
@ -183,21 +191,32 @@ internal class DefaultP2PEthPoolRepository(
|
|||
}
|
||||
|
||||
override fun getStakingAvailability(): Flow<StakingAvailability> {
|
||||
return getVaultsFlow()
|
||||
.distinctUntilChanged()
|
||||
.map { vaults ->
|
||||
if (vaults.isEmpty()) {
|
||||
return@map StakingAvailability.TemporaryUnavailable
|
||||
return combine(
|
||||
getVaultsFlow().distinctUntilChanged(),
|
||||
getVaultLimitsFlow().distinctUntilChanged(),
|
||||
) { vaults, limits ->
|
||||
when {
|
||||
vaults.isEmpty() -> StakingAvailability.TemporaryUnavailable
|
||||
limits == null -> StakingAvailability.TemporaryUnavailable
|
||||
else -> {
|
||||
val integration = P2PEthPoolIntegration(StakingIntegrationID.P2PEthPool, vaults, limits)
|
||||
if (integration.areAllTargetsFull) {
|
||||
StakingAvailability.Unavailable
|
||||
} else {
|
||||
StakingAvailability.Available(StakingOption.P2PEthPool(vaults))
|
||||
}
|
||||
}
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
override suspend fun getStakingAvailabilitySync(): StakingAvailability {
|
||||
val vaults = getVaultsSync()
|
||||
return if (vaults.isEmpty()) {
|
||||
StakingAvailability.TemporaryUnavailable
|
||||
if (vaults.isEmpty()) return StakingAvailability.TemporaryUnavailable
|
||||
val limits = getVaultLimitsSyncOrNull() ?: return StakingAvailability.TemporaryUnavailable
|
||||
val integration = P2PEthPoolIntegration(StakingIntegrationID.P2PEthPool, vaults, limits)
|
||||
return if (integration.areAllTargetsFull) {
|
||||
StakingAvailability.Unavailable
|
||||
} else {
|
||||
StakingAvailability.Available(StakingOption.P2PEthPool(vaults))
|
||||
}
|
||||
|
|
@ -206,4 +225,33 @@ internal class DefaultP2PEthPoolRepository(
|
|||
override suspend fun getVaultsSync(): List<P2PEthPoolVault> {
|
||||
return p2pEthPoolVaultsStore.getSync()
|
||||
}
|
||||
|
||||
override suspend fun fetchVaultLimits() {
|
||||
runSuspendCatching {
|
||||
val response = withContext(dispatchers.io) {
|
||||
tangemTechApi.getCoinsSettings().getOrThrow()
|
||||
}
|
||||
val vaults = response.staking?.vaults.orEmpty()
|
||||
val limits = vaults
|
||||
.mapNotNull { vault ->
|
||||
val limit = vault.limit ?: return@mapNotNull null
|
||||
vault.vaultAddress.lowercase() to VaultLimitInfo(
|
||||
limit = limit,
|
||||
coefficient = vault.coefficient,
|
||||
)
|
||||
}
|
||||
.toMap()
|
||||
p2pVaultLimitsStore.store(limits)
|
||||
}.onFailure { e ->
|
||||
TangemLogger.e("Error fetching P2P vault limits: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getVaultLimitsFlow(): Flow<Map<String, VaultLimitInfo>?> {
|
||||
return p2pVaultLimitsStore.get()
|
||||
}
|
||||
|
||||
override suspend fun getVaultLimitsSyncOrNull(): Map<String, VaultLimitInfo>? {
|
||||
return p2pVaultLimitsStore.getSyncOrNull()
|
||||
}
|
||||
}
|
||||
|
|
@ -12,9 +12,11 @@ import com.tangem.data.staking.utils.DefaultStakingCleaner
|
|||
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.token.P2PEthPoolVaultsStore
|
||||
import com.tangem.datasource.local.token.P2PVaultLimitsStore
|
||||
import com.tangem.datasource.local.token.StakingActionsStore
|
||||
import com.tangem.datasource.local.token.StakingYieldsStore
|
||||
import com.tangem.domain.staking.StakingIdFactory
|
||||
|
|
@ -77,12 +79,16 @@ internal object StakingDataModule {
|
|||
fun provideP2PEthPoolRepository(
|
||||
p2pEthPoolApi: P2PEthPoolApi,
|
||||
p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
|
||||
p2pVaultLimitsStore: P2PVaultLimitsStore,
|
||||
tangemTechApi: TangemTechApi,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
stakingFeatureToggles: StakingFeatureToggles,
|
||||
): P2PEthPoolRepository {
|
||||
return DefaultP2PEthPoolRepository(
|
||||
p2pEthPoolApi = p2pEthPoolApi,
|
||||
p2pEthPoolVaultsStore = p2pEthPoolVaultsStore,
|
||||
p2pVaultLimitsStore = p2pVaultLimitsStore,
|
||||
tangemTechApi = tangemTechApi,
|
||||
dispatchers = dispatchers,
|
||||
stakingFeatureToggles = stakingFeatureToggles,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolNetworkDTO
|
|||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolResponse
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolVaultDTO
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolVaultsResponse
|
||||
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.StakingIntegrationID
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolNetwork
|
||||
import com.tangem.domain.staking.toggles.StakingFeatureToggles
|
||||
|
|
@ -30,12 +32,16 @@ internal class P2PEthPoolVaultFilterTest {
|
|||
|
||||
private val api = mockk<P2PEthPoolApi>()
|
||||
private val store = mockk<P2PEthPoolVaultsStore>(relaxed = true)
|
||||
private val limitsStore = mockk<P2PVaultLimitsStore>(relaxed = true)
|
||||
private val tangemTechApi = mockk<TangemTechApi>(relaxed = true)
|
||||
private val featureToggles = mockk<StakingFeatureToggles> {
|
||||
every { isIntegrationEnabled(StakingIntegrationID.P2PEthPool) } returns true
|
||||
}
|
||||
private val repository = DefaultP2PEthPoolRepository(
|
||||
p2pEthPoolApi = api,
|
||||
p2pEthPoolVaultsStore = store,
|
||||
p2pVaultLimitsStore = limitsStore,
|
||||
tangemTechApi = tangemTechApi,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
stakingFeatureToggles = featureToggles,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.domain.staking.model.ethpool
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Per-vault capacity limits from Tangem API /v1/coins/settings.
|
||||
*
|
||||
* @property limit max stakeable amount in ETH (pre-computed as MAX_Threshold - TVL).
|
||||
* Vaults absent from the API response or with null limit are not stored.
|
||||
* @property coefficient threshold multiplier (e.g. 1.25×); optional server-side field,
|
||||
* reserved for future use, not used in client-side calculations
|
||||
*/
|
||||
data class VaultLimitInfo(
|
||||
val limit: BigDecimal,
|
||||
val coefficient: BigDecimal?,
|
||||
)
|
||||
|
|
@ -26,6 +26,7 @@ class FetchStakingOptionsUseCase(
|
|||
coroutineScope {
|
||||
launch { stakeKitRepository.fetchYields() }
|
||||
launch { p2pEthPoolRepository.fetchVaults() }
|
||||
launch { p2pEthPoolRepository.fetchVaultLimits() }
|
||||
}
|
||||
},
|
||||
catch = { stakingErrorResolver.resolve(it) },
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ 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 com.tangem.domain.staking.model.ethpool.VaultLimitInfo
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
/**
|
||||
* StakingIntegration implementation for P2PEthPool pooled staking.
|
||||
|
|
@ -16,6 +18,7 @@ import java.math.BigDecimal
|
|||
class P2PEthPoolIntegration(
|
||||
override val integrationId: StakingIntegrationID,
|
||||
private val vaults: List<P2PEthPoolVault>,
|
||||
private val vaultLimits: Map<String, VaultLimitInfo>,
|
||||
) : StakingIntegration {
|
||||
|
||||
// Basic
|
||||
|
|
@ -30,9 +33,11 @@ class P2PEthPoolIntegration(
|
|||
vault.toStakingTarget()
|
||||
}
|
||||
|
||||
override val preferredTargets: List<StakingTarget> = targets
|
||||
override val preferredTargets: List<StakingTarget> = vaults
|
||||
.filter { isVaultAvailable(it) }
|
||||
.map { it.toStakingTarget() }
|
||||
|
||||
override val areAllTargetsFull: Boolean = false
|
||||
override val areAllTargetsFull: Boolean = preferredTargets.isEmpty()
|
||||
|
||||
// Enter/Exit Args
|
||||
|
||||
|
|
@ -45,7 +50,7 @@ class P2PEthPoolIntegration(
|
|||
override val enterArgs: StakingActionArgs = StakingActionArgs(
|
||||
amountRequirement = StakingAmountRequirement(
|
||||
isRequired = true,
|
||||
minimum = DEFAULT_MINIMUM_STAKE,
|
||||
minimum = enterMinimumAmount,
|
||||
maximum = calculateMaximumStakeAmount(),
|
||||
),
|
||||
isPartialAmountDisabled = false,
|
||||
|
|
@ -82,19 +87,27 @@ class P2PEthPoolIntegration(
|
|||
|
||||
override fun getCurrentToken(rawCurrencyId: CryptoCurrency.RawID?): YieldToken = token
|
||||
|
||||
private fun isVaultAvailable(vault: P2PEthPoolVault): Boolean {
|
||||
val info = vaultLimits[vault.vaultAddress.lowercase()] ?: return false
|
||||
return info.limit - vault.totalAssets > AVAILABILITY_THRESHOLD
|
||||
}
|
||||
|
||||
private fun calculateMaximumStakeAmount(): BigDecimal? {
|
||||
return vaults
|
||||
.filter { isVaultAvailable(it) }
|
||||
.mapNotNull { vault ->
|
||||
val availableCapacity = vault.capacity - vault.totalAssets
|
||||
if (availableCapacity > BigDecimal.ZERO) availableCapacity else null
|
||||
vaultLimits[vault.vaultAddress.lowercase()]?.let { it.limit - vault.totalAssets }
|
||||
}
|
||||
.maxOrNull()
|
||||
.minOrNull()
|
||||
?.setScale(MAX_AMOUNT_SCALE, RoundingMode.FLOOR)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MIN_COOLDOWN_DAYS = 1
|
||||
private const val MAX_COOLDOWN_DAYS = 4
|
||||
private const val MAX_AMOUNT_SCALE = 1
|
||||
private val DEFAULT_MINIMUM_STAKE = BigDecimal("0.01")
|
||||
private val AVAILABILITY_THRESHOLD = BigDecimal("2")
|
||||
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolNetwork
|
|||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolUnsignedTx
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
||||
import com.tangem.domain.staking.model.ethpool.VaultLimitInfo
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
|
@ -131,6 +132,25 @@ interface P2PEthPoolRepository {
|
|||
*/
|
||||
suspend fun getVaultsSync(): List<P2PEthPoolVault>
|
||||
|
||||
/**
|
||||
* Fetch and store vault limits from Tangem API /v1/coins/settings
|
||||
*/
|
||||
suspend fun fetchVaultLimits()
|
||||
|
||||
/**
|
||||
* Get flow of cached vault limits.
|
||||
*
|
||||
* @return Flow of map from vaultAddress.lowercase() to VaultLimitInfo, null if not yet fetched
|
||||
*/
|
||||
fun getVaultLimitsFlow(): Flow<Map<String, VaultLimitInfo>?>
|
||||
|
||||
/**
|
||||
* Get cached vault limits synchronously.
|
||||
*
|
||||
* @return Map from vaultAddress.lowercase() to VaultLimitInfo, null if not yet fetched
|
||||
*/
|
||||
suspend fun getVaultLimitsSyncOrNull(): Map<String, VaultLimitInfo>?
|
||||
|
||||
/**
|
||||
* Check P2PEthPool staking availability by finding public vault
|
||||
*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
package com.tangem.domain.staking.model
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
import com.tangem.domain.staking.model.ethpool.VaultLimitInfo
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class P2PEthPoolIntegrationTest {
|
||||
|
||||
private fun buildVault(
|
||||
address: String,
|
||||
capacity: String,
|
||||
totalAssets: String,
|
||||
) = P2PEthPoolVault(
|
||||
vaultAddress = address,
|
||||
displayName = "Test Vault",
|
||||
apy = BigDecimal("4.5"),
|
||||
baseApy = BigDecimal("4.0"),
|
||||
capacity = BigDecimal(capacity),
|
||||
totalAssets = BigDecimal(totalAssets),
|
||||
feePercent = BigDecimal("0.1"),
|
||||
isPrivate = false,
|
||||
isGenesis = false,
|
||||
isSmoothingPool = true,
|
||||
isErc20 = false,
|
||||
tokenName = null,
|
||||
tokenSymbol = null,
|
||||
createdAt = 0L,
|
||||
)
|
||||
|
||||
private fun buildLimits(vararg pairs: Pair<String, BigDecimal>) =
|
||||
pairs.associate { (addr, limit) ->
|
||||
addr.lowercase() to VaultLimitInfo(limit = limit, coefficient = BigDecimal("1.25"))
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class MaximumAmount {
|
||||
@Test
|
||||
fun `vault available - uses remaining space as max, rounded down to 0_1 ETH`() {
|
||||
val vaults = listOf(buildVault("0xABC", capacity = "100", totalAssets = "10"))
|
||||
val limits = buildLimits("0xABC" to BigDecimal("50"))
|
||||
val integration = P2PEthPoolIntegration(StakingIntegrationID.P2PEthPool, vaults, limits)
|
||||
|
||||
assertThat(integration.enterArgs!!.amountRequirement!!.maximum).isEqualTo(BigDecimal("40.0"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `remaining with fractional ETH - floored to 0_1 ETH precision`() {
|
||||
val vaults = listOf(buildVault("0xABC", capacity = "100", totalAssets = "10"))
|
||||
val limits = buildLimits("0xABC" to BigDecimal("22.37"))
|
||||
val integration = P2PEthPoolIntegration(StakingIntegrationID.P2PEthPool, vaults, limits)
|
||||
|
||||
assertThat(integration.enterArgs!!.amountRequirement!!.maximum).isEqualTo(BigDecimal("12.3"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vault absent from limits map - treated as full, max is null`() {
|
||||
val vaults = listOf(buildVault("0xABC", capacity = "100", totalAssets = "30"))
|
||||
val limits = emptyMap<String, VaultLimitInfo>()
|
||||
val integration = P2PEthPoolIntegration(StakingIntegrationID.P2PEthPool, vaults, limits)
|
||||
|
||||
assertThat(integration.areAllTargetsFull).isTrue()
|
||||
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")
|
||||
val vault2 = buildVault("0xB", capacity = "100", totalAssets = "20")
|
||||
val limits = buildLimits("0xa" to BigDecimal("50"), "0xb" to BigDecimal("50"))
|
||||
val integration = P2PEthPoolIntegration(StakingIntegrationID.P2PEthPool, listOf(vault1, vault2), limits)
|
||||
|
||||
assertThat(integration.enterArgs!!.amountRequirement!!.maximum).isEqualTo(BigDecimal("30.0"))
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class Availability {
|
||||
@Test
|
||||
fun `all vaults full - areAllTargetsFull is true`() {
|
||||
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.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"))
|
||||
val integration = P2PEthPoolIntegration(StakingIntegrationID.P2PEthPool, vaults, limits)
|
||||
|
||||
assertThat(integration.areAllTargetsFull).isTrue()
|
||||
}
|
||||
|
||||
@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 limits = buildLimits("0xa" to BigDecimal("50"), "0xb" to BigDecimal("50"))
|
||||
val integration = P2PEthPoolIntegration(StakingIntegrationID.P2PEthPool, listOf(vault1, vault2), limits)
|
||||
|
||||
assertThat(integration.areAllTargetsFull).isFalse()
|
||||
}
|
||||
|
||||
@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 limits = buildLimits("0xa" to BigDecimal("50"), "0xb" to BigDecimal("50"))
|
||||
val integration = P2PEthPoolIntegration(StakingIntegrationID.P2PEthPool, listOf(vault1, vault2), limits)
|
||||
|
||||
assertThat(integration.preferredTargets).hasSize(1)
|
||||
assertThat(integration.preferredTargets.first().address).isEqualTo("0xB")
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class MinimumAmount {
|
||||
@Test
|
||||
fun `minimum stake is 0_01 ETH`() {
|
||||
val integration = P2PEthPoolIntegration(StakingIntegrationID.P2PEthPool, emptyList(), emptyMap())
|
||||
|
||||
assertThat(integration.enterMinimumAmount).isEqualTo(BigDecimal("0.01"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -174,17 +174,17 @@ internal open class BaseActionsFactory(
|
|||
protected fun createStakingAction(
|
||||
currency: CryptoCurrency,
|
||||
stakingAvailability: StakingAvailability,
|
||||
): ActionState.Stake {
|
||||
return if (stakingAvailability is StakingAvailability.Available) {
|
||||
ActionState.Stake(
|
||||
): ActionState.Stake? {
|
||||
return when (stakingAvailability) {
|
||||
is StakingAvailability.Available -> ActionState.Stake(
|
||||
unavailabilityReason = ScenarioUnavailabilityReason.None,
|
||||
option = stakingAvailability.option,
|
||||
)
|
||||
} else {
|
||||
ActionState.Stake(
|
||||
StakingAvailability.TemporaryUnavailable -> ActionState.Stake(
|
||||
unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(currency.name),
|
||||
option = null,
|
||||
)
|
||||
StakingAvailability.Unavailable -> null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ internal class CommonActionsFactory(
|
|||
|
||||
// region Stake
|
||||
createStakingAction(currency = cryptoCurrencyStatus.currency, stakingAvailability = stakingAvailability)
|
||||
.addByReason()
|
||||
?.addByReason()
|
||||
// endregion
|
||||
|
||||
val sendUnavailabilityReason = sendUnavailabilityReasonDeferred.await()
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ internal class OutdatedDataActionsFactory(
|
|||
stakingAvailability = stakingAvailability,
|
||||
)
|
||||
|
||||
stakingAction.addByReason()
|
||||
stakingAction?.addByReason()
|
||||
} else {
|
||||
val stakingAction = ActionState.Stake(
|
||||
unavailabilityReason = ScenarioUnavailabilityReason.UsedOutdatedData,
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ import com.tangem.domain.markets.TokenMarketInfo
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.earn.EarnNetworks
|
||||
import com.tangem.domain.models.earn.EarnTokenWithCurrency
|
||||
import com.tangem.domain.models.earn.PreselectedEarnType
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
|
||||
import com.tangem.domain.models.earn.PreselectedEarnType
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager.AnalyticsParams.Companion.CategoryEarn
|
||||
import com.tangem.features.feed.components.earn.DefaultEarnComponent
|
||||
import com.tangem.features.feed.components.earn.EarnNetworkFilterComponent
|
||||
|
|
|
|||
|
|
@ -182,7 +182,8 @@ internal class StakingModel @Inject constructor(
|
|||
}
|
||||
StakingIntegrationID.P2PEthPool -> {
|
||||
val vaults = p2pEthPoolRepository.getVaultsSync()
|
||||
P2PEthPoolIntegration(integrationId, vaults)
|
||||
val limits = p2pEthPoolRepository.getVaultLimitsSyncOrNull().orEmpty()
|
||||
P2PEthPoolIntegration(integrationId, vaults, limits)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ internal sealed class StakingStates {
|
|||
val yieldBalance: InnerYieldBalanceState,
|
||||
val pullToRefreshConfig: PullToRefreshConfig,
|
||||
val legalUrls: LegalUrls,
|
||||
val areAllTargetsFull: Boolean = false,
|
||||
) : InitialInfoState()
|
||||
|
||||
data class LegalUrls(
|
||||
|
|
|
|||
|
|
@ -166,7 +166,11 @@ internal class SetButtonsStateTransformer(
|
|||
val hasNotStaking = initialState?.yieldBalance == InnerYieldBalanceState.Empty
|
||||
val isCardano = BlockchainUtils.isCardano(cryptoCurrencyBlockchainId)
|
||||
|
||||
return !hasNotStaking && isCardano && currentStep == StakingStep.InitialInfo
|
||||
if (!hasNotStaking && isCardano && currentStep == StakingStep.InitialInfo) return true
|
||||
|
||||
val hasStaking = initialState?.yieldBalance is InnerYieldBalanceState.Data
|
||||
val areAllTargetsFull = initialState?.areAllTargetsFull == true
|
||||
return hasStaking && areAllTargetsFull && currentStep == StakingStep.InitialInfo
|
||||
}
|
||||
|
||||
private fun StakingUiState.isApprovalRequired(): Boolean {
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ internal class SetInitialDataStateTransformer(
|
|||
termsOfServiceUrl = integration.legalUrls.termsOfServiceUrl,
|
||||
privacyPolicyUrl = integration.legalUrls.privacyPolicyUrl,
|
||||
),
|
||||
areAllTargetsFull = integration.areAllTargetsFull,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@ internal class StakingModelTransactionTest : StakingModelTestBase() {
|
|||
integrationId = StakingIntegrationID.P2PEthPool,
|
||||
)
|
||||
coEvery { p2pEthPoolRepository.getVaultsSync() } returns emptyList()
|
||||
coEvery { p2pEthPoolRepository.getVaultLimitsSyncOrNull() } returns emptyMap()
|
||||
val uiStateFlow = MutableStateFlow(initialUiState)
|
||||
every { stateController.uiState } returns uiStateFlow
|
||||
coEvery {
|
||||
|
|
@ -218,6 +219,7 @@ internal class StakingModelTransactionTest : StakingModelTestBase() {
|
|||
integrationId = StakingIntegrationID.P2PEthPool,
|
||||
)
|
||||
coEvery { p2pEthPoolRepository.getVaultsSync() } returns emptyList()
|
||||
coEvery { p2pEthPoolRepository.getVaultLimitsSyncOrNull() } returns emptyMap()
|
||||
val uiStateFlow = MutableStateFlow(initialUiState)
|
||||
every { stateController.uiState } returns uiStateFlow
|
||||
coEvery {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue