Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-21 12:03:02 +03:00
parent 616c0b2c05
commit 60d8a4f2a3
7 changed files with 83 additions and 68 deletions

View file

@ -4,8 +4,8 @@ import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.quote.model.StoredQuote
import com.tangem.domain.tokens.model.CryptoCurrency
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.flow.*
internal class DefaultQuotesStore(
private val dataStore: StringKeyDataStore<StoredQuote>,
@ -13,17 +13,20 @@ internal class DefaultQuotesStore(
override fun get(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<StoredQuote>> {
return channelFlow {
if (dataStore.isEmpty()) {
val flows = currenciesIds.mapNotNull { currencyId ->
currencyId.rawCurrencyId?.let(dataStore::get)
}
if (dataStore.isEmpty() || flows.isEmpty()) {
send(emptySet())
}
val quotes = currenciesIds.mapNotNull { currencyId ->
currencyId.rawCurrencyId?.let {
dataStore.getSyncOrNull(it)
merge(*flows.toTypedArray())
.scan<StoredQuote, Set<StoredQuote>>(emptySet()) { acc, quote ->
acc.addOrReplace(quote) { it.rawCurrencyId == quote.rawCurrencyId }
}
}
send(quotes.toSet())
.filter(Set<StoredQuote>::isNotEmpty)
.collect(::send)
}
}

View file

@ -10,16 +10,15 @@ import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
class GetCryptoCurrencyStatusesUseCase(
class GetCryptoCurrencyStatusesSyncUseCase(
internal val currenciesRepository: CurrenciesRepository,
internal val quotesRepository: QuotesRepository,
internal val networksRepository: NetworksRepository,
internal val dispatchers: CoroutineDispatcherProvider,
) {
operator fun invoke(userWalletId: UserWalletId): Flow<Either<TokenListError, List<CryptoCurrencyStatus>>> {
suspend operator fun invoke(userWalletId: UserWalletId): Either<TokenListError, List<CryptoCurrencyStatus>> {
val operations = CurrenciesStatusesOperations(
userWalletId = userWalletId,
currenciesRepository = currenciesRepository,
@ -27,9 +26,7 @@ class GetCryptoCurrencyStatusesUseCase(
networksRepository = networksRepository,
)
return operations.getCurrenciesStatusesMergedFlow()
.map { maybeCurrenciesStatuses ->
maybeCurrenciesStatuses.mapLeft(CurrenciesStatusesOperations.Error::mapToTokenListError)
}
return operations.getCurrenciesStatusesSync()
.mapLeft { error -> error.mapToTokenListError() }
}
}

View file

@ -67,33 +67,21 @@ internal class CurrenciesStatusesOperations(
}
}
fun getCurrenciesStatusesMergedFlow(): Flow<Either<Error, List<CryptoCurrencyStatus>>> {
return getMultiCurrencyWalletCurrencies().transformLatest { maybeCurrencies ->
val nonEmptyCurrencies = maybeCurrencies.fold(
ifLeft = { error ->
emit(error.left())
return@transformLatest
suspend fun getCurrenciesStatusesSync(): Either<Error, List<CryptoCurrencyStatus>> {
return either {
catch(
block = {
val nonEmptyCurrencies =
currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId).toNonEmptyListOrNull()
?: return emptyList<CryptoCurrencyStatus>().right()
val (networks, currenciesIds) = getIds(nonEmptyCurrencies)
val quotes = quotesRepository.getQuotesSync(currenciesIds, false).right()
val networkStatuses =
networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right()
return createCurrenciesStatuses(nonEmptyCurrencies, quotes, networkStatuses)
},
ifRight = List<CryptoCurrency>::toNonEmptyListOrNull,
catch = { raise(Error.DataError(it)) },
)
if (nonEmptyCurrencies == null) {
val emptyCurrenciesStatuses = emptyList<CryptoCurrencyStatus>()
emit(emptyCurrenciesStatuses.right())
return@transformLatest
}
val (networks, currenciesIds) = getIds(nonEmptyCurrencies)
val currenciesFlow = combine(
getQuotes(currenciesIds),
getNetworksStatuses(networks),
) { maybeQuotes, maybeNetworksStatuses ->
createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses)
}
emitAll(currenciesFlow)
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.feature.swap.domain
import arrow.core.getOrElse
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.Network
@ -22,7 +22,6 @@ import com.tangem.lib.crypto.UserWalletManager
import com.tangem.lib.crypto.models.*
import com.tangem.lib.crypto.models.transactions.SendTxResult
import com.tangem.utils.toFiatString
import kotlinx.coroutines.flow.first
import timber.log.Timber
import java.math.BigDecimal
import java.math.RoundingMode
@ -39,7 +38,7 @@ internal class SwapInteractorImpl @Inject constructor(
private val networksRepository: NetworksRepository,
private val walletFeatureToggles: WalletFeatureToggles,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesUseCase,
private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
) : SwapInteractor {
// TODO: Move to DI
@ -61,7 +60,6 @@ internal class SwapInteractorImpl @Inject constructor(
requireNotNull(selectedWallet) { "No selected wallet" }
val walletCurrencyStatuses = getMultiCryptoCurrencyStatusUseCase(selectedWallet.walletId)
.first()
.getOrElse { emptyList() }
val walletCurrencyStatusesExceptInitial = walletCurrencyStatuses.filter {

View file

@ -2,7 +2,7 @@ package com.tangem.feature.swap.domain.di
import com.tangem.domain.tokens.GetCardTokensListUseCase
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
@ -35,7 +35,7 @@ class SwapDomainModule {
networksRepository: NetworksRepository,
walletFeatureToggles: WalletFeatureToggles,
@SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
@SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesUseCase,
@SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
): SwapInteractor {
return SwapInteractorImpl(
transactionManager = transactionManager,
@ -81,8 +81,8 @@ class SwapDomainModule {
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
dispatchers: CoroutineDispatcherProvider,
): GetCryptoCurrencyStatusesUseCase {
return GetCryptoCurrencyStatusesUseCase(
): GetCryptoCurrencyStatusesSyncUseCase {
return GetCryptoCurrencyStatusesSyncUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,

View file

@ -2,10 +2,13 @@ package com.tangem.feature.swap.converters
import com.tangem.common.Provider
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.core.ui.extensions.getTintForTokenIcon
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.swap.domain.models.domain.CryptoCurrencySwapInfo
import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup
@ -26,13 +29,13 @@ class TokensDataConverter(
val availableTitle = TokenToSelectState.Title(stringReference("My tokens")) // todo replace with resource
val unavailableTitle = TokenToSelectState.Title(stringReference("My tokens")) // todo replace with resource
return SwapSelectTokenStateHolder(
availableTokens = value.available.map { tokenWithBalanceToTokenToSelect(it) }
availableTokens = value.available.map { tokenWithBalanceToTokenToSelect(it, true) }
.toMutableList()
.apply {
this.add(0, availableTitle)
}
.toImmutableList(),
unavailableTokens = value.unavailable.map { tokenWithBalanceToTokenToSelect(it) }
unavailableTokens = value.unavailable.map { tokenWithBalanceToTokenToSelect(it, false) }
.toMutableList()
.apply {
this.add(0, unavailableTitle)
@ -43,18 +46,17 @@ class TokensDataConverter(
)
}
private fun tokenWithBalanceToTokenToSelect(cryptoCurrencySwapInfo: CryptoCurrencySwapInfo): TokenToSelectState {
private fun tokenWithBalanceToTokenToSelect(
cryptoCurrencySwapInfo: CryptoCurrencySwapInfo,
isAvailable: Boolean,
): TokenToSelectState {
val cryptoCurrencyStatus = cryptoCurrencySwapInfo.currencyStatus
return TokenToSelectState.TokenToSelect(
id = cryptoCurrencyStatus.currency.id.value,
name = cryptoCurrencyStatus.currency.name,
symbol = cryptoCurrencyStatus.currency.symbol,
tokenIcon = TokenIconState.CoinIcon(
url = cryptoCurrencyStatus.currency.iconUrl,
fallbackResId = cryptoCurrencyStatus.currency.networkIconResId,
isGrayscale = false,
showCustomBadge = cryptoCurrencyStatus.currency.isCustom,
),
available = isAvailable,
tokenIcon = convertIcon(cryptoCurrencyStatus.currency, isAvailable),
addedTokenBalanceData = TokenBalanceData(
amount = formatCryptoAmount(cryptoCurrencyStatus),
amountEquivalent = formatFiatAmount(cryptoCurrencyStatus, appCurrencyProvider.invoke()),
@ -63,6 +65,32 @@ class TokensDataConverter(
)
}
private fun convertIcon(currency: CryptoCurrency, isAvailable: Boolean): TokenIconState {
return when (currency) {
is CryptoCurrency.Coin -> {
TokenIconState.CoinIcon(
url = currency.iconUrl,
fallbackResId = currency.networkIconResId,
isGrayscale = !isAvailable,
showCustomBadge = currency.isCustom,
)
}
is CryptoCurrency.Token -> {
val isGrayscale = currency.network.isTestnet
val background = currency.tryGetBackgroundForTokenIcon(isGrayscale)
val tint = getTintForTokenIcon(background)
TokenIconState.TokenIcon(
url = currency.iconUrl,
isGrayscale = !isAvailable,
showCustomBadge = currency.isCustom,
networkBadgeIconResId = currency.networkIconResId,
fallbackTint = tint,
fallbackBackground = background,
)
}
}
}
private fun formatCryptoAmount(cryptoCurrencyStatus: CryptoCurrencyStatus): String {
return BigDecimalFormatter.formatCryptoAmount(
cryptoCurrencyStatus.value.amount,

View file

@ -111,7 +111,7 @@ private fun TitleHeader(item: TokenToSelectState.Title, modifier: Modifier = Mod
.background(TangemTheme.colors.background.action),
) {
Text(
text = item.title.resolveReference(),
text = item.title.resolveReference().uppercase(),
style = TangemTheme.typography.overline,
modifier = Modifier
.padding(
@ -133,7 +133,10 @@ private fun TokenItem(
modifier = modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size72)
.clickable(onClick = onTokenClick)
.clickable(
enabled = token.available,
onClick = onTokenClick,
)
.padding(
vertical = TangemTheme.dimens.spacing14,
horizontal = TangemTheme.dimens.spacing16,
@ -169,13 +172,7 @@ private fun TokenItem(
Spacer(modifier = Modifier.weight(1f))
if (!token.available) {
Text(
text = stringResource(id = R.string.swapping_token_not_available),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
} else if (token.addedTokenBalanceData != null) {
if (token.addedTokenBalanceData != null) {
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.Center,
@ -190,7 +187,11 @@ private fun TokenItem(
token.addedTokenBalanceData.amountEquivalent.orEmpty()
},
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
color = if (token.available) {
TangemTheme.colors.text.primary1
} else {
TangemTheme.colors.text.tertiary
},
)
SpacerW2()
Text(