Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-06 15:24:38 +03:00
commit 1266ffce6a
10 changed files with 291 additions and 216 deletions

View file

@ -204,11 +204,8 @@ internal class DefaultStakingBalanceStore(
private fun Set<YieldBalance>.getBalance(address: String?, integrationId: String?): YieldBalance? {
return firstOrNull { yieldBalance ->
val data = yieldBalance as? YieldBalance.Data
val balance = data?.balance
val isCorrectAddress = address != null && address == data?.address
val isCorrectIntegration = integrationId != null && balance?.integrationId == integrationId
val isCorrectAddress = address != null && address == yieldBalance.address
val isCorrectIntegration = integrationId != null && yieldBalance.integrationId == integrationId
isCorrectIntegration && isCorrectAddress
}

View file

@ -61,11 +61,8 @@ import com.tangem.lib.crypto.BlockchainUtils.isCardano
import com.tangem.lib.crypto.BlockchainUtils.isSolana
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
@ -188,32 +185,48 @@ internal class DefaultStakingRepository(
}
}
override suspend fun getStakingAvailability(
override fun getStakingAvailability(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): StakingAvailability {
if (!checkFeatureToggleEnabled(cryptoCurrency.network.id)) return StakingAvailability.Unavailable
if (checkForInvalidCardBatch(userWalletId, cryptoCurrency)) return StakingAvailability.Unavailable
val rawCurrencyId = cryptoCurrency.id.rawCurrencyId ?: return StakingAvailability.Unavailable
val isSupportedInMobileApp = getSupportedIntegrationId(cryptoCurrency.id).isNullOrEmpty().not()
val prefetchedYield = findPrefetchedYield(
yields = getEnabledYieldsSync(),
currencyId = rawCurrencyId,
symbol = cryptoCurrency.symbol,
)
return when {
prefetchedYield != null && isSupportedInMobileApp -> {
StakingAvailability.Available(prefetchedYield.id)
): Flow<StakingAvailability> {
return channelFlow {
if (!checkFeatureToggleEnabled(cryptoCurrency.network.id)) {
send(StakingAvailability.Unavailable)
return@channelFlow
}
prefetchedYield == null && isSupportedInMobileApp -> {
StakingAvailability.TemporaryUnavailable
if (checkForInvalidCardBatch(userWalletId, cryptoCurrency)) {
send(StakingAvailability.Unavailable)
return@channelFlow
}
else -> StakingAvailability.Unavailable
val rawCurrencyId = cryptoCurrency.id.rawCurrencyId
if (rawCurrencyId == null) {
send(StakingAvailability.Unavailable)
return@channelFlow
}
val isSupportedInMobileApp = getSupportedIntegrationId(cryptoCurrency.id).isNullOrEmpty().not()
getEnabledYields()
.distinctUntilChanged()
.onEach { yields ->
val prefetchedYield = findPrefetchedYield(
yields = yields,
currencyId = rawCurrencyId,
symbol = cryptoCurrency.symbol,
)
when {
prefetchedYield != null && isSupportedInMobileApp -> {
send(StakingAvailability.Available(prefetchedYield.id))
}
prefetchedYield == null && isSupportedInMobileApp -> {
send(StakingAvailability.TemporaryUnavailable)
}
else -> send(StakingAvailability.Unavailable)
}
}
.launchIn(this)
}
}
@ -378,6 +391,7 @@ internal class DefaultStakingRepository(
} else {
FirebaseCrashlytics.getInstance()
.log("No yield balance available for currency ${cryptoCurrency.id.value}")
send(YieldBalance.Error(integrationId, address))
}
}
}
@ -633,6 +647,15 @@ internal class DefaultStakingRepository(
)
}
private fun getEnabledYields(): Flow<List<Yield>> {
return stakingYieldsStore.get().map {
YieldConverter.convertListIgnoreErrors(
input = it,
onError = { Timber.e("Error converting one of the items in enabled yields: $it") },
)
}
}
private fun getBalanceRequestData(address: String, integrationId: String): YieldBalanceRequestBody {
return YieldBalanceRequestBody(
addresses = Address(

View file

@ -1,12 +1,17 @@
package com.tangem.domain.staking
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
/**
* Use case for getting info about staking capability in tangem app.
@ -16,17 +21,15 @@ class GetStakingAvailabilityUseCase(
private val stakingErrorResolver: StakingErrorResolver,
) {
suspend operator fun invoke(
operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Either<StakingError, StakingAvailability> {
return Either
.catch {
stakingRepository.getStakingAvailability(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
)
}
.mapLeft { stakingErrorResolver.resolve(it) }
): EitherFlow<StakingError, StakingAvailability> {
return stakingRepository.getStakingAvailability(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
).map<StakingAvailability, Either<StakingError, StakingAvailability>> {
it.right()
}.catch { emit(stakingErrorResolver.resolve(it).left()) }
}
}

View file

@ -35,7 +35,7 @@ interface StakingRepository {
suspend fun getYield(yieldId: String): Yield
suspend fun getStakingAvailability(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): StakingAvailability
fun getStakingAvailability(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Flow<StakingAvailability>
suspend fun getActions(
userWalletId: UserWalletId,

View file

@ -60,13 +60,18 @@ class GetCryptoCurrencyActionsUseCase(
val flow = combine(
flow = networkFlow,
flow2 = promoRepository.getStoryById(StoryContentIds.STORY_FIRST_TIME_SWAP.id).conflate(),
) { maybeCoinStatus, maybeSwapStories ->
flow3 = stakingRepository.getStakingAvailability(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
).onStart { emit(StakingAvailability.Unavailable) },
) { maybeCoinStatus, maybeSwapStories, stakingAvailability ->
createTokenActionsState(
userWallet = userWallet,
coinStatus = maybeCoinStatus.getOrNull(),
cryptoCurrencyStatus = cryptoCurrencyStatus,
requirements = requirements,
shouldShowSwapStories = maybeSwapStories != null,
isStakingAvailable = stakingAvailability is StakingAvailability.Available,
)
}
@ -80,6 +85,7 @@ class GetCryptoCurrencyActionsUseCase(
cryptoCurrencyStatus: CryptoCurrencyStatus,
requirements: AssetRequirementsCondition?,
shouldShowSwapStories: Boolean,
isStakingAvailable: Boolean,
): TokenActionsState {
return TokenActionsState(
walletId = userWallet.walletId,
@ -90,6 +96,7 @@ class GetCryptoCurrencyActionsUseCase(
cryptoCurrencyStatus = cryptoCurrencyStatus,
requirements = requirements,
shouldShowSwapStories = shouldShowSwapStories,
isStakingAvailable = isStakingAvailable,
),
)
}
@ -105,6 +112,7 @@ class GetCryptoCurrencyActionsUseCase(
cryptoCurrencyStatus: CryptoCurrencyStatus,
requirements: AssetRequirementsCondition?,
shouldShowSwapStories: Boolean,
isStakingAvailable: Boolean,
): List<TokenActionsState.ActionState> {
val cryptoCurrency = cryptoCurrencyStatus.currency
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.MissedDerivation) {
@ -139,7 +147,7 @@ class GetCryptoCurrencyActionsUseCase(
}
// staking
if (isStakingAvailable(userWallet, cryptoCurrency)) {
if (isStakingAvailable) {
val yield = kotlin.runCatching {
stakingRepository.getYield(
cryptoCurrencyId = cryptoCurrency.id,
@ -354,13 +362,6 @@ class GetCryptoCurrencyActionsUseCase(
return networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty()
}
private suspend fun isStakingAvailable(userWallet: UserWallet, cryptoCurrency: CryptoCurrency): Boolean {
return stakingRepository.getStakingAvailability(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrency,
) is StakingAvailability.Available
}
private companion object {
const val REQUEST_EXCHANGE_DATA_TIMEOUT = 1000L
}

View file

@ -43,10 +43,10 @@ class MockStakingRepository : StakingRepository {
override suspend fun getYield(yieldId: String) = yield
override suspend fun getStakingAvailability(
override fun getStakingAvailability(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): StakingAvailability = StakingAvailability.Unavailable
): Flow<StakingAvailability> = flowOf(StakingAvailability.Unavailable)
override suspend fun getActions(
userWalletId: UserWalletId,

View file

@ -42,7 +42,6 @@ import com.tangem.domain.staking.GetStakingEntryInfoUseCase
import com.tangem.domain.staking.GetStakingIntegrationIdUseCase
import com.tangem.domain.staking.GetYieldUseCase
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrency
@ -144,18 +143,16 @@ internal class TokenDetailsModel @Inject constructor(
private val refreshStateJobHolder = JobHolder()
private val warningsJobHolder = JobHolder()
private val expressTxJobHolder = JobHolder()
private val buttonsJobHolder = JobHolder()
private val stakingJobHolder = JobHolder()
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null
private var stakingEntryInfo: StakingEntryInfo? = null
private var stakingAvailability: StakingAvailability = StakingAvailability.Unavailable
private var expressTxStatusTaskScheduler = SingleTaskScheduler<PersistentList<ExpressTransactionStateUM>>()
private val stateFactory = TokenDetailsStateFactory(
currentStateProvider = Provider { uiState.value },
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
stakingEntryInfoProvider = Provider { stakingEntryInfo },
stakingAvailabilityProvider = Provider { stakingAvailability },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
clickIntents = this,
networkHasDerivationUseCase = networkHasDerivationUseCase,
@ -245,8 +242,6 @@ internal class TokenDetailsModel @Inject constructor(
subscribeOnCurrencyStatusUpdates()
subscribeOnExpressTransactionsUpdates()
updateTxHistory(refresh = false, showItemsLoading = true, initialUpdating = true)
updateStakingInfo()
}
private fun handleBalanceHiding() {
@ -271,6 +266,7 @@ internal class TokenDetailsModel @Inject constructor(
}
.flowOn(dispatchers.main)
.launchIn(modelScope)
.saveIn(buttonsJobHolder)
}
private fun updateWarnings(cryptoCurrencyStatus: CryptoCurrencyStatus) {
@ -308,6 +304,7 @@ internal class TokenDetailsModel @Inject constructor(
updateWarnings(status)
}
currencyStatusAnalyticsSender.send(maybeCurrencyStatus)
subscribeOnUpdateStakingInfo()
}
.flowOn(dispatchers.main)
.launchIn(modelScope)
@ -397,24 +394,29 @@ internal class TokenDetailsModel @Inject constructor(
}
}
private fun updateStakingInfo() {
modelScope.launch {
val availability = getStakingAvailabilityUseCase(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
).getOrElse { StakingAvailability.Unavailable }
private fun subscribeOnUpdateStakingInfo() {
getStakingAvailabilityUseCase(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
)
.map { it.getOrElse { StakingAvailability.Unavailable } }
.distinctUntilChanged()
.onEach {
if (it is StakingAvailability.Available) {
val stakingInfo = getStakingEntryInfoUseCase(
cryptoCurrencyId = cryptoCurrency.id,
symbol = cryptoCurrency.symbol,
)
stakingAvailability = availability
if (stakingAvailability is StakingAvailability.Available) {
val stakingInfo = getStakingEntryInfoUseCase(
cryptoCurrencyId = cryptoCurrency.id,
symbol = cryptoCurrency.symbol,
)
stakingEntryInfo = stakingInfo.getOrNull()
val stakingEntryInfo = stakingInfo.getOrNull()
internalUiState.update { state ->
stateFactory.getStakingInfoState(state, stakingEntryInfo, it)
}
}
}
}
.flowOn(dispatchers.main)
.launchIn(modelScope)
.saveIn(stakingJobHolder)
}
private fun updateTopBarMenu() {

View file

@ -6,16 +6,9 @@ import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.*
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.StatusSource
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.stakekit.RewardBlockType
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -24,10 +17,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryTransactionStateConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance
import com.tangem.features.tokendetails.impl.R
import com.tangem.lib.crypto.BlockchainUtils.isBSC
import com.tangem.lib.crypto.BlockchainUtils.isIncludeStakingTotalBalance
import com.tangem.lib.crypto.BlockchainUtils.isSolana
import com.tangem.utils.Provider
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.utils.converter.Converter
@ -40,8 +30,6 @@ import java.math.BigDecimal
internal class TokenDetailsLoadedBalanceConverter(
private val currentStateProvider: Provider<TokenDetailsState>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val stakingEntryInfoProvider: Provider<StakingEntryInfo?>,
private val stakingAvailabilityProvider: Provider<StakingAvailability>,
private val symbol: String,
private val decimals: Int,
private val clickIntents: TokenDetailsClickIntents,
@ -81,7 +69,7 @@ internal class TokenDetailsLoadedBalanceConverter(
currentState = state.tokenBalanceBlockState,
status = status,
),
stakingBlocksState = getYieldBalance(status, state),
stakingBlocksState = state.stakingBlocksState,
marketPriceBlockState = getMarketPriceState(status = status.value, currencySymbol = currencyName),
pendingTxs = pendingTxs,
txHistoryState = if (state.txHistoryState is TxHistoryState.NotSupported) {
@ -144,58 +132,6 @@ internal class TokenDetailsLoadedBalanceConverter(
}
}
private fun getYieldBalance(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlockUM? {
return when (stakingAvailabilityProvider.invoke()) {
StakingAvailability.TemporaryUnavailable -> StakingBlockUM.TemporaryUnavailable
StakingAvailability.Unavailable -> null
is StakingAvailability.Available -> getStakingInfoBlock(status, state)
}
}
private fun getStakingInfoBlock(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlockUM? {
val yieldBalance = status.value.yieldBalance as? YieldBalance.Data
val stakingCryptoAmount = yieldBalance?.getTotalStakingBalance()
val pendingBalances = yieldBalance?.balance?.items ?: emptyList()
val stakingEntryInfo = stakingEntryInfoProvider.invoke()
val iconState = state.tokenInfoBlockState.iconState
return when {
stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null -> {
if (pendingBalances.isEmpty()) {
getStakeAvailableState(stakingEntryInfo, iconState, isStakingButtonEnabled(status))
} else {
getStakedBlockWithFiatAmount(status, pendingBalances.sumOf { it.amount }, null)
}
}
stakingCryptoAmount.isNullOrZero() && stakingEntryInfo == null -> {
null
}
else -> getStakedBlockWithFiatAmount(status, stakingCryptoAmount, yieldBalance?.getRewardStakingBalance())
}
}
private fun isStakingButtonEnabled(status: CryptoCurrencyStatus): Boolean {
return status.value is CryptoCurrencyStatus.Loaded ||
status.value is CryptoCurrencyStatus.NoQuote ||
status.value is CryptoCurrencyStatus.Custom
}
private fun getStakedBlockWithFiatAmount(
status: CryptoCurrencyStatus,
stakingAmount: BigDecimal?,
rewardAmount: BigDecimal?,
): StakingBlockUM.Staked {
val fiatRate = status.value.fiatRate
return getStakedState(
status = status,
stakingCryptoAmount = stakingAmount,
stakingFiatAmount = stakingAmount?.let { fiatRate?.multiply(it) },
stakingRewardAmount = rewardAmount?.let { fiatRate?.multiply(it) },
)
}
private fun getMarketPriceState(
status: CryptoCurrencyStatus.Value,
currencySymbol: String,
@ -219,52 +155,6 @@ internal class TokenDetailsLoadedBalanceConverter(
}
}
private fun getStakeAvailableState(
stakingEntryInfo: StakingEntryInfo,
iconState: IconState,
isEnabled: Boolean,
): StakingBlockUM.StakeAvailable {
val apr = stakingEntryInfo.apr.format { percent() }
return StakingBlockUM.StakeAvailable(
titleText = resourceReference(
id = R.string.token_details_staking_block_title,
formatArgs = wrappedList(apr),
),
subtitleText = resourceReference(
id = R.string.staking_notification_earn_rewards_text,
formatArgs = wrappedList(stakingEntryInfo.tokenSymbol),
),
iconState = iconState,
isEnabled = isEnabled,
onStakeClicked = clickIntents::onStakeBannerClick,
)
}
private fun getStakedState(
status: CryptoCurrencyStatus,
stakingCryptoAmount: BigDecimal?,
stakingFiatAmount: BigDecimal?,
stakingRewardAmount: BigDecimal?,
): StakingBlockUM.Staked {
return StakingBlockUM.Staked(
cryptoAmount = stakingCryptoAmount,
fiatAmount = stakingFiatAmount,
cryptoValue = stringReference(
stakingCryptoAmount.format { crypto(symbol = symbol, decimals = decimals) },
),
fiatValue = stringReference(
stakingFiatAmount.format {
fiat(
appCurrencyProvider().code,
appCurrencyProvider().symbol,
)
},
),
rewardValue = getRewardText(status, stakingRewardAmount),
onStakeClicked = clickIntents::onStakeBannerClick,
)
}
private fun CryptoCurrencyStatus.Value.toContentConfig(currencySymbol: String): MarketPriceBlockState.Content {
return MarketPriceBlockState.Content(
currencySymbol = currencySymbol,
@ -327,31 +217,6 @@ internal class TokenDetailsLoadedBalanceConverter(
return totalAmount.format { crypto(status.currency) }
}
private fun getRewardText(status: CryptoCurrencyStatus, stakingRewardAmount: BigDecimal?): TextReference {
val blockchainId = status.currency.network.id.value
val rewardBlockType = when {
isSolana(blockchainId) || isBSC(blockchainId) -> RewardBlockType.RewardUnavailable
stakingRewardAmount.isNullOrZero() -> RewardBlockType.NoRewards
else -> RewardBlockType.Rewards
}
return when (rewardBlockType) {
RewardBlockType.Rewards -> resourceReference(
R.string.staking_details_rewards_to_claim,
wrappedList(
stakingRewardAmount.format {
fiat(
appCurrencyProvider().code,
appCurrencyProvider().symbol,
)
},
),
)
RewardBlockType.NoRewards -> resourceReference(R.string.staking_details_no_rewards_to_claim)
RewardBlockType.RewardUnavailable -> TextReference.EMPTY
}
}
private fun CryptoCurrencyStatus.Value.isFlickering(): Boolean = getStatusSource() == StatusSource.CACHE
private fun CryptoCurrencyStatus.Value.getStatusSource(): StatusSource? {

View file

@ -0,0 +1,174 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.stakekit.RewardBlockType
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.features.tokendetails.impl.R
import com.tangem.lib.crypto.BlockchainUtils.isBSC
import com.tangem.lib.crypto.BlockchainUtils.isSolana
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.isNullOrZero
import java.math.BigDecimal
internal class TokenDetailsStakingInfoConverter(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val clickIntents: TokenDetailsClickIntents,
private val currentState: TokenDetailsState,
private val stakingEntryInfo: StakingEntryInfo?,
) : Converter<StakingAvailability, TokenDetailsState> {
override fun convert(value: StakingAvailability): TokenDetailsState {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider.invoke() ?: return currentState
return currentState.copy(
stakingBlocksState = getYieldBalance(cryptoCurrencyStatus, currentState, value),
)
}
private fun getYieldBalance(
status: CryptoCurrencyStatus,
state: TokenDetailsState,
stakingAvailability: StakingAvailability,
): StakingBlockUM? {
return when (stakingAvailability) {
StakingAvailability.TemporaryUnavailable -> StakingBlockUM.TemporaryUnavailable
StakingAvailability.Unavailable -> null
is StakingAvailability.Available -> getStakingInfoBlock(status, state)
}
}
private fun getStakingInfoBlock(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlockUM? {
val yieldBalance = status.value.yieldBalance as? YieldBalance.Data
val stakingCryptoAmount = yieldBalance?.getTotalStakingBalance()
val pendingBalances = yieldBalance?.balance?.items ?: emptyList()
val iconState = state.tokenInfoBlockState.iconState
return when {
stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null -> {
if (pendingBalances.isEmpty()) {
getStakeAvailableState(stakingEntryInfo, iconState, isStakingButtonEnabled(status))
} else {
getStakedBlockWithFiatAmount(status, pendingBalances.sumOf { it.amount }, null)
}
}
stakingCryptoAmount.isNullOrZero() && stakingEntryInfo == null -> {
null
}
else -> getStakedBlockWithFiatAmount(status, stakingCryptoAmount, yieldBalance?.getRewardStakingBalance())
}
}
private fun isStakingButtonEnabled(status: CryptoCurrencyStatus): Boolean {
return status.value is CryptoCurrencyStatus.Loaded ||
status.value is CryptoCurrencyStatus.NoQuote ||
status.value is CryptoCurrencyStatus.Custom
}
private fun getStakedBlockWithFiatAmount(
status: CryptoCurrencyStatus,
stakingAmount: BigDecimal?,
rewardAmount: BigDecimal?,
): StakingBlockUM.Staked {
val fiatRate = status.value.fiatRate
return getStakedState(
status = status,
stakingCryptoAmount = stakingAmount,
stakingFiatAmount = stakingAmount?.let { fiatRate?.multiply(it) },
stakingRewardAmount = rewardAmount?.let { fiatRate?.multiply(it) },
)
}
private fun getStakeAvailableState(
stakingEntryInfo: StakingEntryInfo,
iconState: IconState,
isEnabled: Boolean,
): StakingBlockUM.StakeAvailable {
val apr = stakingEntryInfo.apr.format { percent() }
return StakingBlockUM.StakeAvailable(
titleText = resourceReference(
id = R.string.token_details_staking_block_title,
formatArgs = wrappedList(apr),
),
subtitleText = resourceReference(
id = R.string.staking_notification_earn_rewards_text,
formatArgs = wrappedList(stakingEntryInfo.tokenSymbol),
),
iconState = iconState,
isEnabled = isEnabled,
onStakeClicked = clickIntents::onStakeBannerClick,
)
}
private fun getStakedState(
status: CryptoCurrencyStatus,
stakingCryptoAmount: BigDecimal?,
stakingFiatAmount: BigDecimal?,
stakingRewardAmount: BigDecimal?,
): StakingBlockUM.Staked {
return StakingBlockUM.Staked(
cryptoAmount = stakingCryptoAmount,
fiatAmount = stakingFiatAmount,
cryptoValue = stringReference(
stakingCryptoAmount.format {
crypto(
symbol = status.currency.symbol,
decimals = status.currency.decimals,
)
},
),
fiatValue = stringReference(
stakingFiatAmount.format {
fiat(
appCurrencyProvider().code,
appCurrencyProvider().symbol,
)
},
),
rewardValue = getRewardText(status, stakingRewardAmount),
onStakeClicked = clickIntents::onStakeBannerClick,
)
}
private fun getRewardText(status: CryptoCurrencyStatus, stakingRewardAmount: BigDecimal?): TextReference {
val blockchainId = status.currency.network.id.value
val rewardBlockType = when {
isSolana(blockchainId) || isBSC(blockchainId) -> RewardBlockType.RewardUnavailable
stakingRewardAmount.isNullOrZero() -> RewardBlockType.NoRewards
else -> RewardBlockType.Rewards
}
return when (rewardBlockType) {
RewardBlockType.Rewards -> resourceReference(
R.string.staking_details_rewards_to_claim,
wrappedList(
stakingRewardAmount.format {
fiat(
appCurrencyProvider().code,
appCurrencyProvider().symbol,
)
},
),
)
RewardBlockType.NoRewards -> resourceReference(R.string.staking_details_no_rewards_to_claim)
RewardBlockType.RewardUnavailable -> TextReference.EMPTY
}
}
}

View file

@ -44,8 +44,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
internal class TokenDetailsStateFactory(
private val currentStateProvider: Provider<TokenDetailsState>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val stakingEntryInfoProvider: Provider<StakingEntryInfo?>,
private val stakingAvailabilityProvider: Provider<StakingAvailability>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val clickIntents: TokenDetailsClickIntents,
private val networkHasDerivationUseCase: NetworkHasDerivationUseCase,
@ -74,8 +72,6 @@ internal class TokenDetailsStateFactory(
TokenDetailsLoadedBalanceConverter(
currentStateProvider = currentStateProvider,
appCurrencyProvider = appCurrencyProvider,
stakingEntryInfoProvider = stakingEntryInfoProvider,
stakingAvailabilityProvider = stakingAvailabilityProvider,
symbol = symbol,
decimals = decimals,
clickIntents = clickIntents,
@ -129,6 +125,20 @@ internal class TokenDetailsStateFactory(
return tokenDetailsLoadedBalanceConverter.convert(cryptoCurrencyEither)
}
fun getStakingInfoState(
state: TokenDetailsState,
stakingEntryInfo: StakingEntryInfo?,
stakingAvailability: StakingAvailability,
): TokenDetailsState {
return TokenDetailsStakingInfoConverter(
currentState = state,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
clickIntents = clickIntents,
appCurrencyProvider = appCurrencyProvider,
stakingEntryInfo = stakingEntryInfo,
).convert(stakingAvailability)
}
fun getManageButtonsState(actions: List<TokenActionsState.ActionState>): TokenDetailsState {
return tokenDetailsButtonsConverter.convert(actions)
}