Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-21 12:12:33 +07:00
parent 82eda99ca7
commit 5aa2af25f1
9 changed files with 110 additions and 55 deletions

View file

@ -45,7 +45,7 @@ internal class DefaultQuotesRepository(
.filterNotNull() .filterNotNull()
.flatMapLatest { appCurrency -> .flatMapLatest { appCurrency ->
fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = refresh) fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = refresh)
quotesStore.get(currenciesIds).map(quotesConverter::convertSet) quotesStore.get(currenciesIds).map { quotesConverter.convert(currenciesIds to it) }
} }
.cancellable() .cancellable()
.flowOn(dispatchers.io) .flowOn(dispatchers.io)
@ -77,14 +77,15 @@ internal class DefaultQuotesRepository(
val quotes = quotesStore.getSync(currenciesIds) val quotes = quotesStore.getSync(currenciesIds)
quotesConverter.convertSet(quotes) quotesConverter.convert(currenciesIds to quotes)
} }
} }
override suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote? { override suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote? {
return withContext(dispatchers.io) { return withContext(dispatchers.io) {
val quote = quotesStore.getSync(setOf(currencyId)).firstOrNull() val setOfCurrencyId = setOf(currencyId)
quote?.let { quotesConverter.convert(it) } val quote = quotesStore.getSync(setOfCurrencyId).firstOrNull()
quote?.let { quotesConverter.convert(setOfCurrencyId to setOf(it)).firstOrNull() }
} }
} }

View file

@ -1,16 +1,28 @@
package com.tangem.data.tokens.utils package com.tangem.data.tokens.utils
import com.tangem.datasource.local.quote.model.StoredQuote import com.tangem.datasource.local.quote.model.StoredQuote
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.model.Quote
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.Converter
import java.math.BigDecimal import java.math.BigDecimal
internal class QuotesConverter : Converter<StoredQuote, Quote> { typealias QuotesConverterValue = Pair<Set<CryptoCurrency.ID>, Set<StoredQuote>>
override fun convert(value: StoredQuote): Quote { internal class QuotesConverter : Converter<QuotesConverterValue, Set<Quote>> {
val (rawCurrencyId, responseQuote) = value
return Quote( override fun convert(value: QuotesConverterValue): Set<Quote> {
val (setOfCurrencyId, setOfStoredQuote) = value
return setOfCurrencyId.mapTo(hashSetOf()) { id ->
setOfStoredQuote.find { id.rawCurrencyId == it.rawCurrencyId }
?.let(::convertExistStoredQuote)
?: Quote.Empty(id.rawCurrencyId)
}
}
private fun convertExistStoredQuote(storedQuote: StoredQuote): Quote {
val (rawCurrencyId, responseQuote) = storedQuote
return Quote.Value(
rawCurrencyId = rawCurrencyId, rawCurrencyId = rawCurrencyId,
fiatRate = responseQuote.price ?: BigDecimal.ZERO, fiatRate = responseQuote.price ?: BigDecimal.ZERO,
priceChange = (responseQuote.priceChange24h ?: BigDecimal.ZERO).movePointLeft(2), priceChange = (responseQuote.priceChange24h ?: BigDecimal.ZERO).movePointLeft(2),

View file

@ -15,10 +15,10 @@ class GetCurrencyQuotesUseCase(
currencyID: CryptoCurrency.ID, currencyID: CryptoCurrency.ID,
interval: PriceChangeInterval, interval: PriceChangeInterval,
refresh: Boolean, refresh: Boolean,
): Flow<Option<Quote>> { ): Flow<Option<Quote.Value>> {
return quotesRepository.getQuotesUpdates( return quotesRepository.getQuotesUpdates(
currenciesIds = setOf(currencyID), currenciesIds = setOf(currencyID),
refresh = refresh, refresh = refresh,
).map { it.firstOrNull().toOption() }.catch { emit(None) } ).map { it.filterIsInstance<Quote.Value>().firstOrNull().toOption() }.catch { emit(None) }
} }
} }

View file

@ -2,15 +2,27 @@ package com.tangem.domain.tokens.model
import java.math.BigDecimal import java.math.BigDecimal
/** sealed interface Quote {
* Represents financial information for a specific cryptocurrency, including its fiat exchange rate and price change.
* val rawCurrencyId: String?
* @property rawCurrencyId The unique identifier of the cryptocurrency for which the financial information is provided.
* @property fiatRate The current fiat exchange rate for the cryptocurrency. /**
* @property priceChange The price change for the cryptocurrency. * Represents unknown financial information for a specific cryptocurrency.
*/ *
data class Quote( * @property rawCurrencyId The raw cryptocurrency ID. If it is a custom token, the value will be `null`.
val rawCurrencyId: String, */
val fiatRate: BigDecimal, data class Empty(override val rawCurrencyId: String?) : Quote
val priceChange: BigDecimal,
) /**
* Represents financial information for a specific cryptocurrency, including its fiat exchange rate and price change.
*
* @property rawCurrencyId The unique identifier of the cryptocurrency for which the financial information is provided.
* @property fiatRate The current fiat exchange rate for the cryptocurrency.
* @property priceChange The price change for the cryptocurrency.
*/
data class Value(
override val rawCurrencyId: String,
val fiatRate: BigDecimal,
val priceChange: BigDecimal,
) : Quote
}

View file

@ -12,6 +12,18 @@ internal class CurrencyStatusOperations(
private val ignoreQuote: Boolean, private val ignoreQuote: Boolean,
) { ) {
private val Quote?.fiatRate: BigDecimal?
get() = when (this) {
is Quote.Value -> this.fiatRate
is Quote.Empty, null -> null
}
private val Quote?.priceChange: BigDecimal?
get() = when (this) {
is Quote.Value -> this.priceChange
is Quote.Empty, null -> null
}
fun createTokenStatus(): CryptoCurrencyStatus = CryptoCurrencyStatus(currency, createStatus()) fun createTokenStatus(): CryptoCurrencyStatus = CryptoCurrencyStatus(currency, createStatus())
private fun createStatus(): CryptoCurrencyStatus.Value { private fun createStatus(): CryptoCurrencyStatus.Value {
@ -74,7 +86,7 @@ internal class CurrencyStatusOperations(
null null
} }
return when { return when {
ignoreQuote -> CryptoCurrencyStatus.NoQuote( quote is Quote.Empty || ignoreQuote -> CryptoCurrencyStatus.NoQuote(
amount = amount, amount = amount,
hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, hasCurrentNetworkTransactions = hasCurrentNetworkTransactions,
pendingTransactions = currentTransactions, pendingTransactions = currentTransactions,
@ -91,8 +103,7 @@ internal class CurrencyStatusOperations(
networkAddress = status.address, networkAddress = status.address,
yieldBalance = currentYieldBalance, yieldBalance = currentYieldBalance,
) )
quote == null -> CryptoCurrencyStatus.Loading quote is Quote.Value -> CryptoCurrencyStatus.Loaded(
else -> CryptoCurrencyStatus.Loaded(
amount = amount, amount = amount,
fiatAmount = calculateFiatAmount(amount, quote.fiatRate), fiatAmount = calculateFiatAmount(amount, quote.fiatRate),
fiatRate = quote.fiatRate, fiatRate = quote.fiatRate,
@ -102,6 +113,7 @@ internal class CurrencyStatusOperations(
networkAddress = status.address, networkAddress = status.address,
yieldBalance = currentYieldBalance, yieldBalance = currentYieldBalance,
) )
else -> CryptoCurrencyStatus.Loading
} }
} }

View file

@ -7,65 +7,71 @@ import java.math.BigDecimal
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
internal object MockQuotes { internal object MockQuotes {
val quote1 = Quote( val quote1 = Quote.Value(
rawCurrencyId = MockTokens.token1.id.rawCurrencyId!!, rawCurrencyId = MockTokens.token1.id.rawCurrencyId!!,
fiatRate = BigDecimal("1.23"), fiatRate = BigDecimal("1.23"),
priceChange = BigDecimal("0.01"), priceChange = BigDecimal("0.01"),
) )
val quote2 = Quote( val quote2 = Quote.Value(
rawCurrencyId = MockTokens.token2.id.rawCurrencyId!!, rawCurrencyId = MockTokens.token2.id.rawCurrencyId!!,
fiatRate = BigDecimal("2.34"), fiatRate = BigDecimal("2.34"),
priceChange = BigDecimal("-0.02"), priceChange = BigDecimal("-0.02"),
) )
val quote3 = Quote( val quote3 = Quote.Value(
rawCurrencyId = MockTokens.token3.id.rawCurrencyId!!, rawCurrencyId = MockTokens.token3.id.rawCurrencyId!!,
fiatRate = BigDecimal("3.45"), fiatRate = BigDecimal("3.45"),
priceChange = BigDecimal("0.03"), priceChange = BigDecimal("0.03"),
) )
val quote4 = Quote( val quote4 = Quote.Value(
rawCurrencyId = MockTokens.token4.id.rawCurrencyId!!, rawCurrencyId = MockTokens.token4.id.rawCurrencyId!!,
fiatRate = BigDecimal("4.56"), fiatRate = BigDecimal("4.56"),
priceChange = BigDecimal("-0.04"), priceChange = BigDecimal("-0.04"),
) )
val quote5 = Quote( val quote5 = Quote.Value(
rawCurrencyId = MockTokens.token5.id.rawCurrencyId!!, rawCurrencyId = MockTokens.token5.id.rawCurrencyId!!,
fiatRate = BigDecimal("5.67"), fiatRate = BigDecimal("5.67"),
priceChange = BigDecimal("0.05"), priceChange = BigDecimal("0.05"),
) )
val quote6 = Quote( val quote6 = Quote.Value(
rawCurrencyId = MockTokens.token6.id.rawCurrencyId!!, rawCurrencyId = MockTokens.token6.id.rawCurrencyId!!,
fiatRate = BigDecimal("6.78"), fiatRate = BigDecimal("6.78"),
priceChange = BigDecimal("-0.06"), priceChange = BigDecimal("-0.06"),
) )
val quote7 = Quote( val quote7 = Quote.Value(
rawCurrencyId = MockTokens.token7.id.rawCurrencyId!!, rawCurrencyId = MockTokens.token7.id.rawCurrencyId!!,
fiatRate = BigDecimal("7.89"), fiatRate = BigDecimal("7.89"),
priceChange = BigDecimal("0.07"), priceChange = BigDecimal("0.07"),
) )
val quote8 = Quote( val quote8 = Quote.Value(
rawCurrencyId = MockTokens.token8.id.rawCurrencyId!!, rawCurrencyId = MockTokens.token8.id.rawCurrencyId!!,
fiatRate = BigDecimal("8.90"), fiatRate = BigDecimal("8.90"),
priceChange = BigDecimal("-0.08"), priceChange = BigDecimal("-0.08"),
) )
val quote9 = Quote( val quote9 = Quote.Value(
rawCurrencyId = MockTokens.token9.id.rawCurrencyId!!, rawCurrencyId = MockTokens.token9.id.rawCurrencyId!!,
fiatRate = BigDecimal("9.01"), fiatRate = BigDecimal("9.01"),
priceChange = BigDecimal("0.09"), priceChange = BigDecimal("0.09"),
) )
val quote10 = Quote( val quote10 = Quote.Value(
rawCurrencyId = MockTokens.token10.id.rawCurrencyId!!, rawCurrencyId = MockTokens.token10.id.rawCurrencyId!!,
fiatRate = BigDecimal("10.12"), fiatRate = BigDecimal("10.12"),
priceChange = BigDecimal("-0.10"), priceChange = BigDecimal("-0.10"),
) )
val quotes = nonEmptySetOf(quote1, quote2, quote3, quote4, quote5, quote6, quote7, quote8, quote9, quote10) val quote11 = Quote.Empty(null)
val quote12 = Quote.Empty("null")
val quotes = nonEmptySetOf(
quote1, quote2, quote3, quote4, quote5, quote6, quote7, quote8, quote9, quote10,
quote11, quote12,
)
} }

View file

@ -1,10 +1,7 @@
package com.tangem.domain.tokens.mock package com.tangem.domain.tokens.mock
import arrow.core.nonEmptyListOf import arrow.core.nonEmptyListOf
import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.tokens.model.NetworkStatus
import java.math.BigDecimal import java.math.BigDecimal
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
@ -140,20 +137,29 @@ internal object MockTokensStates {
as? CryptoCurrencyAmountStatus.Loaded as? CryptoCurrencyAmountStatus.Loaded
)?.value ?: BigDecimal.ZERO )?.value ?: BigDecimal.ZERO
val quote = MockQuotes.quotes.first { it.rawCurrencyId == status.currency.id.rawCurrencyId } val quote = MockQuotes.quotes.first { it.rawCurrencyId == status.currency.id.rawCurrencyId }
val fiatAmount = amount * quote.fiatRate
status.copy( val value = when (quote) {
value = CryptoCurrencyStatus.Loaded( is Quote.Empty -> CryptoCurrencyStatus.NoQuote(
amount = status.value.amount!!,
pendingTransactions = emptySet(),
hasCurrentNetworkTransactions = false,
networkAddress = requireNotNull(
value = MockNetworks.verifiedNetworksStatuses.first { it.network == status.currency.network }.value as? NetworkStatus.Verified,
).address,
yieldBalance = null,
)
is Quote.Value -> CryptoCurrencyStatus.Loaded(
amount = amount, amount = amount,
fiatAmount = fiatAmount, fiatAmount = amount * quote.fiatRate,
fiatRate = quote.fiatRate, fiatRate = quote.fiatRate,
priceChange = quote.priceChange, priceChange = quote.priceChange,
pendingTransactions = emptySet(), pendingTransactions = emptySet(),
hasCurrentNetworkTransactions = false, hasCurrentNetworkTransactions = false,
networkAddress = requireNotNull(networkStatus.value as? NetworkStatus.Verified).address, networkAddress = requireNotNull(networkStatus.value as? NetworkStatus.Verified).address,
yieldBalance = null, yieldBalance = null,
), )
) }
status.copy(value = value)
} }
val noQuotesTokensStatuses = loadedTokensStates.map { status -> val noQuotesTokensStatuses = loadedTokensStates.map { status ->

View file

@ -2178,8 +2178,8 @@ internal class SwapInteractorImpl @AssistedInject constructor(
) )
} }
private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map<CryptoCurrency.ID, Quote> { private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map<CryptoCurrency.ID, Quote.Value> {
val set = ids.toSet().getQuotesOrEmpty(false) val set = ids.toSet().getQuotesOrEmpty(false).filterIsInstance<Quote.Value>()
return ids return ids
.mapNotNull { id -> set.find { it.rawCurrencyId == id.rawCurrencyId }?.let { id to it } } .mapNotNull { id -> set.find { it.rawCurrencyId == id.rawCurrencyId }?.let { id to it } }

View file

@ -3,9 +3,9 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
import com.tangem.core.ui.utils.toTimeFormat import com.tangem.core.ui.utils.toTimeFormat
@ -55,16 +55,22 @@ internal class TokenDetailsSwapTransactionsStateConverter(
.forEach { swapTransaction -> .forEach { swapTransaction ->
val toCryptoCurrency = swapTransaction.toCryptoCurrency val toCryptoCurrency = swapTransaction.toCryptoCurrency
val fromCryptoCurrency = swapTransaction.fromCryptoCurrency val fromCryptoCurrency = swapTransaction.fromCryptoCurrency
val toCryptoCurrencyRawId = swapTransaction.toCryptoCurrency.id.rawCurrencyId
val fromCryptoCurrencyRawId = swapTransaction.fromCryptoCurrency.id.rawCurrencyId
swapTransaction.transactions.forEach { transaction -> swapTransaction.transactions.forEach { transaction ->
val toAmount = transaction.toCryptoAmount val toAmount = transaction.toCryptoAmount
val fromAmount = transaction.fromCryptoAmount val fromAmount = transaction.fromCryptoAmount
val toFiatAmount = quotes.firstOrNull { var toFiatAmount: BigDecimal? = null
it.rawCurrencyId == swapTransaction.toCryptoCurrency.id.rawCurrencyId var fromFiatAmount: BigDecimal? = null
}?.fiatRate?.multiply(toAmount) quotes.forEach { quote ->
val fromFiatAmount = quotes.firstOrNull { if (quote is Quote.Value && quote.rawCurrencyId == toCryptoCurrencyRawId) {
it.rawCurrencyId == swapTransaction.fromCryptoCurrency.id.rawCurrencyId toFiatAmount = quote.fiatRate.multiply(toAmount)
}?.fiatRate?.multiply(fromAmount) }
if (quote is Quote.Value && quote.rawCurrencyId == fromCryptoCurrencyRawId) {
fromFiatAmount = quote.fiatRate.multiply(fromAmount)
}
}
val timestamp = transaction.timestamp val timestamp = transaction.timestamp
val notifications = val notifications =
getNotification(transaction.status?.status, transaction.status?.txExternalUrl, null) getNotification(transaction.status?.status, transaction.status?.txExternalUrl, null)