Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-21 15:29:10 +03:00
parent a2b440444e
commit 588456cc71
7 changed files with 240 additions and 39 deletions

View file

@ -0,0 +1,29 @@
package com.tangem.data.staking.fetcher
import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody
import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams
import com.tangem.domain.staking.model.StakingID
/**
* Implementor of internal logic of YieldBalanceFetcher
*
[REDACTED_AUTHOR]
*/
internal interface YieldBalanceFetcherImplementor<out Params : YieldBalanceFetcherParams> {
/** Create set of [StakingID] */
suspend fun createStakingIds(params: @UnsafeVariance Params): Set<StakingID>
/**
* Fetch yield balances
*
* @param params params
* @param stakingIds set of [StakingID]
* @param requests requests
*/
suspend fun fetch(
params: @UnsafeVariance Params,
stakingIds: Set<StakingID>,
requests: List<YieldBalanceRequestBody>,
)
}

View file

@ -0,0 +1,58 @@
package com.tangem.data.staking.multi
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.staking.fetcher.YieldBalanceFetcherImplementor
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.api.stakekit.models.request.YieldBalanceRequestBody
import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams
import com.tangem.domain.staking.model.StakingID
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
/**
* Implementor of fetcher for refreshing multiple yield balances
*
* @property yieldsBalancesStore yields balances store
* @property stakingIdFactory factory for creating [StakingID]
* @property stakeKitApi StakeKit API
* @property dispatchers dispatchers
*/
internal class MultiYieldBalanceFetcherImplementor(
private val yieldsBalancesStore: YieldsBalancesStore,
private val stakingIdFactory: StakingIdFactory,
private val stakeKitApi: StakeKitApi,
private val dispatchers: CoroutineDispatcherProvider,
) : YieldBalanceFetcherImplementor<YieldBalanceFetcherParams.Multi> {
override suspend fun createStakingIds(params: YieldBalanceFetcherParams.Multi): Set<StakingID> {
return params.currencyIdWithNetworkMap.flatMapTo(hashSetOf()) { (currencyId, network) ->
stakingIdFactory.create(userWalletId = params.userWalletId, currencyId = currencyId, network = network)
}
}
override suspend fun fetch(
params: YieldBalanceFetcherParams.Multi,
stakingIds: Set<StakingID>,
requests: List<YieldBalanceRequestBody>,
) {
safeApiCall(
call = {
val yieldBalances = withContext(dispatchers.io) {
stakeKitApi.getMultipleYieldBalances(requests).bind()
}
yieldsBalancesStore.storeActual(userWalletId = params.userWalletId, values = yieldBalances)
},
onError = {
Timber.e(it, "Unable to fetch yield balances $params")
yieldsBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = stakingIds)
throw it
},
)
}
}

View file

@ -0,0 +1,91 @@
package com.tangem.data.staking.single
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.staking.fetcher.YieldBalanceFetcherImplementor
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.api.stakekit.models.request.YieldBalanceRequestBody
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams
import com.tangem.domain.staking.model.StakingID
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
/**
* Implementor of fetcher for refreshing single yield balance
*
* @property yieldsBalancesStore yields balances store
* @property stakingIdFactory factory for creating [StakingID]
* @property stakeKitApi StakeKit API
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
*/
internal class SingleYieldBalanceFetcherImplementor(
private val yieldsBalancesStore: YieldsBalancesStore,
private val stakingIdFactory: StakingIdFactory,
private val stakeKitApi: StakeKitApi,
private val dispatchers: CoroutineDispatcherProvider,
) : YieldBalanceFetcherImplementor<YieldBalanceFetcherParams.Single> {
override suspend fun createStakingIds(params: YieldBalanceFetcherParams.Single): Set<StakingID> {
val dataStakingId = stakingIdFactory.createForDefault(
userWalletId = params.userWalletId,
currencyId = params.currencyId,
network = params.network,
) ?: return emptySet()
return setOf(
StakingID(integrationId = dataStakingId.integrationId, address = dataStakingId.address),
)
}
override suspend fun fetch(
params: YieldBalanceFetcherParams.Single,
stakingIds: Set<StakingID>,
requests: List<YieldBalanceRequestBody>,
) {
fetchInternal(
params = params,
stakingId = stakingIds.first(),
request = requests.first(),
)
}
private suspend fun fetchInternal(
params: YieldBalanceFetcherParams.Single,
stakingId: StakingID,
request: YieldBalanceRequestBody,
) {
safeApiCall(
call = {
val result = withContext(dispatchers.io) {
stakeKitApi.getSingleYieldBalance(
integrationId = stakingId.integrationId,
body = request,
).bind()
}
yieldsBalancesStore.storeActual(
userWalletId = params.userWalletId,
values = setOf(
YieldBalanceWrapperDTO(
balances = result,
integrationId = request.integrationId,
addresses = request.addresses,
),
),
)
},
onError = {
Timber.e(it, "Unable to fetch yield balances $params")
yieldsBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = setOf(stakingId))
throw it
},
)
}
}

View file

@ -59,32 +59,14 @@ internal class DefaultYieldsBalancesStore(
}
override suspend fun refresh(userWalletId: UserWalletId, stakingId: StakingID) {
updateBalanceInRuntime(userWalletId, stakingId) {
updateBalancesInRuntime(userWalletId = userWalletId, stakingIds = setOf(stakingId)) {
it.copySealed(source = StatusSource.CACHE)
}
}
override suspend fun refresh(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
runtimeStore.update(default = emptyMap()) { stored ->
stored.toMutableMap().apply {
val storedBalances = this[userWalletId].orEmpty()
val balances = stakingIds.mapTo(hashSetOf()) { stakingId ->
val balance = storedBalances.firstOrNull {
it.integrationId == stakingId.integrationId &&
it.address == stakingId.address
}
?: createDefaultBalance(id = stakingId)
balance.copySealed(source = StatusSource.CACHE)
}
val updatedBalances = storedBalances.addOrReplace(balances) { old, new ->
old.integrationId == new.integrationId && old.address == new.address
}
put(key = userWalletId, value = updatedBalances)
}
updateBalancesInRuntime(userWalletId = userWalletId, stakingIds = stakingIds) {
it.copySealed(source = StatusSource.CACHE)
}
}
@ -95,8 +77,8 @@ internal class DefaultYieldsBalancesStore(
}
}
override suspend fun storeError(userWalletId: UserWalletId, stakingId: StakingID) {
updateBalanceInRuntime(userWalletId, stakingId) {
override suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
updateBalancesInRuntime(userWalletId = userWalletId, stakingIds = stakingIds) {
it.copySealed(source = StatusSource.ONLY_CACHE)
}
}
@ -127,26 +109,27 @@ internal class DefaultYieldsBalancesStore(
}
}
private suspend fun updateBalanceInRuntime(
private suspend fun updateBalancesInRuntime(
userWalletId: UserWalletId,
stakingID: StakingID,
stakingIds: Set<StakingID>,
update: (YieldBalance) -> YieldBalance,
) {
runtimeStore.update(default = emptyMap()) { stored ->
stored.toMutableMap().apply {
val balance = this[userWalletId].orEmpty()
.firstOrNull {
it.integrationId == stakingID.integrationId &&
it.address == stakingID.address
}
?: createDefaultBalance(id = stakingID)
val portfolioBalances = stored[userWalletId].orEmpty()
val updatedBalances = this[userWalletId].orEmpty()
.addOrReplace(item = update(balance)) {
it.integrationId == balance.integrationId &&
it.address == balance.address
val balances = stakingIds.mapTo(hashSetOf()) { stakingId ->
val balance = portfolioBalances.firstOrNull {
it.integrationId == stakingId.integrationId && it.address == stakingId.address
}
if (balance != null) update(balance) else createDefaultBalance(id = stakingId)
}
val updatedBalances = portfolioBalances.addOrReplace(items = balances) { old, new ->
old.integrationId == new.integrationId && old.address == new.address
}
put(key = userWalletId, value = updatedBalances)
}
}

View file

@ -25,6 +25,6 @@ interface YieldsBalancesStore {
/** Store actual [values] by [userWalletId] */
suspend fun storeActual(userWalletId: UserWalletId, values: Set<YieldBalanceWrapperDTO>)
/** Store error by [userWalletId] and [stakingId] */
suspend fun storeError(userWalletId: UserWalletId, stakingId: StakingID)
/** Store error by [userWalletId] and [stakingIds] */
suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
}

View file

@ -137,7 +137,7 @@ internal class YieldsBalancesStoreUpdateMethodsTest {
@Test
fun `store error if runtime store is empty`() = runTest {
store.storeError(userWalletId = userWalletId, stakingId = stakingId)
store.storeError(userWalletId = userWalletId, stakingIds = setOf(stakingId))
val runtimeExpected = mapOf(
userWalletId to setOf(
@ -162,7 +162,7 @@ internal class YieldsBalancesStoreUpdateMethodsTest {
),
)
store.storeError(userWalletId = userWalletId, stakingId = stakingId)
store.storeError(userWalletId = userWalletId, stakingIds = setOf(stakingId))
val runtimeExpected = mapOf(
userWalletId to setOf(wrapper.toDomain(source = StatusSource.ONLY_CACHE)),

View file

@ -0,0 +1,40 @@
package com.tangem.domain.staking.fetcher
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
/**
* Params for fetchers of yield balance
*
[REDACTED_AUTHOR]
*/
sealed interface YieldBalanceFetcherParams {
/** User wallet ID */
val userWalletId: UserWalletId
/**
* Params for fetching multiple yield balances
*
* @property userWalletId user wallet ID
* @property currencyIdWithNetworkMap map of currency ID to network
*/
data class Multi(
override val userWalletId: UserWalletId,
val currencyIdWithNetworkMap: Map<CryptoCurrency.ID, Network>,
) : YieldBalanceFetcherParams
/**
* Params for fetching single yield balance
*
* @property userWalletId user wallet ID
* @property currencyId currency ID
* @property network network
*/
data class Single(
override val userWalletId: UserWalletId,
val currencyId: CryptoCurrency.ID,
val network: Network,
) : YieldBalanceFetcherParams
}