Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-27 08:24:24 +05:00
parent 9ee1319e40
commit c6b2e89890
20 changed files with 142 additions and 4 deletions

View file

@ -14,6 +14,7 @@ object MockQuoteResponseFactory {
priceChange24h = value,
priceChange1w = value,
priceChange30d = value,
priceUsd = value,
)
}
}

View file

@ -13,6 +13,7 @@ fun QuotesResponse.Quote.toDomain(rawCurrencyId: String, source: StatusSource =
source = source,
fiatRate = price.orZero(),
priceChange = priceChange24h.orZero().movePointLeft(2),
fiatRateUSD = priceUsd.orZero(),
),
)
}

View file

@ -20,6 +20,8 @@ data class QuotesResponse(
val priceChange1w: BigDecimal?,
@Json(name = "priceChange30d")
val priceChange30d: BigDecimal?,
@Json(name = "priceUsd")
val priceUsd: BigDecimal?,
) {
companion object {
@ -29,6 +31,7 @@ data class QuotesResponse(
priceChange24h = null,
priceChange1w = null,
priceChange30d = null,
priceUsd = null,
)
}
}

View file

@ -43,8 +43,9 @@ interface QuotesFetcher {
PRICE_CHANGE_24H(value = "priceChange24h"),
PRICE_CHANGE_1W(value = "priceChange1w"),
PRICE_CHANGE_30D(value = "priceChange30d"),
PRICE_USD(value = "priceUsd"),
ALL_PRICES(
value = setOf(PRICE, PRICE_CHANGE_24H, PRICE_CHANGE_1W, PRICE_CHANGE_30D).combine(),
value = setOf(PRICE, PRICE_CHANGE_24H, PRICE_CHANGE_1W, PRICE_CHANGE_30D, PRICE_USD).combine(),
),
LAST_UPDATED_AT(value = "lastUpdatedAt"),
}

View file

@ -95,6 +95,7 @@ internal class HotCryptoCurrencyConverter(
rawCurrencyId = rawCurrencyId,
value = QuoteStatus.Data(
fiatRate = fiatRate,
fiatRateUSD = BigDecimal.ZERO,
priceChange = priceChange.movePointLeft(2),
source = StatusSource.ACTUAL, // It doesn't matter
),

View file

@ -27,6 +27,7 @@ internal class QuoteStatusConverter(
source = source,
fiatRate = quote.price.orZero(),
priceChange = quote.priceChange24h.orZero().movePointLeft(2),
fiatRateUSD = quote.priceUsd.orZero(),
),
)
}

View file

@ -14,10 +14,11 @@ import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.mapWithStringKeyTypes
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.quotes.GetCurrencyUSDQuoteUseCase
import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteUpdater
import com.tangem.utils.coroutines.AppCoroutineScope
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -72,4 +73,10 @@ internal object QuotesDataModule {
coroutineScope = coroutineScope,
)
}
@Singleton
@Provides
fun provideGetCurrencyUSDQuoteUseCase(quotesRepository: QuotesRepository): GetCurrencyUSDQuoteUseCase {
return GetCurrencyUSDQuoteUseCase(quotesRepository)
}
}

View file

@ -55,7 +55,7 @@ internal class DefaultMultiQuoteStatusFetcher @Inject constructor(
val response = quotesFetcher.fetch(
fiatCurrencyId = appCurrencyId,
currenciesIds = replacementIdsResult.idsForRequest,
fields = setOf(Field.PRICE, Field.PRICE_CHANGE_24H),
fields = setOf(Field.PRICE, Field.PRICE_CHANGE_24H, Field.PRICE_USD),
)
.getOrElse { error("Cause: $it") }

View file

@ -31,4 +31,8 @@ internal class DefaultQuotesRepository(
?: QuoteStatus(rawCurrencyId = currencyId)
}
}
override suspend fun getCurrencyUSDQuote(currencyId: CryptoCurrency.RawID): QuoteStatus? {
return getMultiQuoteSyncOrNull(currenciesIds = setOf(currencyId)).firstOrNull()
}
}

View file

@ -55,6 +55,7 @@ internal class QuoteStatusConverterTest {
priceChange24h = null,
priceChange1w = null,
priceChange30d = null,
priceUsd = null,
),
),
expected = QuoteStatus(
@ -62,6 +63,7 @@ internal class QuoteStatusConverterTest {
value = QuoteStatus.Data(
source = StatusSource.ACTUAL,
fiatRate = BigDecimal.ZERO,
fiatRateUSD = BigDecimal.ZERO,
priceChange = BigDecimal("0.00"),
),
),
@ -74,6 +76,7 @@ internal class QuoteStatusConverterTest {
priceChange24h = null,
priceChange1w = BigDecimal.ZERO,
priceChange30d = BigDecimal.ZERO,
priceUsd = null,
),
),
expected = QuoteStatus(
@ -81,6 +84,7 @@ internal class QuoteStatusConverterTest {
value = QuoteStatus.Data(
source = StatusSource.ACTUAL,
fiatRate = BigDecimal.ZERO,
fiatRateUSD = BigDecimal.ZERO,
priceChange = BigDecimal("0.00"),
),
),
@ -93,6 +97,7 @@ internal class QuoteStatusConverterTest {
priceChange24h = BigDecimal.ONE,
priceChange1w = null,
priceChange30d = null,
priceUsd = BigDecimal.ONE
),
),
expected = QuoteStatus(
@ -100,6 +105,7 @@ internal class QuoteStatusConverterTest {
value = QuoteStatus.Data(
source = StatusSource.ACTUAL,
fiatRate = BigDecimal.ONE,
fiatRateUSD = BigDecimal.ONE,
priceChange = BigDecimal("0.01"),
),
),

View file

@ -240,6 +240,6 @@ internal class DefaultMultiQuoteStatusFetcherTest {
),
)
val fields = setOf(QuotesFetcher.Field.PRICE, QuotesFetcher.Field.PRICE_CHANGE_24H)
val fields = setOf(QuotesFetcher.Field.PRICE, QuotesFetcher.Field.PRICE_CHANGE_24H, QuotesFetcher.Field.PRICE_USD)
}
}

View file

@ -43,6 +43,7 @@ internal class DefaultQuotesRepositoryTest {
value = QuoteStatus.Data(
source = StatusSource.ACTUAL,
fiatRate = BigDecimal.ZERO,
fiatRateUSD = BigDecimal.ZERO,
priceChange = BigDecimal.ZERO,
),
)
@ -100,9 +101,75 @@ internal class DefaultQuotesRepositoryTest {
)
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetCurrencyUSDQuote {
private val btcRawId = CryptoCurrency.RawID(value = "BTC")
private val ethRawId = CryptoCurrency.RawID(value = "ETH")
private val ethQuote = QuoteStatus(
rawCurrencyId = ethRawId,
value = QuoteStatus.Data(
source = StatusSource.ACTUAL,
fiatRate = BigDecimal.ZERO,
fiatRateUSD = BigDecimal.ZERO,
priceChange = BigDecimal.ZERO,
),
)
@ParameterizedTest
@ProvideTestModels
fun getCurrencyUSDQuote(model: GetCurrencyUSDQuoteModel) = runTest {
// Arrange
coEvery { quotesStatusesStore.getAllSyncOrNull() } returns model.initialStore
// Act
val actual = repository.getCurrencyUSDQuote(currencyId = model.currencyId)
// Assert
val expected = model.expected
Truth.assertThat(actual).isEqualTo(expected)
}
private fun provideTestModels() = listOf(
GetCurrencyUSDQuoteModel(
initialStore = null,
currencyId = ethRawId,
expected = QuoteStatus(rawCurrencyId = ethRawId),
),
GetCurrencyUSDQuoteModel(
initialStore = emptySet(),
currencyId = ethRawId,
expected = QuoteStatus(rawCurrencyId = ethRawId),
),
GetCurrencyUSDQuoteModel(
initialStore = setOf(ethQuote),
currencyId = ethRawId,
expected = ethQuote,
),
GetCurrencyUSDQuoteModel(
initialStore = setOf(QuoteStatus(rawCurrencyId = btcRawId)),
currencyId = ethRawId,
expected = QuoteStatus(rawCurrencyId = ethRawId),
),
GetCurrencyUSDQuoteModel(
initialStore = setOf(ethQuote, QuoteStatus(rawCurrencyId = btcRawId)),
currencyId = ethRawId,
expected = ethQuote,
),
)
}
data class GetMultiQuoteSyncOrNullModel(
val initialStore: Set<QuoteStatus>?,
val currencyIds: Set<CryptoCurrency.RawID>,
val expected: Set<QuoteStatus>?,
)
data class GetCurrencyUSDQuoteModel(
val initialStore: Set<QuoteStatus>?,
val currencyId: CryptoCurrency.RawID,
val expected: QuoteStatus?,
)
}

View file

@ -83,6 +83,7 @@ internal class DefaultSingleQuoteStatusProducerTest {
value = QuoteStatus.Data(
fiatRate = BigDecimal.ONE,
priceChange = BigDecimal.ZERO,
fiatRateUSD = BigDecimal.ZERO,
source = StatusSource.ACTUAL,
),
)
@ -129,6 +130,7 @@ internal class DefaultSingleQuoteStatusProducerTest {
rawCurrencyId = params.rawCurrencyId,
value = QuoteStatus.Data(
fiatRate = BigDecimal.ONE,
fiatRateUSD = BigDecimal.ZERO,
priceChange = BigDecimal.ZERO,
source = StatusSource.ACTUAL,
),

View file

@ -40,11 +40,13 @@ data class QuoteStatus(val rawCurrencyId: CryptoCurrency.RawID, val value: Value
*
* @property source status source
* @property fiatRate the current fiat exchange rate for the cryptocurrency
* @property fiatRateUSD the current fiat exchange rate in USD for the cryptocurrency
* @property priceChange the price change for the cryptocurrency
*/
data class Data(
override val source: StatusSource,
val fiatRate: BigDecimal,
val fiatRateUSD: BigDecimal,
val priceChange: BigDecimal,
) : Value
}

View file

@ -0,0 +1,24 @@
package com.tangem.domain.quotes
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.quote.QuoteStatus
import java.math.BigDecimal
/**
* Get currency USD quote use case
*/
class GetCurrencyUSDQuoteUseCase(
private val quotesRepository: QuotesRepository,
) {
/** Get quote by [currencyId] synchronously or null */
suspend operator fun invoke(currencyId: CryptoCurrency.RawID): BigDecimal? {
val value = quotesRepository.getCurrencyUSDQuote(currencyId)?.value
return if (value is QuoteStatus.Data) {
value.fiatRateUSD
} else {
null
}
}
}

View file

@ -12,4 +12,7 @@ interface QuotesRepository {
/** Get quotes by [currenciesIds] synchronously or null */
suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set<CryptoCurrency.RawID>): Set<QuoteStatus>?
/** Get quote by [currencyId] synchronously or null */
suspend fun getCurrencyUSDQuote(currencyId: CryptoCurrency.RawID): QuoteStatus?
}

View file

@ -13,6 +13,7 @@ internal object MockQuotes {
rawCurrencyId = MockTokens.token1.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatRate = BigDecimal("1.23"),
fiatRateUSD = BigDecimal("1.23"),
priceChange = BigDecimal("0.01"),
source = StatusSource.ACTUAL,
),
@ -22,6 +23,7 @@ internal object MockQuotes {
rawCurrencyId = MockTokens.token2.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatRate = BigDecimal("2.34"),
fiatRateUSD = BigDecimal("2.34"),
priceChange = BigDecimal("-0.02"),
source = StatusSource.ACTUAL,
),
@ -31,6 +33,7 @@ internal object MockQuotes {
rawCurrencyId = MockTokens.token3.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatRate = BigDecimal("3.45"),
fiatRateUSD = BigDecimal("3.45"),
priceChange = BigDecimal("0.03"),
source = StatusSource.ACTUAL,
),
@ -40,6 +43,7 @@ internal object MockQuotes {
rawCurrencyId = MockTokens.token4.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatRate = BigDecimal("4.56"),
fiatRateUSD = BigDecimal("4.56"),
priceChange = BigDecimal("-0.04"),
source = StatusSource.ACTUAL,
),
@ -49,6 +53,7 @@ internal object MockQuotes {
rawCurrencyId = MockTokens.token5.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatRate = BigDecimal("5.67"),
fiatRateUSD = BigDecimal("5.67"),
priceChange = BigDecimal("0.05"),
source = StatusSource.ACTUAL,
),
@ -58,6 +63,7 @@ internal object MockQuotes {
rawCurrencyId = MockTokens.token6.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatRate = BigDecimal("6.78"),
fiatRateUSD = BigDecimal("6.78"),
priceChange = BigDecimal("-0.06"),
source = StatusSource.ACTUAL,
),
@ -67,6 +73,7 @@ internal object MockQuotes {
rawCurrencyId = MockTokens.token7.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatRate = BigDecimal("7.89"),
fiatRateUSD = BigDecimal("7.89"),
priceChange = BigDecimal("0.07"),
source = StatusSource.ACTUAL,
),
@ -76,6 +83,7 @@ internal object MockQuotes {
rawCurrencyId = MockTokens.token8.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatRate = BigDecimal("8.90"),
fiatRateUSD = BigDecimal("8.90"),
priceChange = BigDecimal("-0.08"),
source = StatusSource.ACTUAL,
),
@ -85,6 +93,7 @@ internal object MockQuotes {
rawCurrencyId = MockTokens.token9.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatRate = BigDecimal("9.01"),
fiatRateUSD = BigDecimal("9.01"),
priceChange = BigDecimal("0.09"),
source = StatusSource.ACTUAL,
),
@ -94,6 +103,7 @@ internal object MockQuotes {
rawCurrencyId = MockTokens.token10.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatRate = BigDecimal("10.12"),
fiatRateUSD = BigDecimal("10.12"),
priceChange = BigDecimal("-0.10"),
source = StatusSource.ACTUAL,
),

View file

@ -44,6 +44,7 @@ class CryptoCurrencyStatusFactoryTest {
private val fullQuote = QuoteStatus.Data(
fiatRate = 1800.0.toBigDecimal(),
fiatRateUSD = 1800.0.toBigDecimal(),
priceChange = (-2.5).toBigDecimal(),
source = StatusSource.ACTUAL,
)

View file

@ -83,6 +83,7 @@ class YieldSupplyMinAmountUseCaseTest {
value = QuoteStatus.Data(
source = StatusSource.ACTUAL,
fiatRate = nativeFiatRate,
fiatRateUSD = nativeFiatRate,
priceChange = BigDecimal("0.09000000000000007"),
),
),

View file

@ -79,6 +79,7 @@ class YieldSupplyGetCurrentFeeUseCaseTest {
value = QuoteStatus.Data(
source = StatusSource.ACTUAL,
fiatRate = nativeFiatRate,
fiatRateUSD = nativeFiatRate,
priceChange = BigDecimal.ZERO,
),
),
@ -130,6 +131,7 @@ class YieldSupplyGetCurrentFeeUseCaseTest {
value = QuoteStatus.Data(
source = StatusSource.ACTUAL,
fiatRate = nativeFiatRate,
fiatRateUSD = nativeFiatRate,
priceChange = BigDecimal.ZERO,
),
),
@ -256,6 +258,7 @@ class YieldSupplyGetCurrentFeeUseCaseTest {
value = QuoteStatus.Data(
source = StatusSource.ACTUAL,
fiatRate = BigDecimal.ZERO, // non-positive
fiatRateUSD = BigDecimal.ZERO,
priceChange = BigDecimal.ZERO,
),
),