diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt index 13e1c3e368..a680263464 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt @@ -45,7 +45,7 @@ internal class DefaultQuotesRepository( .filterNotNull() .flatMapLatest { appCurrency -> fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = refresh) - quotesStore.get(currenciesIds).map(quotesConverter::convertSet) + quotesStore.get(currenciesIds).map { quotesConverter.convert(currenciesIds to it) } } .cancellable() .flowOn(dispatchers.io) @@ -77,14 +77,15 @@ internal class DefaultQuotesRepository( val quotes = quotesStore.getSync(currenciesIds) - quotesConverter.convertSet(quotes) + quotesConverter.convert(currenciesIds to quotes) } } override suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote? { return withContext(dispatchers.io) { - val quote = quotesStore.getSync(setOf(currencyId)).firstOrNull() - quote?.let { quotesConverter.convert(it) } + val setOfCurrencyId = setOf(currencyId) + val quote = quotesStore.getSync(setOfCurrencyId).firstOrNull() + quote?.let { quotesConverter.convert(setOfCurrencyId to setOf(it)).firstOrNull() } } } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt index 85c903ceec..79af4417b7 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt @@ -1,16 +1,28 @@ package com.tangem.data.tokens.utils 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.utils.converter.Converter import java.math.BigDecimal -internal class QuotesConverter : Converter { +typealias QuotesConverterValue = Pair, Set> - override fun convert(value: StoredQuote): Quote { - val (rawCurrencyId, responseQuote) = value +internal class QuotesConverter : Converter> { - return Quote( + override fun convert(value: QuotesConverterValue): Set { + 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, fiatRate = responseQuote.price ?: BigDecimal.ZERO, priceChange = (responseQuote.priceChange24h ?: BigDecimal.ZERO).movePointLeft(2), diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt index 1ec184fbdc..0a8d0d931d 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt @@ -15,10 +15,10 @@ class GetCurrencyQuotesUseCase( currencyID: CryptoCurrency.ID, interval: PriceChangeInterval, refresh: Boolean, - ): Flow> { + ): Flow> { return quotesRepository.getQuotesUpdates( currenciesIds = setOf(currencyID), refresh = refresh, - ).map { it.firstOrNull().toOption() }.catch { emit(None) } + ).map { it.filterIsInstance().firstOrNull().toOption() }.catch { emit(None) } } } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt index 3db9280b76..d54b9a2342 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt @@ -2,15 +2,27 @@ package com.tangem.domain.tokens.model import java.math.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 Quote( - val rawCurrencyId: String, - val fiatRate: BigDecimal, - val priceChange: BigDecimal, -) \ No newline at end of file +sealed interface Quote { + + val rawCurrencyId: String? + + /** + * Represents unknown financial information for a specific cryptocurrency. + * + * @property rawCurrencyId The raw cryptocurrency ID. If it is a custom token, the value will be `null`. + */ + data class Empty(override val rawCurrencyId: String?) : Quote + + /** + * 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 +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index ff31ccc64a..3e4f3a77ae 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -12,6 +12,18 @@ internal class CurrencyStatusOperations( 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()) private fun createStatus(): CryptoCurrencyStatus.Value { @@ -74,7 +86,7 @@ internal class CurrencyStatusOperations( null } return when { - ignoreQuote -> CryptoCurrencyStatus.NoQuote( + quote is Quote.Empty || ignoreQuote -> CryptoCurrencyStatus.NoQuote( amount = amount, hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, @@ -91,8 +103,7 @@ internal class CurrencyStatusOperations( networkAddress = status.address, yieldBalance = currentYieldBalance, ) - quote == null -> CryptoCurrencyStatus.Loading - else -> CryptoCurrencyStatus.Loaded( + quote is Quote.Value -> CryptoCurrencyStatus.Loaded( amount = amount, fiatAmount = calculateFiatAmount(amount, quote.fiatRate), fiatRate = quote.fiatRate, @@ -102,6 +113,7 @@ internal class CurrencyStatusOperations( networkAddress = status.address, yieldBalance = currentYieldBalance, ) + else -> CryptoCurrencyStatus.Loading } } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt index 68de5e729a..002f80eae8 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt @@ -7,65 +7,71 @@ import java.math.BigDecimal @Suppress("MemberVisibilityCanBePrivate") internal object MockQuotes { - val quote1 = Quote( + val quote1 = Quote.Value( rawCurrencyId = MockTokens.token1.id.rawCurrencyId!!, fiatRate = BigDecimal("1.23"), priceChange = BigDecimal("0.01"), ) - val quote2 = Quote( + val quote2 = Quote.Value( rawCurrencyId = MockTokens.token2.id.rawCurrencyId!!, fiatRate = BigDecimal("2.34"), priceChange = BigDecimal("-0.02"), ) - val quote3 = Quote( + val quote3 = Quote.Value( rawCurrencyId = MockTokens.token3.id.rawCurrencyId!!, fiatRate = BigDecimal("3.45"), priceChange = BigDecimal("0.03"), ) - val quote4 = Quote( + val quote4 = Quote.Value( rawCurrencyId = MockTokens.token4.id.rawCurrencyId!!, fiatRate = BigDecimal("4.56"), priceChange = BigDecimal("-0.04"), ) - val quote5 = Quote( + val quote5 = Quote.Value( rawCurrencyId = MockTokens.token5.id.rawCurrencyId!!, fiatRate = BigDecimal("5.67"), priceChange = BigDecimal("0.05"), ) - val quote6 = Quote( + val quote6 = Quote.Value( rawCurrencyId = MockTokens.token6.id.rawCurrencyId!!, fiatRate = BigDecimal("6.78"), priceChange = BigDecimal("-0.06"), ) - val quote7 = Quote( + val quote7 = Quote.Value( rawCurrencyId = MockTokens.token7.id.rawCurrencyId!!, fiatRate = BigDecimal("7.89"), priceChange = BigDecimal("0.07"), ) - val quote8 = Quote( + val quote8 = Quote.Value( rawCurrencyId = MockTokens.token8.id.rawCurrencyId!!, fiatRate = BigDecimal("8.90"), priceChange = BigDecimal("-0.08"), ) - val quote9 = Quote( + val quote9 = Quote.Value( rawCurrencyId = MockTokens.token9.id.rawCurrencyId!!, fiatRate = BigDecimal("9.01"), priceChange = BigDecimal("0.09"), ) - val quote10 = Quote( + val quote10 = Quote.Value( rawCurrencyId = MockTokens.token10.id.rawCurrencyId!!, fiatRate = BigDecimal("10.12"), 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, + ) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index 45c2f537d4..a9d704b56f 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -1,10 +1,7 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptyListOf -import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.NetworkAddress -import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.* import java.math.BigDecimal @Suppress("MemberVisibilityCanBePrivate") @@ -140,20 +137,29 @@ internal object MockTokensStates { as? CryptoCurrencyAmountStatus.Loaded )?.value ?: BigDecimal.ZERO val quote = MockQuotes.quotes.first { it.rawCurrencyId == status.currency.id.rawCurrencyId } - val fiatAmount = amount * quote.fiatRate - status.copy( - value = CryptoCurrencyStatus.Loaded( + val value = when (quote) { + 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, - fiatAmount = fiatAmount, + fiatAmount = amount * quote.fiatRate, fiatRate = quote.fiatRate, priceChange = quote.priceChange, pendingTransactions = emptySet(), hasCurrentNetworkTransactions = false, networkAddress = requireNotNull(networkStatus.value as? NetworkStatus.Verified).address, yieldBalance = null, - ), - ) + ) + } + status.copy(value = value) } val noQuotesTokensStatuses = loadedTokensStates.map { status -> diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 1d5e225d91..63b4b823e7 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -2178,8 +2178,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } - private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map { - val set = ids.toSet().getQuotesOrEmpty(false) + private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map { + val set = ids.toSet().getQuotesOrEmpty(false).filterIsInstance() return ids .mapNotNull { id -> set.find { it.rawCurrencyId == id.rawCurrencyId }?.let { id to it } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index 2ec87f5ed8..fc0cfa0541 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -3,9 +3,9 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter 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.format -import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday import com.tangem.core.ui.utils.toTimeFormat @@ -55,16 +55,22 @@ internal class TokenDetailsSwapTransactionsStateConverter( .forEach { swapTransaction -> val toCryptoCurrency = swapTransaction.toCryptoCurrency val fromCryptoCurrency = swapTransaction.fromCryptoCurrency + val toCryptoCurrencyRawId = swapTransaction.toCryptoCurrency.id.rawCurrencyId + val fromCryptoCurrencyRawId = swapTransaction.fromCryptoCurrency.id.rawCurrencyId swapTransaction.transactions.forEach { transaction -> val toAmount = transaction.toCryptoAmount val fromAmount = transaction.fromCryptoAmount - val toFiatAmount = quotes.firstOrNull { - it.rawCurrencyId == swapTransaction.toCryptoCurrency.id.rawCurrencyId - }?.fiatRate?.multiply(toAmount) - val fromFiatAmount = quotes.firstOrNull { - it.rawCurrencyId == swapTransaction.fromCryptoCurrency.id.rawCurrencyId - }?.fiatRate?.multiply(fromAmount) + var toFiatAmount: BigDecimal? = null + var fromFiatAmount: BigDecimal? = null + quotes.forEach { quote -> + if (quote is Quote.Value && quote.rawCurrencyId == toCryptoCurrencyRawId) { + toFiatAmount = quote.fiatRate.multiply(toAmount) + } + if (quote is Quote.Value && quote.rawCurrencyId == fromCryptoCurrencyRawId) { + fromFiatAmount = quote.fiatRate.multiply(fromAmount) + } + } val timestamp = transaction.timestamp val notifications = getNotification(transaction.status?.status, transaction.status?.txExternalUrl, null)