Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-22 14:06:01 +03:00
parent 92794cb2f6
commit 51ce41ee51
10 changed files with 1043 additions and 7 deletions

View file

@ -0,0 +1,88 @@
package com.tangem.common.test.data.staking
import com.tangem.datasource.api.stakekit.models.response.model.AddressArgumentDTO
import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO
import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
import com.tangem.domain.staking.model.StakingID
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
object MockYieldDTOFactory {
val defaultStakingID = StakingID(
integrationId = "ton-ton-chorus-one-pools-staking",
address = "0x1",
)
fun create(stakingID: StakingID = defaultStakingID): YieldDTO {
return YieldDTO(
id = stakingID.integrationId,
token = TokenDTO(
name = "Miguel Estes",
network = NetworkTypeDTO.POLYGON,
symbol = "splendide",
decimals = 1323,
address = null,
coinGeckoId = null,
logoURI = null,
isPoints = null,
),
tokens = listOf(),
args = YieldDTO.ArgsDTO(
enter = YieldDTO.ArgsDTO.Enter(
addresses = YieldDTO.ArgsDTO.Enter.Addresses(
address = AddressArgumentDTO(required = false),
),
args = mapOf(),
),
exit = null,
),
status = YieldDTO.StatusDTO(enter = true, exit = true),
apy = BigDecimal.ONE,
rewardRate = 1.0,
rewardType = YieldDTO.RewardTypeDTO.UNKNOWN,
metadata = YieldDTO.MetadataDTO(
name = "name",
logoUri = "logoUri",
description = "description",
documentation = null,
gasFeeTokenDTO = TokenDTO(
name = "Johnnie Mullen",
network = NetworkTypeDTO.POLYGON,
symbol = "fuisset",
decimals = 2957,
address = null,
coinGeckoId = null,
logoURI = null,
isPoints = null,
),
tokenDTO = TokenDTO(
name = "Lazaro Wood",
network = NetworkTypeDTO.POLYGON,
symbol = "vocent",
decimals = 1602,
address = null,
coinGeckoId = null,
logoURI = null,
isPoints = null,
),
tokensDTO = listOf(),
type = "type",
rewardSchedule = YieldDTO.MetadataDTO.RewardScheduleDTO.DAY,
cooldownPeriod = null,
warmupPeriod = YieldDTO.MetadataDTO.PeriodDTO(1),
rewardClaiming = YieldDTO.MetadataDTO.RewardClaimingDTO.AUTO,
defaultValidator = null,
minimumStake = null,
supportsMultipleValidators = null,
revshare = YieldDTO.MetadataDTO.EnabledDTO(enabled = true),
fee = YieldDTO.MetadataDTO.EnabledDTO(enabled = true),
),
validators = listOf(),
isAvailable = true,
)
}
}

View file

@ -0,0 +1,114 @@
package com.tangem.data.staking.fetcher
import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.left
import arrow.core.raise.catch
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.tangem.data.staking.store.YieldsBalancesStore
import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory
import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.core.flow.FlowFetcher
import com.tangem.domain.core.utils.catchOn
import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams
import com.tangem.domain.staking.model.StakingID
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import timber.log.Timber
internal fun <Params : YieldBalanceFetcherParams> commonFetcher(
implementor: YieldBalanceFetcherImplementor<Params>,
stakingYieldsStore: StakingYieldsStore,
yieldsBalancesStore: YieldsBalancesStore,
dispatchers: CoroutineDispatcherProvider,
): FlowFetcher<Params> {
return CommonYieldBalanceFetcher(
implementor = implementor,
stakingYieldsStore = stakingYieldsStore,
yieldsBalancesStore = yieldsBalancesStore,
dispatchers = dispatchers,
)
}
/**
* Common implementation of YieldBalanceFetcher
*
* @param implementor fetcher implementor
* @property stakingYieldsStore staking yields store
* @property yieldsBalancesStore yields balances store
* @property dispatchers dispatchers
*/
private class CommonYieldBalanceFetcher<Params : YieldBalanceFetcherParams>(
private val implementor: YieldBalanceFetcherImplementor<Params>,
private val stakingYieldsStore: StakingYieldsStore,
private val yieldsBalancesStore: YieldsBalancesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : FlowFetcher<Params> {
override suspend fun invoke(params: Params): Either<Throwable, Unit> {
val stakingIds = getStakingIds(params).getOrElse {
return it.left()
}
return Either.catchOn(dispatchers.default) {
val requests = prefetch(userWalletId = params.userWalletId, stakingIds = stakingIds)
implementor.fetch(params = params, stakingIds = stakingIds, requests)
}
.onLeft { yieldsBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = stakingIds) }
}
private suspend fun getStakingIds(params: Params): Either<Throwable, Set<StakingID>> = either {
val stakingIds = catch(
block = { implementor.createStakingIds(params = params) },
catch = { raise(it) },
)
ensure(stakingIds.isNotEmpty()) {
val exception = IllegalStateException("Unable to create staking ids for $params: list is empty")
Timber.e(exception)
raise(exception)
}
stakingIds
}
private suspend fun prefetch(
userWalletId: UserWalletId,
stakingIds: Set<StakingID>,
): List<YieldBalanceRequestBody> {
yieldsBalancesStore.refresh(userWalletId = userWalletId, stakingIds = stakingIds)
val yieldDTOs = stakingYieldsStore.getSyncWithTimeout()
if (yieldDTOs.isNullOrEmpty()) {
val exception = IllegalStateException("No enabled yields for $userWalletId")
Timber.e(exception)
throw exception
}
val yieldIds = yieldDTOs.mapNotNullTo(destination = hashSetOf(), transform = YieldDTO::id)
val requests = stakingIds
.filter { stakingId -> yieldIds.any { it == stakingId.integrationId } }
.map(YieldBalanceRequestBodyFactory::create)
if (requests.isEmpty()) {
val exception = IllegalStateException(
"""
No available yields to fetch yield balances:
userWalletId: $userWalletId
stakingIds: ${stakingIds.joinToString()}
""".trimIndent(),
)
Timber.d(exception)
throw exception
}
return requests
}
}

View file

@ -9,10 +9,10 @@ import com.tangem.domain.staking.model.StakingID
*
[REDACTED_AUTHOR]
*/
internal interface YieldBalanceFetcherImplementor<out Params : YieldBalanceFetcherParams> {
internal interface YieldBalanceFetcherImplementor<in Params : YieldBalanceFetcherParams> {
/** Create set of [StakingID] */
suspend fun createStakingIds(params: @UnsafeVariance Params): Set<StakingID>
suspend fun createStakingIds(params: Params): Set<StakingID>
/**
* Fetch yield balances
@ -21,9 +21,5 @@ internal interface YieldBalanceFetcherImplementor<out Params : YieldBalanceFetch
* @param stakingIds set of [StakingID]
* @param requests requests
*/
suspend fun fetch(
params: @UnsafeVariance Params,
stakingIds: Set<StakingID>,
requests: List<YieldBalanceRequestBody>,
)
suspend fun fetch(params: Params, stakingIds: Set<StakingID>, requests: List<YieldBalanceRequestBody>)
}

View file

@ -0,0 +1,51 @@
package com.tangem.data.staking.multi
import com.tangem.data.staking.fetcher.YieldBalanceFetcherImplementor
import com.tangem.data.staking.fetcher.commonFetcher
import com.tangem.data.staking.store.YieldsBalancesStore
import com.tangem.data.staking.utils.StakingIdFactory
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.core.flow.FlowFetcher
import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
/**
* Default implementation of [MultiYieldBalanceFetcher]
*
* @property stakingYieldsStore staking yields store
* @property yieldsBalancesStore yields balances store
* @property stakingIdFactory factory for creating StakingID
* @property stakeKitApi stake kit API
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
*/
internal class DefaultMultiYieldBalanceFetcher(
private val stakingYieldsStore: StakingYieldsStore,
private val yieldsBalancesStore: YieldsBalancesStore,
private val stakingIdFactory: StakingIdFactory,
private val stakeKitApi: StakeKitApi,
private val dispatchers: CoroutineDispatcherProvider,
) : MultiYieldBalanceFetcher,
FlowFetcher<YieldBalanceFetcherParams.Multi> by commonFetcher(
implementor = createMultiFetcherImplementor(yieldsBalancesStore, stakingIdFactory, stakeKitApi, dispatchers),
stakingYieldsStore = stakingYieldsStore,
yieldsBalancesStore = yieldsBalancesStore,
dispatchers = dispatchers,
)
private fun createMultiFetcherImplementor(
yieldsBalancesStore: YieldsBalancesStore,
stakingIdFactory: StakingIdFactory,
stakeKitApi: StakeKitApi,
dispatchers: CoroutineDispatcherProvider,
): YieldBalanceFetcherImplementor<YieldBalanceFetcherParams.Multi> {
return MultiYieldBalanceFetcherImplementor(
yieldsBalancesStore = yieldsBalancesStore,
stakingIdFactory = stakingIdFactory,
stakeKitApi = stakeKitApi,
dispatchers = dispatchers,
)
}

View file

@ -0,0 +1,52 @@
package com.tangem.data.staking.single
import com.tangem.data.staking.fetcher.YieldBalanceFetcherImplementor
import com.tangem.data.staking.fetcher.commonFetcher
import com.tangem.data.staking.store.YieldsBalancesStore
import com.tangem.data.staking.utils.StakingIdFactory
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.core.flow.FlowFetcher
import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
/**
* Default implementation of [MultiYieldBalanceFetcher]
*
* @property stakingYieldsStore staking yields store
* @property yieldsBalancesStore yields balances store
* @property stakingIdFactory factory for creating StakingID
* @property stakeKitApi stake kit API
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
*/
internal class DefaultSingleYieldBalanceFetcher(
private val stakingYieldsStore: StakingYieldsStore,
private val yieldsBalancesStore: YieldsBalancesStore,
private val stakingIdFactory: StakingIdFactory,
private val stakeKitApi: StakeKitApi,
private val dispatchers: CoroutineDispatcherProvider,
) : SingleYieldBalanceFetcher,
FlowFetcher<YieldBalanceFetcherParams.Single> by commonFetcher(
implementor = createSingleFetcherImplementor(yieldsBalancesStore, stakingIdFactory, stakeKitApi, dispatchers),
stakingYieldsStore = stakingYieldsStore,
yieldsBalancesStore = yieldsBalancesStore,
dispatchers = dispatchers,
)
private fun createSingleFetcherImplementor(
yieldsBalancesStore: YieldsBalancesStore,
stakingIdFactory: StakingIdFactory,
stakeKitApi: StakeKitApi,
dispatchers: CoroutineDispatcherProvider,
): YieldBalanceFetcherImplementor<YieldBalanceFetcherParams.Single> {
return SingleYieldBalanceFetcherImplementor(
yieldsBalancesStore = yieldsBalancesStore,
stakingIdFactory = stakingIdFactory,
stakeKitApi = stakeKitApi,
dispatchers = dispatchers,
)
}

View file

@ -0,0 +1,27 @@
package com.tangem.data.staking.utils
import com.tangem.datasource.api.stakekit.models.request.Address
import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody
import com.tangem.domain.staking.model.StakingID
/**
* Factory for creating [YieldBalanceRequestBody]
*
[REDACTED_AUTHOR]
*/
internal object YieldBalanceRequestBodyFactory {
fun create(stakingID: StakingID): YieldBalanceRequestBody {
return YieldBalanceRequestBody(
addresses = Address(
address = stakingID.address,
additionalAddresses = null, // todo fill additional addresses metadata if needed
explorerUrl = "", // todo fill exporer url [REDACTED_JIRA]
),
args = YieldBalanceRequestBody.YieldBalanceRequestArgs(
validatorAddresses = listOf(), // todo add validators [REDACTED_JIRA]
),
integrationId = stakingID.integrationId,
)
}
}

View file

@ -0,0 +1,323 @@
package com.tangem.data.staking.multi
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
import com.tangem.common.test.data.staking.MockYieldDTOFactory
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.data.staking.store.YieldsBalancesStore
import com.tangem.data.staking.utils.StakingIdFactory
import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams
import com.tangem.domain.staking.model.StakingID
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Test
/**
[REDACTED_AUTHOR]
*/
internal class DefaultMultiYieldBalanceFetcherTest {
private val stakingYieldsStore: StakingYieldsStore = mockk()
private val yieldsBalancesStore: YieldsBalancesStore = mockk()
private val stakingIdFactory: StakingIdFactory = mockk()
private val stakeKitApi: StakeKitApi = mockk()
private val fetcher = DefaultMultiYieldBalanceFetcher(
stakingYieldsStore = stakingYieldsStore,
yieldsBalancesStore = yieldsBalancesStore,
stakingIdFactory = stakingIdFactory,
stakeKitApi = stakeKitApi,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@Test
fun `fetch yields balances successfully`() = runTest {
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap)
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId)
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId)
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId))
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields
val requests = tonAndSolanaIds.map(YieldBalanceRequestBodyFactory::create).sortedBy { it.integrationId }
val result = setOf(
MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId),
MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId),
)
coEvery { stakeKitApi.getMultipleYieldBalances(requests) } returns ApiResponse.Success(result)
coEvery { yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result) } just Runs
val actual = fetcher(params)
coVerify {
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
stakingYieldsStore.getSyncWithTimeout()
stakeKitApi.getMultipleYieldBalances(requests)
yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result)
}
coVerify(inverse = true) { yieldsBalancesStore.storeError(any(), any()) }
Truth.assertThat(actual.isRight()).isTrue()
}
@Test
fun `fetch yields balances failure if stakingIdFactory returns empty list`() = runTest {
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap)
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns emptySet()
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns emptySet()
val actual = fetcher(params)
coVerify {
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
}
coVerify(inverse = true) {
yieldsBalancesStore.refresh(any(), any<Set<StakingID>>())
stakingYieldsStore.getSyncWithTimeout()
stakeKitApi.getMultipleYieldBalances(any())
yieldsBalancesStore.storeActual(any(), any())
yieldsBalancesStore.storeError(any(), any())
}
val expected = IllegalStateException("Unable to create staking ids for $params: list is empty")
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
}
@Test
fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns null`() = runTest {
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap)
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId)
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId)
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns null
coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs
val actual = fetcher(params)
coVerify {
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds)
stakingYieldsStore.getSyncWithTimeout()
yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds)
}
coVerify(inverse = true) {
stakeKitApi.getMultipleYieldBalances(any())
yieldsBalancesStore.storeActual(userWalletId = any(), values = any())
}
val expected = IllegalStateException("No enabled yields for ${params.userWalletId}")
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
}
@Test
fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns empty list`() = runTest {
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap)
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId)
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId)
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns emptyList()
coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs
val actual = fetcher(params)
coVerify {
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
stakingYieldsStore.getSyncWithTimeout()
yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds)
}
coVerify(inverse = true) {
stakeKitApi.getMultipleYieldBalances(any())
yieldsBalancesStore.storeActual(userWalletId = any(), values = any())
}
val expected = IllegalStateException("No enabled yields for ${params.userWalletId}")
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
}
@Test
fun `fetch yields balances failure if yields converting is failed`() = runTest {
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap)
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId)
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId)
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
val yields = listOf(
MockYieldDTOFactory.create(tonId).copy(id = null),
MockYieldDTOFactory.create(solanaId).copy(id = null),
)
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields
coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs
val actual = fetcher(params)
coVerify {
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
stakingYieldsStore.getSyncWithTimeout()
yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds)
}
coVerify(inverse = true) {
stakeKitApi.getMultipleYieldBalances(any())
yieldsBalancesStore.storeActual(userWalletId = any(), values = any())
}
val expected = IllegalStateException(
"""
No available yields to fetch yield balances:
userWalletId: $userWalletId
stakingIds: ${setOf(solanaId, tonId).joinToString()}
""".trimIndent(),
)
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
}
@Test
fun `fetch yields balances failure if available yields does not contain ids from params`() = runTest {
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap)
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId)
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId)
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
val yields = listOf(MockYieldDTOFactory.create(StakingID(integrationId = "polygon", address = "0x1")))
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields
coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs
val actual = fetcher(params)
coVerify {
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
stakingYieldsStore.getSyncWithTimeout()
yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds)
}
coVerify(inverse = true) {
stakeKitApi.getMultipleYieldBalances(any())
yieldsBalancesStore.storeActual(userWalletId = any(), values = any())
}
val expected = IllegalStateException(
"""
No available yields to fetch yield balances:
userWalletId: $userWalletId
stakingIds: ${setOf(solanaId, tonId).joinToString()}
""".trimIndent(),
)
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
}
@Test
fun `fetch yields balances failure if stakeKitApi getMultipleYieldBalances is failed`() = runTest {
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap)
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId)
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId)
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId))
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields
val requests = setOf(solanaId, tonId).map(YieldBalanceRequestBodyFactory::create)
@Suppress("UNCHECKED_CAST")
val errorResponse = ApiResponse.Error(ApiResponseError.NetworkException)
as ApiResponse<Set<YieldBalanceWrapperDTO>>
coEvery { stakeKitApi.getMultipleYieldBalances(requests) } returns errorResponse
coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs
val actual = fetcher(params)
coVerify {
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
stakingYieldsStore.getSyncWithTimeout()
stakeKitApi.getMultipleYieldBalances(requests)
yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
}
coVerify(inverse = true) { yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) }
val expected = ApiResponseError.NetworkException
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
}
private companion object {
val userWalletId = UserWalletId("011")
val mocks = MockCryptoCurrencyFactory()
val ton = mocks.createCoin(Blockchain.TON)
val solana = mocks.createCoin(Blockchain.Solana)
val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId
val solanaId = StakingID(
integrationId = "solana-sol-native-multivalidator-staking",
address = "0x1",
)
val tonAndSolanaIds = setOf(tonId, solanaId)
}
}

View file

@ -0,0 +1,363 @@
package com.tangem.data.staking.single
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
import com.tangem.common.test.data.staking.MockYieldDTOFactory
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.data.staking.store.YieldsBalancesStore
import com.tangem.data.staking.utils.StakingIdFactory
import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO
import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams
import com.tangem.domain.staking.model.StakingID
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
internal class DefaultSingleYieldBalanceFetcherTest {
private val stakingYieldsStore: StakingYieldsStore = mockk()
private val yieldsBalancesStore: YieldsBalancesStore = mockk()
private val stakingIdFactory: StakingIdFactory = mockk()
private val stakeKitApi: StakeKitApi = mockk()
private val fetcher = DefaultSingleYieldBalanceFetcher(
stakingYieldsStore = stakingYieldsStore,
yieldsBalancesStore = yieldsBalancesStore,
stakingIdFactory = stakingIdFactory,
stakeKitApi = stakeKitApi,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@Test
fun `fetch yields balances successfully`() = runTest {
val params = YieldBalanceFetcherParams.Single(
userWalletId = userWalletId,
currencyId = ton.id,
network = ton.network,
)
coEvery { stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) } returns tonId
coEvery {
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId))
} just Runs
val yields = listOf(MockYieldDTOFactory.create(tonId))
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields
val request = YieldBalanceRequestBodyFactory.create(tonId)
val result = listOf(createBalanceDTO())
coEvery { stakeKitApi.getSingleYieldBalance(tonId.integrationId, request) } returns ApiResponse.Success(result)
val values = result.mapTo(hashSetOf()) { it.toWrapper(request) }
coEvery { yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = values) } just Runs
val actual = fetcher(params)
coVerify {
stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network)
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId))
stakingYieldsStore.getSyncWithTimeout()
stakeKitApi.getSingleYieldBalance(integrationId = tonId.integrationId, body = request)
yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = values)
}
coVerify(inverse = true) { yieldsBalancesStore.storeError(any(), any()) }
Truth.assertThat(actual.isRight()).isTrue()
}
@Test
fun `fetch yields balances failure if stakingIdFactory createForDefault returns null`() = runTest {
val params = YieldBalanceFetcherParams.Single(
userWalletId = userWalletId,
currencyId = ton.id,
network = ton.network,
)
coEvery { stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) } returns null
val actual = fetcher(params)
coVerify {
stakingIdFactory.createForDefault(userWalletId = userWalletId, currencyId = ton.id, network = ton.network)
}
coVerify(inverse = true) {
yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any())
stakingYieldsStore.getSyncWithTimeout()
stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any())
yieldsBalancesStore.storeActual(userWalletId = any(), values = any())
yieldsBalancesStore.storeError(userWalletId = any(), stakingIds = any())
}
val expected = IllegalStateException("Unable to create staking ids for $params: list is empty")
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
}
@Test
fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns null`() = runTest {
val params = YieldBalanceFetcherParams.Single(
userWalletId = userWalletId,
currencyId = ton.id,
network = ton.network,
)
coEvery { stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) } returns tonId
coEvery {
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId))
} just Runs
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns null
coEvery { yieldsBalancesStore.storeError(userWalletId, setOf(tonId)) } just Runs
val actual = fetcher(params)
coVerify {
stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network)
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId))
stakingYieldsStore.getSyncWithTimeout()
yieldsBalancesStore.storeError(userWalletId, setOf(tonId))
}
coVerify(inverse = true) {
stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any())
yieldsBalancesStore.storeActual(userWalletId = any(), values = any())
}
val expected = IllegalStateException("No enabled yields for ${params.userWalletId}")
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
}
@Test
fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns empty list`() = runTest {
val params = YieldBalanceFetcherParams.Single(
userWalletId = userWalletId,
currencyId = ton.id,
network = ton.network,
)
coEvery { stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) } returns tonId
coEvery {
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId))
} just Runs
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns emptyList()
coEvery { yieldsBalancesStore.storeError(userWalletId, setOf(tonId)) } just Runs
val actual = fetcher(params)
coVerify {
stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network)
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId))
stakingYieldsStore.getSyncWithTimeout()
yieldsBalancesStore.storeError(userWalletId, setOf(tonId))
}
coVerify(inverse = true) {
stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any())
yieldsBalancesStore.storeActual(userWalletId = any(), values = any())
}
val expected = IllegalStateException("No enabled yields for ${params.userWalletId}")
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
}
@Test
fun `fetch yields balances failure if yields converting is failed`() = runTest {
val params = YieldBalanceFetcherParams.Single(
userWalletId = userWalletId,
currencyId = ton.id,
network = ton.network,
)
coEvery { stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) } returns tonId
coEvery {
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId))
} just Runs
val yields = listOf(MockYieldDTOFactory.create(tonId).copy(id = null))
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields
coEvery { yieldsBalancesStore.storeError(userWalletId, setOf(tonId)) } just Runs
val actual = fetcher(params)
coVerify {
stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network)
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId))
stakingYieldsStore.getSyncWithTimeout()
yieldsBalancesStore.storeError(userWalletId, setOf(tonId))
}
coVerify(inverse = true) {
stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any())
yieldsBalancesStore.storeActual(userWalletId = any(), values = any())
}
val expected = IllegalStateException(
"""
No available yields to fetch yield balances:
userWalletId: $userWalletId
stakingIds: ${setOf(tonId).joinToString()}
""".trimIndent(),
)
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
}
@Test
fun `fetch yields balances failure if available yields does not contain ids from params`() = runTest {
val params = YieldBalanceFetcherParams.Single(
userWalletId = userWalletId,
currencyId = ton.id,
network = ton.network,
)
coEvery { stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) } returns tonId
coEvery {
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId))
} just Runs
val yields = listOf(MockYieldDTOFactory.create(StakingID(integrationId = "polygon", address = "0x1")))
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields
coEvery { yieldsBalancesStore.storeError(userWalletId, setOf(tonId)) } just Runs
val actual = fetcher(params)
coVerify {
stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network)
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId))
stakingYieldsStore.getSyncWithTimeout()
yieldsBalancesStore.storeError(userWalletId, setOf(tonId))
}
coVerify(inverse = true) {
stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any())
yieldsBalancesStore.storeActual(userWalletId = any(), values = any())
}
val expected = IllegalStateException(
"""
No available yields to fetch yield balances:
userWalletId: $userWalletId
stakingIds: ${setOf(tonId).joinToString()}
""".trimIndent(),
)
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
}
@Test
fun `fetch yields balances failure if stakeKitApi getMultipleYieldBalances is failed`() = runTest {
val params = YieldBalanceFetcherParams.Single(
userWalletId = userWalletId,
currencyId = ton.id,
network = ton.network,
)
coEvery { stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) } returns tonId
coEvery {
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId))
} just Runs
val yields = listOf(MockYieldDTOFactory.create(tonId))
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields
val request = YieldBalanceRequestBodyFactory.create(tonId)
@Suppress("UNCHECKED_CAST")
val errorResponse = ApiResponse.Error(ApiResponseError.NetworkException) as ApiResponse<List<BalanceDTO>>
coEvery { stakeKitApi.getSingleYieldBalance(tonId.integrationId, request) } returns errorResponse
coEvery { yieldsBalancesStore.storeError(userWalletId, setOf(tonId)) } just Runs
val actual = fetcher(params)
coVerify {
stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network)
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId))
stakingYieldsStore.getSyncWithTimeout()
stakeKitApi.getSingleYieldBalance(tonId.integrationId, request)
yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = setOf(tonId))
}
coVerify(inverse = true) { yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) }
val expected = ApiResponseError.NetworkException
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
}
private fun createBalanceDTO(): BalanceDTO {
return BalanceDTO(
groupId = "dictas",
type = BalanceDTO.BalanceTypeDTO.REWARDS,
amount = BigDecimal.ONE,
date = null,
pricePerShare = BigDecimal.ONE,
pendingActions = listOf(),
pendingActionConstraints = listOf(),
tokenDTO = TokenDTO(
name = "Casandra Paul",
network = NetworkTypeDTO.POLYGON,
symbol = "vim",
decimals = 3994,
address = null,
coinGeckoId = null,
logoURI = null,
isPoints = null,
),
validatorAddress = null,
validatorAddresses = listOf(),
providerId = null,
)
}
private fun BalanceDTO.toWrapper(request: YieldBalanceRequestBody): YieldBalanceWrapperDTO {
return YieldBalanceWrapperDTO(
balances = listOf(this),
integrationId = request.integrationId,
addresses = request.addresses,
)
}
private companion object {
val userWalletId = UserWalletId("011")
val mocks = MockCryptoCurrencyFactory()
val ton = mocks.createCoin(Blockchain.TON)
val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.domain.staking.multi
import com.tangem.domain.core.flow.FlowFetcher
import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams
/**
* Fetcher of yields balances
*
[REDACTED_AUTHOR]
*/
interface MultiYieldBalanceFetcher : FlowFetcher<YieldBalanceFetcherParams.Multi>

View file

@ -0,0 +1,11 @@
package com.tangem.domain.staking.single
import com.tangem.domain.core.flow.FlowFetcher
import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams
/**
* Fetcher of yield balance
*
[REDACTED_AUTHOR]
*/
interface SingleYieldBalanceFetcher : FlowFetcher<YieldBalanceFetcherParams.Single>