Updated on 2026-08-14
This commit is contained in:
parent
a2504e3c4b
commit
62b5e25567
3 changed files with 280 additions and 15 deletions
|
|
@ -28,10 +28,10 @@ typealias LceFlow<E, C> = Flow<Lce<E, C>>
|
|||
* @property producerScope The [ProducerScope] instance that this class wraps.
|
||||
* @property ifLoading The function to call if a loading state is raised.
|
||||
*/
|
||||
class LceFlowScope<E : Any, C : Any> @PublishedApi internal constructor(
|
||||
class LceFlowRaise<E : Any, C : Any> @PublishedApi internal constructor(
|
||||
private val raise: LceRaise<E>,
|
||||
private val producerScope: ProducerScope<Lce<E, C>>,
|
||||
private val ifLoading: suspend LceFlowScope<E, C>.(C?) -> Unit,
|
||||
private val ifLoading: suspend LceFlowRaise<E, C>.(C?) -> Unit,
|
||||
) : Raise<E>, CoroutineScope by producerScope {
|
||||
|
||||
val isLoading: AtomicBoolean = AtomicBoolean(value = true)
|
||||
|
|
@ -92,25 +92,25 @@ class LceFlowScope<E : Any, C : Any> @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 <E : Any, C : Any> lceFlow(
|
||||
ifLoading: suspend LceFlowScope<E, C>.(C?) -> Unit = { send(lceLoading(partialContent = it)) },
|
||||
@BuilderInference block: suspend LceFlowScope<E, C>.() -> Unit,
|
||||
ifLoading: suspend LceFlowRaise<E, C>.(C?) -> Unit = { send(lceLoading(partialContent = it)) },
|
||||
@BuilderInference block: suspend LceFlowRaise<E, C>.() -> Unit,
|
||||
): LceFlow<E, C> {
|
||||
return channelFlow {
|
||||
trySend(lceLoading())
|
||||
|
||||
lce {
|
||||
val scope = LceFlowScope(
|
||||
val scope = LceFlowRaise(
|
||||
raise = this@lce,
|
||||
producerScope = this@channelFlow,
|
||||
ifLoading = ifLoading,
|
||||
|
|
|
|||
|
|
@ -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<TokenListError, TokenList> {
|
||||
val operations = CurrenciesStatusesLceOperations(
|
||||
currenciesRepository = currenciesRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
networksRepository = networksRepository,
|
||||
stakingRepository = stakingRepository,
|
||||
)
|
||||
fun launch(userWalletId: UserWalletId, usePersistenceCache: Boolean = false): LceFlow<TokenListError, TokenList> {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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<TokenListError, List<CryptoCurrencyStatus>> {
|
||||
return transformToCurrenciesStatuses(
|
||||
userWalletId = userWalletId,
|
||||
currenciesFlow = getCurrencies(userWalletId),
|
||||
)
|
||||
}
|
||||
|
||||
private fun transformToCurrenciesStatuses(
|
||||
userWalletId: UserWalletId,
|
||||
currenciesFlow: EitherFlow<TokenListError, List<CryptoCurrency>>,
|
||||
): LceFlow<TokenListError, List<CryptoCurrencyStatus>> = 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<TokenListError, Set<Quote>>?,
|
||||
maybeNetworkStatuses: Either<TokenListError, Set<NetworkStatus>>?,
|
||||
maybeYieldBalances: Either<TokenListError, YieldBalanceList>?,
|
||||
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<TokenListError>.fetchComponents(
|
||||
userWalletId: UserWalletId,
|
||||
networks: NonEmptySet<Network>,
|
||||
currenciesIds: NonEmptySet<CryptoCurrency.ID>,
|
||||
currencies: NonEmptyList<CryptoCurrency>,
|
||||
) = 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<CryptoCurrency>,
|
||||
maybeQuotes: Either<TokenListError, Set<Quote>>?,
|
||||
maybeNetworkStatuses: Either<TokenListError, Set<NetworkStatus>>?,
|
||||
maybeYieldBalances: Either<TokenListError, YieldBalanceList>?,
|
||||
isUpdating: Boolean,
|
||||
): Lce<TokenListError, List<CryptoCurrencyStatus>> = 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<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 getQuotes(tokensIds: NonEmptySet<CryptoCurrency.ID>): Flow<Either<TokenListError, Set<Quote>>> {
|
||||
return quotesRepository.getQuotesUpdates(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_DELAY)
|
||||
true
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun getNetworksStatuses(
|
||||
userWalletId: UserWalletId,
|
||||
networks: NonEmptySet<Network>,
|
||||
): EitherFlow<TokenListError, Set<NetworkStatus>> {
|
||||
return networksRepository.getNetworkStatusesUpdates(userWalletId, networks)
|
||||
.map<Set<NetworkStatus>, Either<TokenListError, Set<NetworkStatus>>> { 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<CryptoCurrency>,
|
||||
): EitherFlow<TokenListError, YieldBalanceList> {
|
||||
return stakingRepository.getMultiYieldBalanceUpdates(userWalletId, cryptoCurrencies)
|
||||
.map<YieldBalanceList, Either<TokenListError, YieldBalanceList>> { 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<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
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val RETRY_DELAY = 2000L
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue