diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt index 65e29bca2b..497c3b7399 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt @@ -28,10 +28,10 @@ typealias LceFlow = Flow> * @property producerScope The [ProducerScope] instance that this class wraps. * @property ifLoading The function to call if a loading state is raised. */ -class LceFlowScope @PublishedApi internal constructor( +class LceFlowRaise @PublishedApi internal constructor( private val raise: LceRaise, private val producerScope: ProducerScope>, - private val ifLoading: suspend LceFlowScope.(C?) -> Unit, + private val ifLoading: suspend LceFlowRaise.(C?) -> Unit, ) : Raise, CoroutineScope by producerScope { val isLoading: AtomicBoolean = AtomicBoolean(value = true) @@ -92,25 +92,25 @@ class LceFlowScope @PublishedApi internal constructor( } /** - * Creates a [LceFlow] by executing the given [block] within a [LceFlowScope] context. + * Creates a [LceFlow] by executing the given [block] within a [LceFlowRaise] context. * * Flow starts with a [Lce.Loading] state. * * @param ifLoading The function to call if received a loading content. * By default, it creates a new [Lce.Loading] state with the value returned by the [block]. - * @param block The block to execute within a [LceFlowScope] context. + * @param block The block to execute within a [LceFlowRaise] context. * @return A [LceFlow] representing the result of the [block]. */ @OptIn(ExperimentalTypeInference::class) fun lceFlow( - ifLoading: suspend LceFlowScope.(C?) -> Unit = { send(lceLoading(partialContent = it)) }, - @BuilderInference block: suspend LceFlowScope.() -> Unit, + ifLoading: suspend LceFlowRaise.(C?) -> Unit = { send(lceLoading(partialContent = it)) }, + @BuilderInference block: suspend LceFlowRaise.() -> Unit, ): LceFlow { return channelFlow { trySend(lceLoading()) lce { - val scope = LceFlowScope( + val scope = LceFlowRaise( raise = this@lce, producerScope = this@channelFlow, ifLoading = ifLoading, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt index 9cd14a845c..3de638379d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt @@ -9,6 +9,7 @@ import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.operations.CurrenciesStatusesCachedOperations import com.tangem.domain.tokens.operations.CurrenciesStatusesLceOperations import com.tangem.domain.tokens.operations.TokenListOperations import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -28,15 +29,24 @@ class GetTokenListUseCase( ) { @OptIn(ExperimentalCoroutinesApi::class) - fun launch(userWalletId: UserWalletId): LceFlow { - val operations = CurrenciesStatusesLceOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ) + fun launch(userWalletId: UserWalletId, usePersistenceCache: Boolean = false): LceFlow { + val statusesFlow = if (usePersistenceCache) { + CurrenciesStatusesCachedOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + stakingRepository = stakingRepository, + ).getCurrenciesStatuses(userWalletId) + } else { + CurrenciesStatusesLceOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + stakingRepository = stakingRepository, + ).getCurrenciesStatuses(userWalletId) + } - return operations.getCurrenciesStatuses(userWalletId).transformLatest { maybeCurrencies -> + return statusesFlow.transformLatest { maybeCurrencies -> maybeCurrencies.fold( ifLoading = { maybeContent -> if (maybeContent != null) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesCachedOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesCachedOperations.kt new file mode 100644 index 0000000000..70fff8b1da --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesCachedOperations.kt @@ -0,0 +1,255 @@ +package com.tangem.domain.tokens.operations + +import arrow.core.* +import arrow.core.raise.Raise +import arrow.core.raise.catch +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.* +import kotlinx.coroutines.flow.* + +internal class CurrenciesStatusesCachedOperations( + private val currenciesRepository: CurrenciesRepository, + private val quotesRepository: QuotesRepository, + private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, +) { + + fun getCurrenciesStatuses(userWalletId: UserWalletId): LceFlow> { + return transformToCurrenciesStatuses( + userWalletId = userWalletId, + currenciesFlow = getCurrencies(userWalletId), + ) + } + + private fun transformToCurrenciesStatuses( + userWalletId: UserWalletId, + currenciesFlow: EitherFlow>, + ): LceFlow> = lceFlow { + currenciesFlow.collectLatest { maybeCurrencies -> + val isUpdating = MutableStateFlow(value = true) + val nonEmptyCurrencies = maybeCurrencies.bind().toNonEmptyListOrNull() + + ensureNotNull(nonEmptyCurrencies) { TokenListError.EmptyTokens } + + val (networks, currenciesIds) = getIds(nonEmptyCurrencies) + + fun createCurrenciesStatuses( + maybeQuotes: Either>?, + maybeNetworkStatuses: Either>?, + maybeYieldBalances: Either?, + isUpdating: Boolean, + ) = createCurrenciesStatuses( + currencies = nonEmptyCurrencies, + maybeQuotes = maybeQuotes, + maybeNetworkStatuses = maybeNetworkStatuses, + maybeYieldBalances = maybeYieldBalances, + isUpdating = isUpdating, + ) + + combine( + flow = getQuotes(currenciesIds), + flow2 = getNetworksStatuses(userWalletId, networks), + flow3 = getYieldBalances(userWalletId, nonEmptyCurrencies), + flow4 = isUpdating, + transform = ::createCurrenciesStatuses, + ) + .distinctUntilChanged() + .onEach { maybeCurrenciesStatuses -> + send(maybeCurrenciesStatuses) + } + .launchIn(scope = this) + + launch { + fetchComponents(userWalletId, networks, currenciesIds, nonEmptyCurrencies) + }.invokeOnCompletion { + isUpdating.value = false + } + } + } + + private suspend fun Raise.fetchComponents( + userWalletId: UserWalletId, + networks: NonEmptySet, + currenciesIds: NonEmptySet, + currencies: NonEmptyList, + ) = coroutineScope { + catch( + block = { + awaitAll( + async { networksRepository.fetchNetworkStatuses(userWalletId, networks) }, + async { + val rawCurrenciesIds = currenciesIds.mapNotNullTo(mutableSetOf()) { it.rawCurrencyId } + quotesRepository.fetchQuotes(rawCurrenciesIds) + }, + async { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies) }, + ) + }, + catch = { + raise(TokenListError.DataError(it)) + }, + ) + } + + private fun createCurrenciesStatuses( + currencies: NonEmptyList, + maybeQuotes: Either>?, + maybeNetworkStatuses: Either>?, + maybeYieldBalances: Either?, + isUpdating: Boolean, + ): Lce> = lce { + isLoading.set(isUpdating) + + 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, + ) + + 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 getCurrencies(userWalletId: UserWalletId): EitherFlow> { + return currenciesRepository.getWalletCurrenciesUpdates(userWalletId) + .map, Either>> { it.right() } + .catch { emit(TokenListError.DataError(it).left()) } + .distinctUntilChanged() + } + + private fun getQuotes(tokensIds: NonEmptySet): Flow>> { + return quotesRepository.getQuotesUpdates(tokensIds.mapNotNull { it.rawCurrencyId }.toSet()) + .map, Either>> { it.right() } + .retryWhen { cause, _ -> + emit(TokenListError.DataError(cause).left()) + // adding delay before retry to avoid spam when flow restarted + delay(RETRY_DELAY) + true + } + .distinctUntilChanged() + } + + private fun getNetworksStatuses( + userWalletId: UserWalletId, + networks: NonEmptySet, + ): EitherFlow> { + return networksRepository.getNetworkStatusesUpdates(userWalletId, networks) + .map, Either>> { it.right() } + .retryWhen { cause, _ -> + emit(TokenListError.DataError(cause).left()) + // adding delay before retry to avoid spam when flow restarted + delay(RETRY_DELAY) + true + } + .distinctUntilChanged() + } + + private fun getYieldBalances( + userWalletId: UserWalletId, + cryptoCurrencies: List, + ): EitherFlow { + return stakingRepository.getMultiYieldBalanceUpdates(userWalletId, cryptoCurrencies) + .map> { it.right() } + .retryWhen { cause, _ -> + emit(TokenListError.DataError(cause).left()) + // adding delay before retry to avoid spam when flow restarted + delay(RETRY_DELAY) + true + } + .distinctUntilChanged() + } + + private fun getIds(currencies: List): Pair, NonEmptySet> { + 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 + } + } + + companion object { + private const val RETRY_DELAY = 2000L + } +} \ No newline at end of file