Updated on 2026-08-14

This commit is contained in:
Tangem 2024-04-01 12:44:54 +04:00
parent 861f650cd4
commit 59680b5305
7 changed files with 225 additions and 34 deletions

View file

@ -1,7 +1,7 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository

View file

@ -1,11 +1,16 @@
package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.left
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.core.utils.toLce
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.CurrenciesStatusesLceOperations
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
import com.tangem.domain.tokens.operations.TokenListOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -13,7 +18,6 @@ import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transformLatest
@ -25,11 +29,18 @@ class GetTokenListUseCase(
) {
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(userWalletId: UserWalletId): Flow<Either<TokenListError, TokenList>> {
return getTokensStatuses(userWalletId).transformLatest { maybeTokens ->
fun launch(userWalletId: UserWalletId): EitherFlow<TokenListError, TokenList> {
val operations = CurrenciesStatusesOperations(
userWalletId = userWalletId,
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
)
return operations.getCurrenciesStatusesFlow().transformLatest { maybeTokens ->
maybeTokens.fold(
ifLeft = { error ->
emit(error.left())
emit(error.mapToTokenListError().left())
},
ifRight = { tokens ->
emitAll(createTokenList(userWalletId, tokens))
@ -38,26 +49,35 @@ class GetTokenListUseCase(
}
}
private fun getTokensStatuses(
userWalletId: UserWalletId,
): Flow<Either<TokenListError, List<CryptoCurrencyStatus>>> {
val operations = CurrenciesStatusesOperations(
userWalletId = userWalletId,
@OptIn(ExperimentalCoroutinesApi::class)
fun launchLce(userWalletId: UserWalletId): LceFlow<TokenListError, TokenList> {
val operations = CurrenciesStatusesLceOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
)
return operations.getCurrenciesStatusesFlow()
.map { maybeCurrenciesStatuses ->
maybeCurrenciesStatuses.mapLeft(CurrenciesStatusesOperations.Error::mapToTokenListError)
}
return operations.getCurrenciesStatuses(userWalletId).transformLatest { maybeCurrencies ->
maybeCurrencies.fold(
ifLoading = { maybeContent ->
if (maybeContent != null) {
emitAll(createTokenListLce(userWalletId, maybeContent, isCurrenciesLoading = true))
} else {
emit(lceLoading())
}
},
ifContent = { content ->
emitAll(createTokenListLce(userWalletId, content, isCurrenciesLoading = false))
},
ifError = { error -> emit(error.lceError()) },
)
}
}
private fun createTokenList(
userWalletId: UserWalletId,
tokens: List<CryptoCurrencyStatus>,
): Flow<Either<TokenListError, TokenList>> {
): EitherFlow<TokenListError, TokenList> {
val operations = TokenListOperations(
userWalletId = userWalletId,
tokens = tokens,
@ -68,4 +88,22 @@ class GetTokenListUseCase(
maybeTokenList.mapLeft(TokenListOperations.Error::mapToTokenListError)
}
}
private fun createTokenListLce(
userWalletId: UserWalletId,
currencies: List<CryptoCurrencyStatus>,
isCurrenciesLoading: Boolean,
): LceFlow<TokenListError, TokenList> {
val operations = TokenListOperations(
userWalletId = userWalletId,
tokens = currencies,
currenciesRepository = currenciesRepository,
)
return operations.getTokenListFlow().map { maybeTokenList ->
maybeTokenList
.mapLeft(TokenListOperations.Error::mapToTokenListError)
.toLce(isCurrenciesLoading)
}
}
}

View file

@ -0,0 +1,153 @@
package com.tangem.domain.tokens.operations
import arrow.core.*
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.utils.lceError
import com.tangem.domain.core.utils.lceLoading
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.flow.*
internal class CurrenciesStatusesLceOperations(
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
) {
fun getCurrenciesStatuses(userWalletId: UserWalletId): LceFlow<TokenListError, List<CryptoCurrencyStatus>> {
return getMultiCurrencyWalletCurrencies(userWalletId).transform transform@{ maybeCurrencies ->
val nonEmptyCurrencies = maybeCurrencies.fold(
ifLoading = { maybeContent ->
emit(createLoadingCurrenciesStatuses(maybeContent))
return@transform
},
ifContent = { content ->
val nonEmptyCurrencies = content.toNonEmptyListOrNull()
if (nonEmptyCurrencies == null) {
emit(TokenListError.EmptyTokens.lceError())
return@transform
} else {
nonEmptyCurrencies
}
},
ifError = { error ->
emit(error.lceError())
return@transform
},
)
val (networks, currenciesIds) = getIds(nonEmptyCurrencies)
combine(
getQuotes(currenciesIds),
getNetworksStatuses(userWalletId, networks),
) { maybeQuotes, maybeNetworksStatuses ->
val statuses = createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses)
emit(statuses)
}.collect()
}
}
private fun createLoadingCurrenciesStatuses(
maybeCurrencies: List<CryptoCurrency>?,
): Lce<TokenListError, List<CryptoCurrencyStatus>> {
val nonEmptyCurrencies = maybeCurrencies?.toNonEmptyListOrNull()
val statuses = if (nonEmptyCurrencies == null) {
lceLoading()
} else {
createCurrenciesStatuses(
nonEmptyCurrencies,
maybeNetworkStatuses = null,
maybeQuotes = null,
)
}
return statuses
}
private fun getMultiCurrencyWalletCurrencies(
userWalletId: UserWalletId,
): LceFlow<TokenListError, List<CryptoCurrency>> {
return currenciesRepository.getMultiCurrencyWalletCurrenciesUpdatesLce(userWalletId)
.map { maybeCurrencies ->
maybeCurrencies.mapError { TokenListError.DataError(it) }
}
}
private fun createCurrenciesStatuses(
currencies: NonEmptyList<CryptoCurrency>,
maybeQuotes: Either<TokenListError, Set<Quote>>?,
maybeNetworkStatuses: Lce<TokenListError, Set<NetworkStatus>>?,
): Lce<TokenListError, List<CryptoCurrencyStatus>> = lce {
isLoading.set(maybeNetworkStatuses == null)
var quotesRetrievingFailed = false
val networksStatuses = maybeNetworkStatuses?.bindOrNull()?.toNonEmptySetOrNull()
val quotes = recover({ maybeQuotes?.bind()?.toNonEmptySetOrNull() }) {
quotesRetrievingFailed = true
null
}
currencies.map { currency ->
val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }
val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network }
createCurrencyStatus(currency, quote, networkStatus, ignoreQuote = quotesRetrievingFailed)
}
}
private fun createCurrencyStatus(
currency: CryptoCurrency,
quote: Quote?,
networkStatus: NetworkStatus?,
ignoreQuote: Boolean,
): CryptoCurrencyStatus {
val currencyStatusOperations = CurrencyStatusOperations(
currency = currency,
quote = quote,
networkStatus = networkStatus,
ignoreQuote = ignoreQuote,
)
return currencyStatusOperations.createTokenStatus()
}
private fun getQuotes(tokensIds: NonEmptySet<CryptoCurrency.ID>): Flow<Either<TokenListError, Set<Quote>>> {
return quotesRepository.getQuotesUpdates(tokensIds)
.map<Set<Quote>, Either<TokenListError, Set<Quote>>> { it.right() }
.catch { emit(TokenListError.DataError(it).left()) }
}
private fun getNetworksStatuses(
userWalletId: UserWalletId,
networks: NonEmptySet<Network>,
): LceFlow<TokenListError, Set<NetworkStatus>> {
return networksRepository.getNetworkStatusesUpdatesLce(userWalletId, networks)
.map { maybeStatuses ->
maybeStatuses.mapError { TokenListError.DataError(it) }
}
}
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
}
}

View file

@ -43,7 +43,7 @@ internal class GetTokenListUseCaseTest {
)
// When
val result = useCase(userWalletId)
val result = useCase.launch(userWalletId)
.take(count = 2)
.toList()
@ -59,7 +59,7 @@ internal class GetTokenListUseCaseTest {
val useCase = getUseCase(tokens = flowOf(DataError.NetworkError.NoInternetConnection.left()))
// When
val result = useCase(userWalletId).first()
val result = useCase.launch(userWalletId).first()
// Then
assertEquals(expectedResult, result)
@ -79,7 +79,7 @@ internal class GetTokenListUseCaseTest {
)
// When
val result = useCase(userWalletId)
val result = useCase.launch(userWalletId)
.take(count = 2)
.toList()
@ -95,7 +95,7 @@ internal class GetTokenListUseCaseTest {
val useCase = getUseCase(isGrouped = flowOf(DataError.NetworkError.NoInternetConnection.left()))
// When
val result = useCase(userWalletId).first()
val result = useCase.launch(userWalletId).first()
// Then
assertEquals(expectedResult, result)
@ -109,7 +109,7 @@ internal class GetTokenListUseCaseTest {
val useCase = getUseCase(isSortedByBalance = flowOf(DataError.NetworkError.NoInternetConnection.left()))
// When
val result = useCase(userWalletId).first()
val result = useCase.launch(userWalletId).first()
// Then
assertEquals(expectedResult, result)
@ -134,7 +134,7 @@ internal class GetTokenListUseCaseTest {
)
// When
val result = useCase(userWalletId)
val result = useCase.launch(userWalletId)
.take(count = 3)
.toList()
@ -153,7 +153,7 @@ internal class GetTokenListUseCaseTest {
val useCase = getUseCase(isGrouped = flowOf(true.right()))
// When
val result = useCase(userWalletId)
val result = useCase.launch(userWalletId)
.take(count = 2)
.toList()
@ -175,7 +175,7 @@ internal class GetTokenListUseCaseTest {
)
// When
val result = useCase(userWalletId)
val result = useCase.launch(userWalletId)
.take(count = 2)
.toList()
@ -197,7 +197,7 @@ internal class GetTokenListUseCaseTest {
)
// When
val result = useCase(userWalletId)
val result = useCase.launch(userWalletId)
.take(count = 2)
.toList()
@ -212,7 +212,7 @@ internal class GetTokenListUseCaseTest {
val useCase = getUseCase(tokens = flowOf(emptyList<CryptoCurrency>().right()))
// When
val result = useCase(userWalletId).first()
val result = useCase.launch(userWalletId).first()
// Then
assertEquals(expectedResult, result)
@ -225,7 +225,7 @@ internal class GetTokenListUseCaseTest {
val useCase = getUseCase(tokens = flowOf())
// When
val result = useCase(userWalletId).first()
val result = useCase.launch(userWalletId).first()
// Then
assertEquals(expectedResult, result)
@ -241,7 +241,7 @@ internal class GetTokenListUseCaseTest {
val useCase = getUseCase(statuses = flowOf())
// When
val result = useCase(userWalletId)
val result = useCase.launch(userWalletId)
.take(count = 2)
.toList()
@ -256,7 +256,7 @@ internal class GetTokenListUseCaseTest {
val useCase = getUseCase(statuses = flowOf(emptySet<NetworkStatus>().right()))
// When
val result = useCase(userWalletId).first()
val result = useCase.launch(userWalletId).first()
// Then
assertEquals(expectedResult, result)
@ -275,7 +275,7 @@ internal class GetTokenListUseCaseTest {
)
// When
val result = useCase(userWalletId)
val result = useCase.launch(userWalletId)
.take(count = 2)
.toList()
@ -293,7 +293,7 @@ internal class GetTokenListUseCaseTest {
)
// When
val result = useCase(userWalletId).first()
val result = useCase.launch(userWalletId).first()
// Then
assertEquals(expectedResult, result)

View file

@ -161,7 +161,7 @@ internal class OrganizeTokensViewModel @Inject constructor(
private fun bootstrapTokenList() {
viewModelScope.launch(dispatchers.default) {
val maybeTokenList = getTokenListUseCase(userWalletId)
val maybeTokenList = getTokenListUseCase.launch(userWalletId)
.first { it.getOrNull()?.totalFiatBalance !is TokenList.FiatBalance.Loading }
maybeTokenList.fold(

View file

@ -47,7 +47,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
val promoFlow = flow { emit(promoRepository.getChangellyPromoBanner()) }
return combine(
flow = getTokenListUseCase(userWallet.walletId).conflate(),
flow = getTokenListUseCase.launch(userWallet.walletId).conflate(),
flow2 = isReadyToShowRateAppUseCase().conflate(),
flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(),
flow4 = shouldShowSwapPromoWalletUseCase().conflate(),

View file

@ -33,7 +33,7 @@ internal class MultiWalletTokenListSubscriber(
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
) {
override fun tokenListFlow(): MaybeTokenListFlow = getTokenListUseCase(userWallet.walletId)
override fun tokenListFlow(): MaybeTokenListFlow = getTokenListUseCase.launch(userWallet.walletId)
override suspend fun onTokenListReceived(maybeTokenList: Either<TokenListError, TokenList>) {
// TODO disabled for 5.7.2 because of potential critical