Updated on 2026-08-14
This commit is contained in:
parent
a5488207ed
commit
2a41321e74
20 changed files with 440 additions and 9 deletions
|
|
@ -1,12 +1,12 @@
|
|||
package com.tangem.data.staking
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.raise.Raise
|
||||
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.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
|
||||
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolBroadcastRequest
|
||||
|
|
@ -14,6 +14,7 @@ import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolTransactionReq
|
|||
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.P2PEthPoolRegionBlockedStore
|
||||
import com.tangem.datasource.local.token.P2PEthPoolVaultsStore
|
||||
import com.tangem.datasource.local.token.P2PVaultLimitsStore
|
||||
import com.tangem.domain.models.staking.P2PEthPoolStakingAccount
|
||||
|
|
@ -41,6 +42,7 @@ import kotlinx.coroutines.withContext
|
|||
/**
|
||||
* P2PEthPool staking repository implementation
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultP2PEthPoolRepository(
|
||||
private val p2pEthPoolApi: P2PEthPoolApi,
|
||||
private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
|
||||
|
|
@ -48,6 +50,7 @@ internal class DefaultP2PEthPoolRepository(
|
|||
private val tangemTechApi: TangemTechApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val stakingFeatureToggles: StakingFeatureToggles,
|
||||
private val p2pEthPoolRegionBlockedStore: P2PEthPoolRegionBlockedStore,
|
||||
) : P2PEthPoolRepository {
|
||||
|
||||
private val vaultConverter = P2PEthPoolVaultConverter
|
||||
|
|
@ -76,17 +79,31 @@ internal class DefaultP2PEthPoolRepository(
|
|||
|
||||
override suspend fun fetchVaults(network: P2PEthPoolNetwork) {
|
||||
val vaults = if (stakingFeatureToggles.isIntegrationEnabled(StakingIntegrationID.P2PEthPool)) {
|
||||
getVaults(network).getOrElse { error ->
|
||||
TangemLogger.e("Error fetching P2PEthPool vaults: $error")
|
||||
emptyList()
|
||||
}
|
||||
getVaults(network).fold(
|
||||
ifLeft = { error ->
|
||||
TangemLogger.e("Error fetching P2PEthPool vaults: $error")
|
||||
p2pEthPoolRegionBlockedStore.store(error.isRegionBlocked())
|
||||
emptyList()
|
||||
},
|
||||
ifRight = { fetched ->
|
||||
p2pEthPoolRegionBlockedStore.store(false)
|
||||
fetched
|
||||
},
|
||||
)
|
||||
} else {
|
||||
p2pEthPoolRegionBlockedStore.store(false)
|
||||
emptyList()
|
||||
}
|
||||
|
||||
p2pEthPoolVaultsStore.store(vaults)
|
||||
}
|
||||
|
||||
private fun StakingError.isRegionBlocked(): Boolean {
|
||||
if (!stakingFeatureToggles.isRegionUnavailableHandlingEnabled()) return false
|
||||
val httpException = (this as? StakingError.UnknownError)?.exception as? ApiResponseError.HttpException
|
||||
return httpException?.code == ApiResponseError.HttpException.Code.UNAVAILABLE_FOR_LEGAL_REASONS
|
||||
}
|
||||
|
||||
override suspend fun getVaults(network: P2PEthPoolNetwork): Either<StakingError, List<P2PEthPoolVault>> = either {
|
||||
withContext(dispatchers.io) {
|
||||
handleApiResponse(p2pEthPoolApi.getVaults(network.value)) { result ->
|
||||
|
|
@ -194,8 +211,10 @@ internal class DefaultP2PEthPoolRepository(
|
|||
return combine(
|
||||
getVaultsFlow().distinctUntilChanged(),
|
||||
getVaultLimitsFlow().distinctUntilChanged(),
|
||||
) { vaults, limits ->
|
||||
p2pEthPoolRegionBlockedStore.get(),
|
||||
) { vaults, limits, regionBlocked ->
|
||||
when {
|
||||
regionBlocked -> StakingAvailability.RegionUnavailable
|
||||
vaults.isEmpty() -> StakingAvailability.TemporaryUnavailable
|
||||
limits == null -> StakingAvailability.TemporaryUnavailable
|
||||
else -> {
|
||||
|
|
@ -211,6 +230,7 @@ internal class DefaultP2PEthPoolRepository(
|
|||
}
|
||||
|
||||
override suspend fun getStakingAvailabilitySync(): StakingAvailability {
|
||||
if (p2pEthPoolRegionBlockedStore.getSyncOrNull() == true) return StakingAvailability.RegionUnavailable
|
||||
val vaults = getVaultsSync()
|
||||
if (vaults.isEmpty()) return StakingAvailability.TemporaryUnavailable
|
||||
val limits = getVaultLimitsSyncOrNull() ?: return StakingAvailability.TemporaryUnavailable
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitEr
|
|||
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.P2PEthPoolRegionBlockedStore
|
||||
import com.tangem.datasource.local.token.P2PEthPoolVaultsStore
|
||||
import com.tangem.datasource.local.token.P2PVaultLimitsStore
|
||||
import com.tangem.datasource.local.token.StakingActionsStore
|
||||
|
|
@ -83,6 +84,7 @@ internal object StakingDataModule {
|
|||
tangemTechApi: TangemTechApi,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
stakingFeatureToggles: StakingFeatureToggles,
|
||||
p2pEthPoolRegionBlockedStore: P2PEthPoolRegionBlockedStore,
|
||||
): P2PEthPoolRepository {
|
||||
return DefaultP2PEthPoolRepository(
|
||||
p2pEthPoolApi = p2pEthPoolApi,
|
||||
|
|
@ -91,6 +93,7 @@ internal object StakingDataModule {
|
|||
tangemTechApi = tangemTechApi,
|
||||
dispatchers = dispatchers,
|
||||
stakingFeatureToggles = stakingFeatureToggles,
|
||||
p2pEthPoolRegionBlockedStore = p2pEthPoolRegionBlockedStore,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@ internal class DefaultStakingFeatureToggles(
|
|||
)
|
||||
}
|
||||
|
||||
override fun isRegionUnavailableHandlingEnabled(): Boolean {
|
||||
return featureTogglesManager.isFeatureEnabled(
|
||||
FeatureToggles.AND_15231_STAKING_REGION_UNAVAILABLE_ENABLED,
|
||||
)
|
||||
}
|
||||
|
||||
private fun StakingIntegrationID.getFeatureToggle(): FeatureToggles? = when (this) {
|
||||
is StakingIntegrationID.P2PEthPool -> null
|
||||
is StakingIntegrationID.StakeKit -> this.getStakeKitFeatureToggle()
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
package com.tangem.data.staking
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
|
||||
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.P2PEthPoolVaultsResponse
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.token.P2PEthPoolRegionBlockedStore
|
||||
import com.tangem.datasource.local.token.P2PEthPoolVaultsStore
|
||||
import com.tangem.datasource.local.token.P2PVaultLimitsStore
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
|
|
@ -10,13 +16,16 @@ 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.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
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.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -29,6 +38,7 @@ internal class DefaultP2PEthPoolRepositoryAvailabilityTest {
|
|||
private val limitsStore = mockk<P2PVaultLimitsStore>(relaxed = true)
|
||||
private val tangemTechApi = mockk<TangemTechApi>(relaxed = true)
|
||||
private val featureToggles = mockk<StakingFeatureToggles>(relaxed = true)
|
||||
private val regionBlockedStore = mockk<P2PEthPoolRegionBlockedStore>(relaxed = true)
|
||||
|
||||
private val repository = DefaultP2PEthPoolRepository(
|
||||
p2pEthPoolApi = api,
|
||||
|
|
@ -37,8 +47,14 @@ internal class DefaultP2PEthPoolRepositoryAvailabilityTest {
|
|||
tangemTechApi = tangemTechApi,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
stakingFeatureToggles = featureToggles,
|
||||
p2pEthPoolRegionBlockedStore = regionBlockedStore,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(api, vaultsStore, limitsStore, tangemTechApi, featureToggles, regionBlockedStore)
|
||||
}
|
||||
|
||||
private fun buildVault(address: String, totalAssets: String) = P2PEthPoolVault(
|
||||
vaultAddress = address,
|
||||
displayName = "Vault",
|
||||
|
|
@ -59,10 +75,22 @@ internal class DefaultP2PEthPoolRepositoryAvailabilityTest {
|
|||
private fun limits(address: String, limit: String) =
|
||||
mapOf(address.lowercase() to VaultLimitInfo(limit = BigDecimal(limit), coefficient = null))
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun regionBlockedErrorResponse(): ApiResponse<P2PEthPoolResponse<P2PEthPoolVaultsResponse>> {
|
||||
return ApiResponse.Error(
|
||||
cause = ApiResponseError.HttpException(
|
||||
code = ApiResponseError.HttpException.Code.UNAVAILABLE_FOR_LEGAL_REASONS,
|
||||
message = "451",
|
||||
errorBody = null,
|
||||
),
|
||||
) as ApiResponse<P2PEthPoolResponse<P2PEthPoolVaultsResponse>>
|
||||
}
|
||||
|
||||
@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
|
||||
every { regionBlockedStore.get() } returns MutableStateFlow(false)
|
||||
|
||||
val result = repository.getStakingAvailability().first()
|
||||
|
||||
|
|
@ -73,12 +101,89 @@ internal class DefaultP2PEthPoolRepositoryAvailabilityTest {
|
|||
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
|
||||
every { regionBlockedStore.get() } returns MutableStateFlow(false)
|
||||
|
||||
val result = repository.getStakingAvailability().first()
|
||||
|
||||
assertThat(result).isInstanceOf(StakingAvailability.Available::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN region blocked flag WHEN getStakingAvailability THEN emits RegionUnavailable`() = runTest {
|
||||
// Arrange
|
||||
every { vaultsStore.get() } returns flowOf(emptyList())
|
||||
every { limitsStore.get() } returns MutableStateFlow(null)
|
||||
every { regionBlockedStore.get() } returns MutableStateFlow(true)
|
||||
|
||||
// Act
|
||||
val result = repository.getStakingAvailability().first()
|
||||
|
||||
// Assert
|
||||
assertThat(result).isInstanceOf(StakingAvailability.RegionUnavailable::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN region blocked flag WHEN getStakingAvailabilitySync THEN returns RegionUnavailable`() = runTest {
|
||||
// Arrange
|
||||
coEvery { regionBlockedStore.getSyncOrNull() } returns true
|
||||
|
||||
// Act
|
||||
val result = repository.getStakingAvailabilitySync()
|
||||
|
||||
// Assert
|
||||
assertThat(result).isInstanceOf(StakingAvailability.RegionUnavailable::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN 451 AND toggle on WHEN fetchVaults THEN region flag set true`() = runTest {
|
||||
// Arrange
|
||||
every { featureToggles.isIntegrationEnabled(any()) } returns true
|
||||
every { featureToggles.isRegionUnavailableHandlingEnabled() } returns true
|
||||
coEvery { api.getVaults(any()) } returns regionBlockedErrorResponse()
|
||||
|
||||
// Act
|
||||
repository.fetchVaults()
|
||||
|
||||
// Assert
|
||||
coVerify { regionBlockedStore.store(true) }
|
||||
coVerify { vaultsStore.store(emptyList()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN 451 AND toggle off WHEN fetchVaults THEN region flag stays false`() = runTest {
|
||||
// Arrange
|
||||
every { featureToggles.isIntegrationEnabled(any()) } returns true
|
||||
every { featureToggles.isRegionUnavailableHandlingEnabled() } returns false
|
||||
coEvery { api.getVaults(any()) } returns regionBlockedErrorResponse()
|
||||
|
||||
// Act
|
||||
repository.fetchVaults()
|
||||
|
||||
// Assert
|
||||
coVerify { regionBlockedStore.store(false) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN successful fetch WHEN fetchVaults THEN region flag reset to false`() = runTest {
|
||||
// Arrange
|
||||
every { featureToggles.isIntegrationEnabled(any()) } returns true
|
||||
coEvery { api.getVaults(any()) } returns ApiResponse.Success(
|
||||
P2PEthPoolResponse(
|
||||
error = null,
|
||||
result = P2PEthPoolVaultsResponse(
|
||||
network = P2PEthPoolNetworkDTO.MAINNET,
|
||||
vaults = emptyList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Act
|
||||
repository.fetchVaults()
|
||||
|
||||
// Assert
|
||||
coVerify { regionBlockedStore.store(false) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync - all vaults full - returns Full with option`() = runTest {
|
||||
coEvery { vaultsStore.getSync() } returns listOf(buildVault("0xABC", totalAssets = "999.95"))
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ 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.P2PEthPoolRegionBlockedStore
|
||||
import com.tangem.datasource.local.token.P2PEthPoolVaultsStore
|
||||
import com.tangem.datasource.local.token.P2PVaultLimitsStore
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
|
|
@ -37,6 +38,7 @@ internal class P2PEthPoolVaultFilterTest {
|
|||
private val featureToggles = mockk<StakingFeatureToggles> {
|
||||
every { isIntegrationEnabled(StakingIntegrationID.P2PEthPool) } returns true
|
||||
}
|
||||
private val regionBlockedStore = mockk<P2PEthPoolRegionBlockedStore>(relaxed = true)
|
||||
private val repository = DefaultP2PEthPoolRepository(
|
||||
p2pEthPoolApi = api,
|
||||
p2pEthPoolVaultsStore = store,
|
||||
|
|
@ -44,6 +46,7 @@ internal class P2PEthPoolVaultFilterTest {
|
|||
tangemTechApi = tangemTechApi,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
stakingFeatureToggles = featureToggles,
|
||||
p2pEthPoolRegionBlockedStore = regionBlockedStore,
|
||||
)
|
||||
|
||||
private fun buildVaultDTO(address: String) = P2PEthPoolVaultDTO(
|
||||
|
|
|
|||
|
|
@ -73,4 +73,32 @@ internal class DefaultStakingFeatureTogglesTest {
|
|||
featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_16148_SOLANA_UNSTAKE_VALIDATION_ENABLED)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle enabled WHEN isRegionUnavailableHandlingEnabled THEN returns true`() {
|
||||
// Arrange
|
||||
every {
|
||||
featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_15231_STAKING_REGION_UNAVAILABLE_ENABLED)
|
||||
} returns true
|
||||
|
||||
// Act
|
||||
val result = toggles.isRegionUnavailableHandlingEnabled()
|
||||
|
||||
// Assert
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle disabled WHEN isRegionUnavailableHandlingEnabled THEN returns false`() {
|
||||
// Arrange
|
||||
every {
|
||||
featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_15231_STAKING_REGION_UNAVAILABLE_ENABLED)
|
||||
} returns false
|
||||
|
||||
// Act
|
||||
val result = toggles.isRegionUnavailableHandlingEnabled()
|
||||
|
||||
// Assert
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue