Updated on 2026-08-14
This commit is contained in:
commit
cab30e9431
186 changed files with 3729 additions and 2030 deletions
|
|
@ -2,7 +2,6 @@ package com.tangem.data.staking
|
|||
|
||||
import android.util.Base64
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.raise.catch
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
|
|
@ -30,8 +29,6 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrap
|
|||
import com.tangem.datasource.api.stakekit.models.response.model.transaction.tron.TronStakeKitTransaction
|
||||
import com.tangem.datasource.local.token.StakingBalanceStore
|
||||
import com.tangem.datasource.local.token.StakingYieldsStore
|
||||
import com.tangem.domain.core.lce.LceFlow
|
||||
import com.tangem.domain.core.lce.lceFlow
|
||||
import com.tangem.domain.staking.model.StakingApproval
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.staking.model.StakingEntryInfo
|
||||
|
|
@ -57,6 +54,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import com.tangem.utils.extensions.orZero
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
|
|
@ -103,10 +101,6 @@ internal class DefaultStakingRepository(
|
|||
private val yieldBalanceConverter = YieldBalanceConverter()
|
||||
private val yieldBalanceListConverter = YieldBalanceListConverter(yieldBalanceConverter)
|
||||
|
||||
private val isYieldBalanceFetching = MutableStateFlow(
|
||||
value = emptyMap<UserWalletId, Boolean>(),
|
||||
)
|
||||
|
||||
private val tronStakeKitTransactionAdapter by lazy { moshi.adapter(TronStakeKitTransaction::class.java) }
|
||||
|
||||
override fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String = with(cryptoCurrencyId) {
|
||||
|
|
@ -126,7 +120,7 @@ internal class DefaultStakingRepository(
|
|||
val stakingTokensWithYields = stakeKitApi.getEnabledYields(preferredValidatorsOnly = false)
|
||||
.getOrThrow()
|
||||
|
||||
stakingYieldsStore.store(stakingTokensWithYields.data.filter { it.isAvailable })
|
||||
stakingYieldsStore.store(stakingTokensWithYields.data.filter { it.isAvailable ?: false })
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -151,7 +145,7 @@ internal class DefaultStakingRepository(
|
|||
val yield = getYield(cryptoCurrencyId, symbol)
|
||||
|
||||
StakingEntryInfo(
|
||||
apr = requireNotNull(yield.validators.maxByOrNull { it.apr.orZero() }?.apr),
|
||||
apr = requireNotNull(yield.preferredValidators.maxByOrNull { it.apr.orZero() }?.apr),
|
||||
rewardSchedule = yield.metadata.rewardSchedule,
|
||||
tokenSymbol = yield.token.symbol,
|
||||
)
|
||||
|
|
@ -207,23 +201,21 @@ internal class DefaultStakingRepository(
|
|||
): StakingAction {
|
||||
return withContext(dispatchers.io) {
|
||||
val response = when (params.actionCommonType) {
|
||||
StakingActionCommonType.ENTER -> stakeKitApi.createEnterAction(
|
||||
StakingActionCommonType.Enter -> stakeKitApi.createEnterAction(
|
||||
createActionRequestBody(
|
||||
userWalletId,
|
||||
network,
|
||||
params,
|
||||
),
|
||||
)
|
||||
StakingActionCommonType.EXIT -> stakeKitApi.createExitAction(
|
||||
StakingActionCommonType.Exit -> stakeKitApi.createExitAction(
|
||||
createActionRequestBody(
|
||||
userWalletId,
|
||||
network,
|
||||
params,
|
||||
),
|
||||
)
|
||||
StakingActionCommonType.PENDING_OTHER,
|
||||
StakingActionCommonType.PENDING_REWARDS,
|
||||
-> stakeKitApi.createPendingAction(
|
||||
is StakingActionCommonType.Pending -> stakeKitApi.createPendingAction(
|
||||
createPendingActionRequestBody(params),
|
||||
)
|
||||
}
|
||||
|
|
@ -239,23 +231,21 @@ internal class DefaultStakingRepository(
|
|||
): StakingGasEstimate {
|
||||
return withContext(dispatchers.io) {
|
||||
val gasEstimateDTO = when (params.actionCommonType) {
|
||||
StakingActionCommonType.ENTER -> stakeKitApi.estimateGasOnEnter(
|
||||
StakingActionCommonType.Enter -> stakeKitApi.estimateGasOnEnter(
|
||||
createActionRequestBody(
|
||||
userWalletId,
|
||||
network,
|
||||
params,
|
||||
),
|
||||
)
|
||||
StakingActionCommonType.EXIT -> stakeKitApi.estimateGasOnExit(
|
||||
StakingActionCommonType.Exit -> stakeKitApi.estimateGasOnExit(
|
||||
createActionRequestBody(
|
||||
userWalletId,
|
||||
network,
|
||||
params,
|
||||
),
|
||||
)
|
||||
StakingActionCommonType.PENDING_REWARDS,
|
||||
StakingActionCommonType.PENDING_OTHER,
|
||||
-> stakeKitApi.estimateGasOnPending(
|
||||
is StakingActionCommonType.Pending -> stakeKitApi.estimateGasOnPending(
|
||||
createPendingActionRequestBody(params),
|
||||
)
|
||||
}
|
||||
|
|
@ -389,88 +379,66 @@ internal class DefaultStakingRepository(
|
|||
refresh: Boolean,
|
||||
) = withContext(dispatchers.io) {
|
||||
if (!stakingFeatureToggle.isStakingEnabled) return@withContext
|
||||
try {
|
||||
isYieldBalanceFetching.update {
|
||||
it + (userWalletId to true)
|
||||
}
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getYieldBalancesKey(userWalletId),
|
||||
skipCache = refresh,
|
||||
block = {
|
||||
val yields = getEnabledYields()
|
||||
val availableCurrencies = cryptoCurrencies
|
||||
.mapNotNull { currency ->
|
||||
val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network)
|
||||
val integrationId = integrationIdMap[getIntegrationKey(currency.id)]
|
||||
|
||||
if (integrationId != null && yields.any { it.id == integrationId }) {
|
||||
addresses to integrationId
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.flatMap { (addresses, integrationId) ->
|
||||
addresses.map { address -> address to integrationId }
|
||||
}
|
||||
.map { getBalanceRequestData(it.first.value, it.second) }
|
||||
.ifEmpty {
|
||||
cacheRegistry.invalidate(getYieldBalancesKey(userWalletId))
|
||||
error("No addresses found")
|
||||
}
|
||||
val result = stakeKitApi.getMultipleYieldBalances(availableCurrencies).getOrThrow()
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getYieldBalancesKey(userWalletId),
|
||||
skipCache = refresh,
|
||||
block = {
|
||||
val yields = getEnabledYields().ifEmpty {
|
||||
Timber.i("No enabled yields for $userWalletId")
|
||||
stakingBalanceStore.store(userWalletId, emptySet())
|
||||
|
||||
stakingBalanceStore.store(userWalletId, result)
|
||||
},
|
||||
)
|
||||
} finally {
|
||||
isYieldBalanceFetching.update {
|
||||
it - userWalletId
|
||||
}
|
||||
}
|
||||
return@invokeOnExpire
|
||||
}
|
||||
val availableCurrencies = cryptoCurrencies
|
||||
.mapNotNull { currency ->
|
||||
val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network)
|
||||
val integrationId = integrationIdMap[getIntegrationKey(currency.id)]
|
||||
|
||||
if (integrationId != null && yields.any { it.id == integrationId }) {
|
||||
addresses to integrationId
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.flatMap { (addresses, integrationId) ->
|
||||
addresses.map { address -> address to integrationId }
|
||||
}
|
||||
.map { getBalanceRequestData(it.first.value, it.second) }
|
||||
.ifEmpty {
|
||||
Timber.i("No yield balances available for $userWalletId")
|
||||
stakingBalanceStore.store(userWalletId, emptySet())
|
||||
|
||||
cacheRegistry.invalidate(getYieldBalancesKey(userWalletId))
|
||||
|
||||
return@invokeOnExpire
|
||||
}
|
||||
|
||||
val result = stakeKitApi
|
||||
.getMultipleYieldBalances(availableCurrencies)
|
||||
.getOrThrow()
|
||||
|
||||
stakingBalanceStore.store(userWalletId, result)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun getMultiYieldBalanceFlow(
|
||||
override fun getMultiYieldBalanceUpdates(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencies: List<CryptoCurrency>,
|
||||
): Flow<YieldBalanceList> = channelFlow {
|
||||
if (!stakingFeatureToggle.isStakingEnabled) {
|
||||
send(YieldBalanceList.Empty)
|
||||
} else {
|
||||
launch(dispatchers.io) {
|
||||
stakingBalanceStore.get(userWalletId)
|
||||
.collectLatest { send(yieldBalanceListConverter.convert(it)) }
|
||||
}
|
||||
stakingBalanceStore.get(userWalletId)
|
||||
.onEach {
|
||||
val balances = yieldBalanceListConverter.convert(it)
|
||||
send(balances)
|
||||
}
|
||||
.launchIn(scope = this + dispatchers.io)
|
||||
|
||||
withContext(dispatchers.io) {
|
||||
fetchMultiYieldBalance(
|
||||
userWalletId,
|
||||
cryptoCurrencies,
|
||||
)
|
||||
}
|
||||
}
|
||||
}.cancellable()
|
||||
|
||||
override fun getMultiYieldBalanceLce(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencies: List<CryptoCurrency>,
|
||||
): LceFlow<Throwable, YieldBalanceList> = lceFlow {
|
||||
if (!stakingFeatureToggle.isStakingEnabled) {
|
||||
send(YieldBalanceList.Empty)
|
||||
} else {
|
||||
launch(dispatchers.io) {
|
||||
combine(
|
||||
stakingBalanceStore.get(userWalletId),
|
||||
isYieldBalanceFetching.map { it.getOrElse(userWalletId) { false } },
|
||||
) { result, isFetching ->
|
||||
val balances = yieldBalanceListConverter.convert(result)
|
||||
send(balances, isStillLoading = isFetching)
|
||||
}.collect()
|
||||
}
|
||||
withContext(dispatchers.io) {
|
||||
catch(
|
||||
block = { fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false) },
|
||||
catch = { raise(it) },
|
||||
)
|
||||
fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -583,9 +551,10 @@ internal class DefaultStakingRepository(
|
|||
}
|
||||
|
||||
private fun getEnabledYields(): List<Yield> {
|
||||
return stakingYieldsStore
|
||||
.get()
|
||||
.map { yieldConverter.convert(it) }
|
||||
return yieldConverter.convertListIgnoreErrors(
|
||||
input = stakingYieldsStore.get(),
|
||||
onError = { Timber.e("Error converting enabled yields list: $it") },
|
||||
)
|
||||
}
|
||||
|
||||
private fun getBalanceRequestData(address: String, integrationId: String): YieldBalanceRequestBody {
|
||||
|
|
|
|||
|
|
@ -17,37 +17,37 @@ class YieldConverter(
|
|||
|
||||
override fun convert(value: YieldDTO): Yield {
|
||||
return Yield(
|
||||
id = value.id,
|
||||
token = tokenConverter.convert(value.token),
|
||||
tokens = value.tokens.map { tokenConverter.convert(it) },
|
||||
args = convertArgs(value.args),
|
||||
status = convertStatus(value.status),
|
||||
apy = value.apy,
|
||||
rewardRate = value.rewardRate,
|
||||
rewardType = convertRewardType(value.rewardType),
|
||||
metadata = convertMetadata(value.metadata),
|
||||
validators = value.validators
|
||||
id = value.id.asMandatory("id"),
|
||||
token = tokenConverter.convert(value.token.asMandatory("token")),
|
||||
tokens = value.tokens.asMandatory("tokens").map { tokenConverter.convert(it) },
|
||||
args = convertArgs(value.args.asMandatory("args")),
|
||||
status = convertStatus(value.status.asMandatory("status")),
|
||||
apy = value.apy.asMandatory("apy"),
|
||||
rewardRate = value.rewardRate.asMandatory("rewardRate"),
|
||||
rewardType = convertRewardType(value.rewardType.asMandatory("rewardType")),
|
||||
metadata = convertMetadata(value.metadata.asMandatory("metadata")),
|
||||
validators = value.validators.asMandatory("validators")
|
||||
.asSequence()
|
||||
.filter { it.status == ValidatorStatusDTO.ACTIVE }
|
||||
.map { convertValidator(it) }
|
||||
.sortedByDescending { it.isStrategicPartner }
|
||||
.sortedByDescending { it.apr }
|
||||
.toImmutableList(),
|
||||
isAvailable = value.isAvailable,
|
||||
isAvailable = value.isAvailable.asMandatory("isAvailable"),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertArgs(argsDTO: YieldDTO.ArgsDTO): Yield.Args {
|
||||
return Yield.Args(
|
||||
enter = convertEnter(argsDTO.enter),
|
||||
enter = convertEnter(argsDTO.enter.asMandatory("enter")),
|
||||
exit = argsDTO.exit?.let { convertEnter(it) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertEnter(enterDTO: YieldDTO.ArgsDTO.Enter): Yield.Args.Enter {
|
||||
return Yield.Args.Enter(
|
||||
addresses = convertAddresses(enterDTO.addresses),
|
||||
args = enterDTO.args
|
||||
addresses = convertAddresses(enterDTO.addresses.asMandatory("addresses")),
|
||||
args = enterDTO.args.asMandatory("args")
|
||||
.mapKeys { convertArgType(it.key) }
|
||||
.mapValues { convertAddressArgument(it.value) },
|
||||
)
|
||||
|
|
@ -55,7 +55,7 @@ class YieldConverter(
|
|||
|
||||
private fun convertAddresses(addressesDTO: YieldDTO.ArgsDTO.Enter.Addresses): Yield.Args.Enter.Addresses {
|
||||
return Yield.Args.Enter.Addresses(
|
||||
address = convertAddressArgument(addressesDTO.address),
|
||||
address = convertAddressArgument(addressesDTO.address.asMandatory("address")),
|
||||
additionalAddresses = addressesDTO.additionalAddresses
|
||||
?.mapKeys { convertArgType(it.key) }
|
||||
?.mapValues { convertAddressArgument(it.value) },
|
||||
|
|
@ -73,48 +73,51 @@ class YieldConverter(
|
|||
|
||||
private fun convertStatus(statusDTO: YieldDTO.StatusDTO): Yield.Status {
|
||||
return Yield.Status(
|
||||
enter = statusDTO.enter,
|
||||
enter = statusDTO.enter.asMandatory("enter"),
|
||||
exit = statusDTO.exit,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertMetadata(metadataDTO: YieldDTO.MetadataDTO): Yield.Metadata {
|
||||
return Yield.Metadata(
|
||||
name = metadataDTO.name,
|
||||
logoUri = metadataDTO.logoUri,
|
||||
description = metadataDTO.description,
|
||||
name = metadataDTO.name.asMandatory("name"),
|
||||
logoUri = metadataDTO.logoUri.asMandatory("logoUri"),
|
||||
description = metadataDTO.description.asMandatory("description"),
|
||||
documentation = metadataDTO.documentation,
|
||||
gasFeeToken = tokenConverter.convert(metadataDTO.gasFeeTokenDTO),
|
||||
token = tokenConverter.convert(metadataDTO.tokenDTO),
|
||||
tokens = metadataDTO.tokensDTO.map { tokenConverter.convert(it) },
|
||||
type = metadataDTO.type,
|
||||
rewardSchedule = convertRewardSchedule(metadataDTO.rewardSchedule),
|
||||
gasFeeToken = tokenConverter.convert(metadataDTO.gasFeeTokenDTO.asMandatory("gasFeeTokenDTO")),
|
||||
token = tokenConverter.convert(metadataDTO.tokenDTO.asMandatory("tokenDTO")),
|
||||
tokens = metadataDTO.tokensDTO.asMandatory("tokensDTO").map { tokenConverter.convert(it) },
|
||||
type = metadataDTO.type.asMandatory("type"),
|
||||
rewardSchedule = convertRewardSchedule(metadataDTO.rewardSchedule.asMandatory("rewardSchedule")),
|
||||
cooldownPeriod = metadataDTO.cooldownPeriod?.let { convertPeriod(it) },
|
||||
warmupPeriod = convertPeriod(metadataDTO.warmupPeriod),
|
||||
rewardClaiming = convertRewardClaiming(metadataDTO.rewardClaiming),
|
||||
warmupPeriod = convertPeriod(metadataDTO.warmupPeriod.asMandatory("warmupPeriod")),
|
||||
rewardClaiming = convertRewardClaiming(metadataDTO.rewardClaiming.asMandatory("rewardClaiming")),
|
||||
defaultValidator = metadataDTO.defaultValidator,
|
||||
minimumStake = metadataDTO.minimumStake,
|
||||
supportsMultipleValidators = metadataDTO.supportsMultipleValidators,
|
||||
revshare = convertEnabled(metadataDTO.revshare),
|
||||
fee = convertEnabled(metadataDTO.fee),
|
||||
supportsMultipleValidators = metadataDTO.supportsMultipleValidators.asMandatory(
|
||||
"supportsMultipleValidators",
|
||||
),
|
||||
revshare = convertEnabled(metadataDTO.revshare.asMandatory("revshare")),
|
||||
fee = convertEnabled(metadataDTO.fee.asMandatory("fee")),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertPeriod(periodDTO: YieldDTO.MetadataDTO.PeriodDTO): Yield.Metadata.Period {
|
||||
return Yield.Metadata.Period(
|
||||
days = periodDTO.days,
|
||||
days = periodDTO.days.asMandatory("days"),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertEnabled(enabledDTO: YieldDTO.MetadataDTO.EnabledDTO): Yield.Metadata.Enabled {
|
||||
return Yield.Metadata.Enabled(
|
||||
enabled = enabledDTO.enabled,
|
||||
enabled = enabledDTO.enabled.asMandatory("enabled"),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertValidator(validatorDTO: YieldDTO.ValidatorDTO): Yield.Validator {
|
||||
val address = validatorDTO.address.asMandatory("address")
|
||||
return Yield.Validator(
|
||||
address = validatorDTO.address,
|
||||
address = address,
|
||||
status = convertValidatorStatus(validatorDTO.status),
|
||||
name = validatorDTO.name,
|
||||
image = validatorDTO.image,
|
||||
|
|
@ -124,7 +127,7 @@ class YieldConverter(
|
|||
stakedBalance = validatorDTO.stakedBalance,
|
||||
votingPower = validatorDTO.votingPower,
|
||||
preferred = validatorDTO.preferred,
|
||||
isStrategicPartner = isStrategicPartner(validatorDTO.address),
|
||||
isStrategicPartner = isStrategicPartner(address),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue