Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-25 17:09:47 +04:00
parent 4c7016557f
commit d10f285f44
8 changed files with 33 additions and 316 deletions

View file

@ -5,7 +5,9 @@ import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.operations.*
import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.features.swap.SwapFeatureToggles
@ -351,21 +353,13 @@ internal object TokensDomainModule {
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): BaseCurrenciesStatusesOperations {
return if (tokensFeatureToggles.isBalancesCachingEnabled) {
CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
} else {
LceCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
}
return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@Provides
@ -377,20 +371,12 @@ internal object TokensDomainModule {
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): BaseCurrencyStatusOperations {
return if (tokensFeatureToggles.isBalancesCachingEnabled) {
CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
} else {
CurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
}
return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
tokensFeatureToggles = tokensFeatureToggles,
)
}
}

View file

@ -7,6 +7,6 @@ internal class DefaultTokensFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : TokensFeatureToggles {
override val isBalancesCachingEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "BALANCES_CACHING_ENABLED")
override val isNetworksLoadingRefactoringEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "NETWORKS_LOADING_REFACTORING_ENABLED")
}

View file

@ -43,10 +43,6 @@
"name": "STAKING_TON_ENABLED",
"version": "undefined"
},
{
"name": "BALANCES_CACHING_ENABLED",
"version": "5.21.0"
},
{
"name": "NFT_ENABLED",
"version": "undefined"
@ -82,5 +78,9 @@
{
"name": "WALLET_CONNECT_REDESIGN_ENABLED",
"version": "undefined"
},
{
"name": "NETWORKS_LOADING_REFACTORING_ENABLED",
"version": "undefined"
}
]

View file

@ -7,5 +7,5 @@ package com.tangem.domain.tokens
*/
interface TokensFeatureToggles {
val isBalancesCachingEnabled: Boolean
val isNetworksLoadingRefactoringEnabled: Boolean
}

View file

@ -12,6 +12,7 @@ import com.tangem.domain.core.utils.lceError
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.model.stakekit.YieldBalanceList
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error
@ -28,6 +29,7 @@ class CachedCurrenciesStatusesOperations(
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
@Suppress("UnusedPrivateMember") private val tokensFeatureToggles: TokensFeatureToggles,
) : BaseCurrenciesStatusesOperations,
BaseCurrencyStatusOperations(currenciesRepository, quotesRepository, networksRepository, stakingRepository) {
@ -258,6 +260,7 @@ class CachedCurrenciesStatusesOperations(
true
}
.distinctUntilChanged()
.onEmpty { emit(Error.EmptyNetworksStatuses.left()) }
}
private fun getNetworksStatuses(

View file

@ -1,57 +1,6 @@
package com.tangem.domain.tokens.operations
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations.Companion.RETRY_DELAY
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
class CurrenciesStatusesOperations(
currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
) : BaseCurrencyStatusOperations(currenciesRepository, quotesRepository, networksRepository, stakingRepository) {
override fun getQuotes(id: CryptoCurrency.RawID): Flow<Either<Error, Set<Quote>>> {
return quotesRepository.getQuotesUpdatesLegacy(setOf(id))
.map<Set<Quote>, Either<Error, Set<Quote>>> { quotes ->
if (quotes.isEmpty()) Error.EmptyQuotes.left() else quotes.right()
}
.retryWhen { cause, _ ->
emit(Error.DataError(cause).left())
delay(RETRY_DELAY)
true
}
}
override fun getNetworksStatuses(
userWalletId: UserWalletId,
network: Network,
): EitherFlow<Error, Set<NetworkStatus>> {
return networksRepository.getNetworkStatusesUpdatesLegacy(userWalletId, setOf(network))
.map<Set<NetworkStatus>, Either<Error, Set<NetworkStatus>>> { it.right() }
.catch { emit(Error.DataError(it).left()) }
.onEmpty { emit(Error.EmptyNetworksStatuses.left()) }
}
override suspend fun fetchComponents(
userWalletId: UserWalletId,
networks: Set<Network>,
currenciesIds: Set<CryptoCurrency.ID>,
currencies: List<CryptoCurrency>,
): Either<Throwable, Unit> = Unit.right()
class CurrenciesStatusesOperations {
sealed class Error {

View file

@ -1,224 +0,0 @@
package com.tangem.domain.tokens.operations
import arrow.core.*
import arrow.core.raise.ensureNotNull
import arrow.core.raise.recover
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.lce.lce
import com.tangem.domain.core.lce.lceFlow
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.model.stakekit.YieldBalanceList
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
class LceCurrenciesStatusesOperations(
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
) : BaseCurrenciesStatusesOperations {
override fun getCurrenciesStatuses(
userWalletId: UserWalletId,
): LceFlow<TokenListError, List<CryptoCurrencyStatus>> {
return transformToCurrenciesStatuses(
userWalletId = userWalletId,
currenciesFlow = getWalletCurrencies(userWalletId),
)
}
private fun transformToCurrenciesStatuses(
userWalletId: UserWalletId,
currenciesFlow: EitherFlow<TokenListError, List<CryptoCurrency>>,
): LceFlow<TokenListError, List<CryptoCurrencyStatus>> = lceFlow {
currenciesFlow.collectLatest { maybeCurrencies ->
val nonEmptyCurrencies = maybeCurrencies.bind().toNonEmptyListOrNull()
ensureNotNull(nonEmptyCurrencies) { TokenListError.EmptyTokens }
// This is only 'true' when the flow here is empty, such as during initial loading
if (isLoading.get()) {
val loadingCurrencies = createCurrenciesStatuses(
currencies = nonEmptyCurrencies,
maybeNetworkStatuses = null,
maybeQuotes = null,
maybeYieldBalances = null,
)
send(loadingCurrencies)
}
val (networks, currenciesIds) = getIds(nonEmptyCurrencies)
fun createCurrenciesStatuses(
maybeQuotes: Either<TokenListError, Set<Quote>>?,
maybeNetworkStatuses: Either<TokenListError, Set<NetworkStatus>>?,
maybeYieldBalances: Either<TokenListError, YieldBalanceList>?,
): Lce<TokenListError, List<CryptoCurrencyStatus>> = createCurrenciesStatuses(
currencies = nonEmptyCurrencies,
maybeQuotes = maybeQuotes,
maybeNetworkStatuses = maybeNetworkStatuses,
maybeYieldBalances = maybeYieldBalances,
)
combine(
getQuotes(currenciesIds),
getNetworksStatuses(userWalletId, networks),
getYieldBalances(userWalletId, nonEmptyCurrencies),
::createCurrenciesStatuses,
)
.distinctUntilChanged()
.collectLatest { maybeCurrenciesStatuses ->
send(maybeCurrenciesStatuses)
}
}
}
private fun getWalletCurrencies(userWalletId: UserWalletId): EitherFlow<TokenListError, List<CryptoCurrency>> {
return currenciesRepository.getWalletCurrenciesUpdates(userWalletId)
.map<List<CryptoCurrency>, Either<TokenListError, List<CryptoCurrency>>> { it.right() }
.catch { emit(TokenListError.DataError(it).left()) }
.distinctUntilChanged()
}
private fun createCurrenciesStatuses(
currencies: NonEmptyList<CryptoCurrency>,
maybeQuotes: Either<TokenListError, Set<Quote>>?,
maybeNetworkStatuses: Either<TokenListError, Set<NetworkStatus>>?,
maybeYieldBalances: Either<TokenListError, YieldBalanceList>?,
): Lce<TokenListError, List<CryptoCurrencyStatus>> = lce {
isLoading.set(maybeNetworkStatuses == null || maybeYieldBalances == null)
var quotesRetrievingFailed = false
val networksStatuses = maybeNetworkStatuses?.bindEither()?.toNonEmptySetOrNull()
val yieldBalances = maybeYieldBalances?.bindEither()
val quotes = recover({ maybeQuotes?.bind()?.toNonEmptySetOrNull() }) {
null
}
if (quotes == null) {
quotesRetrievingFailed = true
}
currencies.map { currency ->
val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }
val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network }
val yieldBalance = findYieldBalanceOrNull(yieldBalances, currency, networkStatus)
val currencyStatus = createCurrencyStatus(
currency = currency,
quote = quote,
networkStatus = networkStatus,
yieldBalance = yieldBalance,
ignoreQuote = quotesRetrievingFailed,
)
if (currencyStatus.value is CryptoCurrencyStatus.Loading) {
isLoading.set(true)
}
currencyStatus
}
}
private fun findYieldBalanceOrNull(
yieldBalances: YieldBalanceList?,
currency: CryptoCurrency,
networkStatus: NetworkStatus?,
): YieldBalance? {
if (yieldBalances !is YieldBalanceList.Data) return null
val supportedIntegration = stakingRepository.getSupportedIntegrationId(currency.id)
if (supportedIntegration.isNullOrBlank()) return null
return yieldBalances.getBalance(
address = extractAddress(networkStatus),
integrationId = supportedIntegration,
)
}
private fun createCurrencyStatus(
currency: CryptoCurrency,
quote: Quote?,
networkStatus: NetworkStatus?,
yieldBalance: YieldBalance?,
ignoreQuote: Boolean,
): CryptoCurrencyStatus {
val currencyStatusOperations = CurrencyStatusOperations(
currency = currency,
quote = quote,
networkStatus = networkStatus,
yieldBalance = yieldBalance,
ignoreQuote = ignoreQuote,
)
return currencyStatusOperations.createTokenStatus()
}
private fun getQuotes(tokensIds: NonEmptySet<CryptoCurrency.ID>): Flow<Either<TokenListError, Set<Quote>>> {
return quotesRepository.getQuotesUpdatesLegacy(tokensIds.mapNotNull { it.rawCurrencyId }.toSet())
.map<Set<Quote>, Either<TokenListError, Set<Quote>>> { it.right() }
.retryWhen { cause, _ ->
emit(TokenListError.DataError(cause).left())
// adding delay before retry to avoid spam when flow restarted
delay(RETRY_QUOTES_DELAY)
true
}
.distinctUntilChanged()
}
private fun getNetworksStatuses(
userWalletId: UserWalletId,
networks: NonEmptySet<Network>,
): EitherFlow<TokenListError, Set<NetworkStatus>> {
return networksRepository.getNetworkStatusesUpdatesLegacy(userWalletId, networks)
.map<Set<NetworkStatus>, Either<TokenListError, Set<NetworkStatus>>> { it.right() }
.catch { emit(TokenListError.DataError(it).left()) }
.distinctUntilChanged()
}
private fun getYieldBalances(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
): EitherFlow<TokenListError, YieldBalanceList> {
return stakingRepository.getMultiYieldBalanceUpdatesLegacy(userWalletId, cryptoCurrencies)
.map<YieldBalanceList, Either<TokenListError, YieldBalanceList>> { it.right() }
.catch { emit(TokenListError.DataError(it).left()) }
.distinctUntilChanged()
}
private fun getIds(currencies: List<CryptoCurrency>): Pair<NonEmptySet<Network>, NonEmptySet<CryptoCurrency.ID>> {
val currencyIdToNetworkId = currencies.associate { currency ->
currency.id to currency.network
}
val currenciesIds = currencyIdToNetworkId.keys.toNonEmptySetOrNull()
val networks = currencyIdToNetworkId.values.toNonEmptySetOrNull()
requireNotNull(currenciesIds) { "Currencies IDs cannot be empty" }
requireNotNull(networks) { "Networks IDs cannot be empty" }
return networks to currenciesIds
}
private fun extractAddress(networkStatus: NetworkStatus?): String? {
return when (val value = networkStatus?.value) {
is NetworkStatus.NoAccount -> value.address.defaultAddress.value
is NetworkStatus.Unreachable -> value.address?.defaultAddress?.value
is NetworkStatus.Verified -> value.address.defaultAddress.value
else -> null
}
}
private companion object {
const val RETRY_QUOTES_DELAY = 2000L
}
}

View file

@ -10,7 +10,7 @@ import com.tangem.domain.tokens.mock.MockQuotes
import com.tangem.domain.tokens.mock.MockTokens
import com.tangem.domain.tokens.mock.MockTokensStates
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.MockCurrenciesRepository
import com.tangem.domain.tokens.repository.MockNetworksRepository
import com.tangem.domain.tokens.repository.MockQuotesRepository
@ -164,7 +164,7 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest {
quotes: Flow<Either<DataError, Set<Quote>>> = flowOf(MockQuotes.quotes.right()),
statuses: Flow<Either<DataError, Set<NetworkStatus>>> = flowOf(MockNetworks.verifiedNetworksStatuses.right()),
) = GetPrimaryCurrencyStatusUpdatesUseCase(
currencyStatusOperations = CurrenciesStatusesOperations(
currencyStatusOperations = CachedCurrenciesStatusesOperations(
currenciesRepository = MockCurrenciesRepository(
sortTokensResult = Unit.right(),
removeCurrencyResult = removeCurrencyResult,
@ -176,6 +176,9 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest {
quotesRepository = MockQuotesRepository(quotes),
networksRepository = MockNetworksRepository(statuses),
stakingRepository = MockStakingRepository(),
tokensFeatureToggles = object : TokensFeatureToggles {
override val isNetworksLoadingRefactoringEnabled: Boolean = false
},
),
dispatchers = dispatchers,
)