Updated on 2026-08-14
This commit is contained in:
parent
b9bb9e1086
commit
058a6ab4d5
15 changed files with 969 additions and 256 deletions
|
|
@ -33,4 +33,5 @@ dependencies {
|
|||
implementation(tangemDeps.card.core)
|
||||
|
||||
implementation(deps.test.junit5)
|
||||
implementation(deps.test.truth)
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.common.test.utils
|
||||
|
||||
import arrow.core.Either
|
||||
import com.google.common.truth.Truth
|
||||
|
||||
fun <B> assertEither(actual: Either<Throwable, B>, expected: Either<Throwable, B>) {
|
||||
actual
|
||||
.onRight { Truth.assertThat(it).isEqualTo(expected) }
|
||||
.onLeft {
|
||||
val expectedError = expected.leftOrNull() ?: error("Expected must be Either.Left")
|
||||
|
||||
Truth.assertThat(it::class.java).isEqualTo(expectedError::class.java)
|
||||
}
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ interface TangemTechApi {
|
|||
suspend fun getQuotes(
|
||||
@Query("currencyId") currencyId: String,
|
||||
@Query("coinIds") coinIds: String,
|
||||
@Query("fields") fields: String = "price,priceChange24h,lastUpdatedAt",
|
||||
@Query("fields") fields: String,
|
||||
): ApiResponse<QuotesResponse>
|
||||
|
||||
@GET("promotion")
|
||||
|
|
@ -176,13 +176,4 @@ interface TangemTechApi {
|
|||
@GET("user-wallets/wallets/by-app/{app_id}")
|
||||
suspend fun getWallets(@Path("app_id") appId: String): ApiResponse<List<WalletResponse>>
|
||||
// endregion
|
||||
|
||||
companion object {
|
||||
val marketsQuoteFields = listOf(
|
||||
"price",
|
||||
"priceChange24h",
|
||||
"priceChange1w",
|
||||
"priceChange30d",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ import com.tangem.data.common.currency.CardCryptoCurrencyFactory
|
|||
import com.tangem.data.common.currency.DefaultCardCryptoCurrencyFactory
|
||||
import com.tangem.data.common.currency.UserTokensResponseAddressesEnricher
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.data.common.quote.DefaultQuotesFetcher
|
||||
import com.tangem.data.common.quote.QuotesFetcher
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
|
|
@ -70,4 +72,10 @@ internal object DataCommonModule {
|
|||
userTokensResponseAddressesEnricher = enricher,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideQuotesFetcher(tangemTechApi: TangemTechApi, dispatchers: CoroutineDispatcherProvider): QuotesFetcher {
|
||||
return DefaultQuotesFetcher(tangemTechApi = tangemTechApi, dispatchers = dispatchers)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
package com.tangem.data.common.quote
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.Raise
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.ensure
|
||||
import arrow.core.raise.ensureNotNull
|
||||
import com.tangem.data.common.api.safeApiCallWithTimeout
|
||||
import com.tangem.data.common.quote.QuotesFetcher.Error
|
||||
import com.tangem.data.common.quote.QuotesFetcher.Field
|
||||
import com.tangem.data.common.quote.utils.combine
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||
import com.tangem.domain.core.utils.eitherOn
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.joda.time.DateTime
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Default implementation of [QuotesFetcher]
|
||||
*
|
||||
* @property tangemTechApi Tangem tech API
|
||||
* @property dispatchers dispatchers
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultQuotesFetcher(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : QuotesFetcher {
|
||||
|
||||
/** Mutex for synchronization of the [getFetchMutex] method */
|
||||
private val getFetchMethodMutex = Mutex()
|
||||
|
||||
/**
|
||||
* Map for tracking active [fetch] methods.
|
||||
* The key is the parameters of the [fetch] method, and the value is [Mutex].
|
||||
*/
|
||||
private val fetchMutexMap = ConcurrentHashMap<RequestParams, Mutex>()
|
||||
|
||||
/**
|
||||
* Cache that stores a list of quotes for a certain fiat currency.
|
||||
* Quotes are considered expired if they are in the cache for more than [tenSecInMillis] seconds.
|
||||
*/
|
||||
private val quotesCache = ConcurrentHashMap<String, Set<QuoteMetadata>>()
|
||||
|
||||
override suspend fun fetch(
|
||||
fiatCurrencyId: String,
|
||||
currenciesIds: Set<String>,
|
||||
fields: Set<Field>,
|
||||
): Either<Error, QuotesResponse> = eitherOn(dispatchers.default) {
|
||||
val validatedParams = validateParams(fiatCurrencyId, currenciesIds, fields)
|
||||
|
||||
if (validatedParams.currenciesIds.isEmpty()) return@eitherOn emptyQuotesResponse
|
||||
|
||||
val mutex = getFetchMutex(params = validatedParams)
|
||||
|
||||
return@eitherOn fetch(params = validatedParams, mutex = mutex)
|
||||
}
|
||||
.onLeft {
|
||||
val params = RequestParams(fiatCurrencyId, currenciesIds, fields)
|
||||
fetchMutexMap.remove(params)
|
||||
}
|
||||
|
||||
private fun Raise<Error>.validateParams(
|
||||
fiatCurrencyId: String,
|
||||
currenciesIds: Set<String>,
|
||||
fields: Set<Field>,
|
||||
): RequestParams {
|
||||
ensure(fiatCurrencyId.isNotBlank() && fields.isNotEmpty()) {
|
||||
raise(Error.InvalidArgumentsError)
|
||||
}
|
||||
|
||||
val filterCurrenciesIds = currenciesIds.filter(String::isNotEmpty).toSet()
|
||||
|
||||
return RequestParams(fiatCurrencyId = fiatCurrencyId, currenciesIds = filterCurrenciesIds, fields = fields)
|
||||
}
|
||||
|
||||
/**
|
||||
* The method for determining the [Mutex], which will be used by the [fetch] method.
|
||||
*
|
||||
* @param params request params
|
||||
*
|
||||
* @return if at the moment the method of [fetch] is already executed for an adjacent set of parameters
|
||||
|
||||
*/
|
||||
private suspend fun Raise<Error>.getFetchMutex(params: RequestParams): Mutex {
|
||||
return getFetchMethodMutex.withLock {
|
||||
val similarJobsMutexes = fetchMutexMap.filterKeys { metadata ->
|
||||
metadata.fiatCurrencyId == params.fiatCurrencyId &&
|
||||
params.currenciesIds.any { it in metadata.currenciesIds }
|
||||
}
|
||||
|
||||
val storedMutex = similarJobsMutexes.firstNotNullOfOrNull { it.value }
|
||||
|
||||
when {
|
||||
storedMutex == null || !storedMutex.isLocked -> Mutex()
|
||||
storedMutex.isLocked -> storedMutex
|
||||
else -> raise(Error.CacheOperationError)
|
||||
}
|
||||
.also { fetchMutexMap[params] = it }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch quotes by [params].
|
||||
*
|
||||
* It works in the scope of the transferred [mutex].
|
||||
* If at the moment [Mutex] is busy, then it is working on fetching related data, then the current request will
|
||||
* wait for its execution.
|
||||
*
|
||||
* @return [QuotesResponse]
|
||||
*/
|
||||
private suspend fun Raise<Error>.fetch(params: RequestParams, mutex: Mutex): QuotesResponse {
|
||||
val (fiatCurrencyId, currenciesIds) = params
|
||||
|
||||
return mutex.withLock {
|
||||
val quotes = quotesCache[fiatCurrencyId].orEmpty()
|
||||
|
||||
val expiredOrAbsentIds = currenciesIds.filterExpiredOrAbsent(quotes)
|
||||
|
||||
// We will fetch quotes only for those quotes that are absent in cache or expired
|
||||
if (expiredOrAbsentIds.isNotEmpty()) {
|
||||
val response = requestQuotes(
|
||||
fiatCurrencyId = fiatCurrencyId,
|
||||
currenciesIds = expiredOrAbsentIds.toSet(),
|
||||
fields = params.fields,
|
||||
)
|
||||
|
||||
saveQuotes(fiatCurrencyId = fiatCurrencyId, response = response)
|
||||
}
|
||||
|
||||
fetchMutexMap.remove(params)
|
||||
|
||||
getCachedResult(fiatCurrencyId = fiatCurrencyId, currenciesIds = currenciesIds)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Set<String>.filterExpiredOrAbsent(quotes: Set<QuoteMetadata>?): List<String> {
|
||||
return filter { id ->
|
||||
val quote = quotes?.firstOrNull { it.cryptoCurrencyId == id && !it.isExpired }
|
||||
|
||||
quote == null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Raise<Error>.getCachedResult(fiatCurrencyId: String, currenciesIds: Set<String>): QuotesResponse {
|
||||
val storedQuotes = quotesCache[fiatCurrencyId]
|
||||
|
||||
ensureNotNull(storedQuotes) {
|
||||
raise(Error.CacheOperationError)
|
||||
}
|
||||
|
||||
val quotes = catch(
|
||||
block = {
|
||||
currenciesIds.associateWith { currencyId ->
|
||||
storedQuotes.first { it.cryptoCurrencyId == currencyId }.value
|
||||
}
|
||||
},
|
||||
catch = { raise(Error.CacheOperationError) },
|
||||
)
|
||||
|
||||
return QuotesResponse(quotes = quotes)
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.requestQuotes(
|
||||
fiatCurrencyId: String,
|
||||
currenciesIds: Set<String>,
|
||||
fields: Set<Field>,
|
||||
) = withContext(dispatchers.io) {
|
||||
safeApiCallWithTimeout(
|
||||
call = {
|
||||
tangemTechApi.getQuotes(
|
||||
currencyId = fiatCurrencyId,
|
||||
coinIds = currenciesIds.joinToString(separator = ","),
|
||||
fields = fields.combine(),
|
||||
)
|
||||
.bind()
|
||||
},
|
||||
onError = { raise(Error.ApiOperationError(it)) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveQuotes(fiatCurrencyId: String, response: QuotesResponse) {
|
||||
val newQuotes = response.quotes.mapTo(destination = hashSetOf()) { (currencyId, quote) ->
|
||||
QuoteMetadata(
|
||||
cryptoCurrencyId = currencyId,
|
||||
timestamp = DateTime.now().millis,
|
||||
value = quote,
|
||||
)
|
||||
}
|
||||
|
||||
val storedQuotes = quotesCache[fiatCurrencyId].orEmpty()
|
||||
|
||||
val storedUniqueQuotes = storedQuotes.filterNot { stored ->
|
||||
newQuotes.any { stored.cryptoCurrencyId == it.cryptoCurrencyId }
|
||||
}
|
||||
|
||||
quotesCache[fiatCurrencyId] = (storedUniqueQuotes + newQuotes).toSet()
|
||||
}
|
||||
|
||||
data class RequestParams(
|
||||
val fiatCurrencyId: String,
|
||||
val currenciesIds: Set<String>,
|
||||
val fields: Set<Field>,
|
||||
)
|
||||
|
||||
data class QuoteMetadata(
|
||||
val cryptoCurrencyId: String,
|
||||
val timestamp: Long,
|
||||
val value: QuotesResponse.Quote,
|
||||
) {
|
||||
|
||||
val isExpired: Boolean
|
||||
get() = DateTime.now().millis - timestamp > tenSecInMillis
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun getCachedQuotes() = quotesCache
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun setCachedQuotes(fiatCurrencyId: String, quotes: Set<QuoteMetadata>) {
|
||||
quotesCache[fiatCurrencyId] = quotes
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun clearCache() {
|
||||
quotesCache.clear()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val emptyQuotesResponse = QuotesResponse(quotes = emptyMap())
|
||||
val tenSecInMillis = 10.seconds.inWholeMilliseconds
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.tangem.data.common.quote
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.data.common.quote.utils.combine
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||
|
||||
/**
|
||||
* Fetcher of quotes
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*
|
||||
* @see <a href = "https://www.notion.so/tangem/Quotes-21b5d34eb6788038af9ccee37c7db7f9">Documentation<a/>
|
||||
*/
|
||||
interface QuotesFetcher {
|
||||
|
||||
/**
|
||||
* Fetch
|
||||
*
|
||||
* @param fiatCurrencyId fiat currency id
|
||||
* @param currenciesIds crypto currencies ids
|
||||
* @param fields fields of [QuotesResponse.Quote]
|
||||
*/
|
||||
suspend fun fetch(
|
||||
fiatCurrencyId: String,
|
||||
currenciesIds: Set<String>,
|
||||
fields: Set<Field>,
|
||||
): Either<Error, QuotesResponse>
|
||||
|
||||
/**
|
||||
* Fetch
|
||||
*
|
||||
* @param fiatCurrencyId fiat currency id
|
||||
* @param currencyId crypto currencies ids
|
||||
* @param field fields of [QuotesResponse.Quote]
|
||||
*/
|
||||
suspend fun fetch(fiatCurrencyId: String, currencyId: String, field: Field): Either<Error, QuotesResponse> {
|
||||
return fetch(fiatCurrencyId = fiatCurrencyId, currenciesIds = setOf(currencyId), fields = setOf(field))
|
||||
}
|
||||
|
||||
enum class Field(internal val value: String) {
|
||||
PRICE(value = "price"),
|
||||
PRICE_CHANGE_24H(value = "priceChange24h"),
|
||||
PRICE_CHANGE_1W(value = "priceChange1w"),
|
||||
PRICE_CHANGE_30D(value = "priceChange30d"),
|
||||
ALL_PRICES(
|
||||
value = setOf(PRICE, PRICE_CHANGE_24H, PRICE_CHANGE_1W, PRICE_CHANGE_30D).combine(),
|
||||
),
|
||||
LAST_UPDATED_AT(value = "lastUpdatedAt"),
|
||||
;
|
||||
}
|
||||
|
||||
sealed interface Error {
|
||||
|
||||
data object InvalidArgumentsError : Error
|
||||
|
||||
data object CacheOperationError : Error
|
||||
|
||||
data class ApiOperationError(val apiError: ApiResponseError) : Error
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.data.common.quote.utils
|
||||
|
||||
import com.tangem.data.common.quote.QuotesFetcher
|
||||
|
||||
fun Set<QuotesFetcher.Field>.combine(): String = joinToString(separator = ",", transform = QuotesFetcher.Field::value)
|
||||
|
|
@ -0,0 +1,444 @@
|
|||
package com.tangem.data.common.quote
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.data.quote.MockQuoteResponseFactory
|
||||
import com.tangem.data.common.quote.DefaultQuotesFetcher.QuoteMetadata
|
||||
import com.tangem.data.common.quote.QuotesFetcher.Field
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.joda.time.DateTime
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultQuotesFetcherTest {
|
||||
|
||||
private val tangemTechApi = mockk<TangemTechApi>()
|
||||
private val fetcher = DefaultQuotesFetcher(
|
||||
tangemTechApi = tangemTechApi,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(tangemTechApi)
|
||||
fetcher.clearCache()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch if fiatCurrencyId is EMPTY`() = runTest {
|
||||
// Act
|
||||
val actual = fetcher.fetch(
|
||||
fiatCurrencyId = "",
|
||||
currenciesIds = setOf("ethereum"),
|
||||
fields = setOf(Field.PRICE),
|
||||
)
|
||||
|
||||
val actualCacheData = fetcher.getCachedQuotes()
|
||||
|
||||
// Assert
|
||||
val expected = QuotesFetcher.Error.InvalidArgumentsError.left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
val expectedCacheData = ConcurrentHashMap<String, Set<QuoteMetadata>>()
|
||||
Truth.assertThat(actualCacheData).isEqualTo(expectedCacheData)
|
||||
|
||||
coVerify(inverse = true) { tangemTechApi.getQuotes(currencyId = any(), coinIds = any(), fields = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch if currenciesIds is EMPTY`() = runTest {
|
||||
// Act
|
||||
val actual = fetcher.fetch(
|
||||
fiatCurrencyId = "usd",
|
||||
currenciesIds = emptySet(),
|
||||
fields = setOf(Field.PRICE),
|
||||
)
|
||||
|
||||
val actualCacheData = fetcher.getCachedQuotes()
|
||||
|
||||
// Assert
|
||||
val expected = QuotesResponse(quotes = emptyMap()).right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
val expectedCacheData = ConcurrentHashMap<String, Set<QuoteMetadata>>()
|
||||
Truth.assertThat(actualCacheData).isEqualTo(expectedCacheData)
|
||||
|
||||
coVerify(inverse = true) { tangemTechApi.getQuotes(currencyId = any(), coinIds = any(), fields = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch if fields is EMPTY`() = runTest {
|
||||
// Act
|
||||
val actual = fetcher.fetch(
|
||||
fiatCurrencyId = "usd",
|
||||
currenciesIds = setOf("ethereum"),
|
||||
fields = emptySet(),
|
||||
)
|
||||
|
||||
val actualCacheData = fetcher.getCachedQuotes()
|
||||
|
||||
// Assert
|
||||
val expected = QuotesFetcher.Error.InvalidArgumentsError.left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
val expectedCacheData = ConcurrentHashMap<String, Set<QuoteMetadata>>()
|
||||
Truth.assertThat(actualCacheData).isEqualTo(expectedCacheData)
|
||||
|
||||
coVerify(inverse = true) { tangemTechApi.getQuotes(currencyId = any(), coinIds = any(), fields = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch if all currencies ids ARE CACHED and ARE NOT EXPIRED`() = runTest {
|
||||
// Arrange
|
||||
val quote = MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO)
|
||||
|
||||
fetcher.setCachedQuotes(
|
||||
fiatCurrencyId = "usd",
|
||||
quotes = setOf(
|
||||
QuoteMetadata(
|
||||
cryptoCurrencyId = "ethereum",
|
||||
timestamp = DateTime.now().millis,
|
||||
value = quote,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = fetcher.fetch(
|
||||
fiatCurrencyId = "usd",
|
||||
currenciesIds = setOf("ethereum"),
|
||||
fields = setOf(Field.PRICE),
|
||||
)
|
||||
|
||||
val actualCacheData = fetcher.getCachedQuotes()
|
||||
|
||||
// Assert
|
||||
val expected = QuotesResponse(quotes = mapOf("ethereum" to quote)).right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
Truth.assertThat(actualCacheData.keys().toList().size).isEqualTo(1)
|
||||
Truth.assertThat(actualCacheData["usd"].toResponseQuotes()).isEqualTo(mapOf("ethereum" to quote))
|
||||
|
||||
coVerify(inverse = true) { tangemTechApi.getQuotes(currencyId = any(), coinIds = any(), fields = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch if all currencies ids ARE CACHED and EXPIRED`() = runTest {
|
||||
// Arrange
|
||||
val cachedQuote = MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO)
|
||||
val apiQuote = MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE)
|
||||
val apiResponse = ApiResponse.Success(
|
||||
data = QuotesResponse(quotes = mapOf("ethereum" to apiQuote)),
|
||||
)
|
||||
|
||||
fetcher.setCachedQuotes(
|
||||
fiatCurrencyId = "usd",
|
||||
quotes = setOf(
|
||||
QuoteMetadata(
|
||||
cryptoCurrencyId = "ethereum",
|
||||
timestamp = DateTime.now().millis - 10_000,
|
||||
value = cachedQuote,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum", fields = "price")
|
||||
} returns apiResponse
|
||||
|
||||
// Act
|
||||
val actual = fetcher.fetch(
|
||||
fiatCurrencyId = "usd",
|
||||
currenciesIds = setOf("ethereum"),
|
||||
fields = setOf(Field.PRICE),
|
||||
)
|
||||
|
||||
val actualCacheData = fetcher.getCachedQuotes()
|
||||
|
||||
// Assert
|
||||
val expected = QuotesResponse(quotes = mapOf("ethereum" to apiQuote)).right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
Truth.assertThat(actualCacheData.keys().toList().size).isEqualTo(1)
|
||||
Truth.assertThat(actualCacheData["usd"].toResponseQuotes()).isEqualTo(mapOf("ethereum" to apiQuote))
|
||||
|
||||
coVerify(exactly = 1) { tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum", fields = "price") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch if cache contain EXPIRED and NOT EXPIRED quotes`() = runTest {
|
||||
// Arrange
|
||||
val cachedQuote = MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO)
|
||||
val apiQuote = MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE)
|
||||
val apiResponse = ApiResponse.Success(
|
||||
data = QuotesResponse(quotes = mapOf("ethereum" to apiQuote)),
|
||||
)
|
||||
|
||||
fetcher.setCachedQuotes(
|
||||
fiatCurrencyId = "usd",
|
||||
quotes = setOf(
|
||||
QuoteMetadata(
|
||||
cryptoCurrencyId = "ethereum",
|
||||
timestamp = DateTime.now().millis - 10_000,
|
||||
value = cachedQuote,
|
||||
),
|
||||
QuoteMetadata(
|
||||
cryptoCurrencyId = "bitcoin",
|
||||
timestamp = DateTime.now().millis,
|
||||
value = cachedQuote,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum", fields = "price")
|
||||
} returns apiResponse
|
||||
|
||||
// Act
|
||||
val actual = fetcher.fetch(
|
||||
fiatCurrencyId = "usd",
|
||||
currenciesIds = setOf("ethereum", "bitcoin"),
|
||||
fields = setOf(Field.PRICE),
|
||||
)
|
||||
|
||||
val actualCacheData = fetcher.getCachedQuotes()
|
||||
|
||||
// Assert
|
||||
val expected = QuotesResponse(
|
||||
quotes = mapOf("ethereum" to apiQuote, "bitcoin" to cachedQuote),
|
||||
).right()
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
Truth.assertThat(actualCacheData.keys().toList().size).isEqualTo(1)
|
||||
Truth.assertThat(actualCacheData["usd"].toResponseQuotes())
|
||||
.isEqualTo(mapOf("ethereum" to apiQuote, "bitcoin" to cachedQuote))
|
||||
|
||||
coVerify(exactly = 1) { tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum", fields = "price") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch if cache DOES NOT CONTAIN fiat currency`() = runTest {
|
||||
// Arrange
|
||||
val apiQuote = MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE)
|
||||
val apiResponse = ApiResponse.Success(
|
||||
data = QuotesResponse(quotes = mapOf("ethereum" to apiQuote)),
|
||||
)
|
||||
|
||||
fetcher.setCachedQuotes(fiatCurrencyId = "eu", quotes = emptySet())
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum", fields = "price")
|
||||
} returns apiResponse
|
||||
|
||||
// Act
|
||||
val actual = fetcher.fetch(
|
||||
fiatCurrencyId = "usd",
|
||||
currenciesIds = setOf("ethereum"),
|
||||
fields = setOf(Field.PRICE),
|
||||
)
|
||||
|
||||
val actualCacheData = fetcher.getCachedQuotes()
|
||||
|
||||
// Assert
|
||||
val expected = QuotesResponse(quotes = mapOf("ethereum" to apiQuote)).right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
Truth.assertThat(actualCacheData.keys().toList().size).isEqualTo(2)
|
||||
Truth.assertThat(actualCacheData["eu"]).isEmpty()
|
||||
Truth.assertThat(actualCacheData["usd"].toResponseQuotes()).isEqualTo(mapOf("ethereum" to apiQuote))
|
||||
|
||||
coVerify(exactly = 1) { tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum", fields = "price") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch if cached quotes are ABSENT`() = runTest {
|
||||
// Arrange
|
||||
val apiQuote = MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE)
|
||||
val apiResponse = ApiResponse.Success(
|
||||
data = QuotesResponse(quotes = mapOf("ethereum" to apiQuote)),
|
||||
)
|
||||
|
||||
fetcher.setCachedQuotes(fiatCurrencyId = "usd", quotes = emptySet())
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum", fields = "price")
|
||||
} returns apiResponse
|
||||
|
||||
// Act
|
||||
val actual = fetcher.fetch(
|
||||
fiatCurrencyId = "usd",
|
||||
currenciesIds = setOf("ethereum"),
|
||||
fields = setOf(Field.PRICE),
|
||||
)
|
||||
|
||||
val actualCacheData = fetcher.getCachedQuotes()
|
||||
|
||||
// Assert
|
||||
val expected = QuotesResponse(quotes = mapOf("ethereum" to apiQuote)).right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
Truth.assertThat(actualCacheData.keys().toList().size).isEqualTo(1)
|
||||
Truth.assertThat(actualCacheData["usd"].toResponseQuotes())
|
||||
.isEqualTo(mapOf("ethereum" to apiQuote))
|
||||
|
||||
coVerify(exactly = 1) { tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum", fields = "price") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `two parallel fetch if cache is empty`() = runTest {
|
||||
// Arrange
|
||||
val apiQuote = MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE)
|
||||
|
||||
val usdApiResponse = ApiResponse.Success(
|
||||
data = QuotesResponse(quotes = mapOf("ethereum" to apiQuote)),
|
||||
)
|
||||
|
||||
val euApiResponse = ApiResponse.Success(
|
||||
data = QuotesResponse(quotes = mapOf("bitcoin" to apiQuote)),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum", fields = "price")
|
||||
} returns usdApiResponse
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.getQuotes(currencyId = "eu", coinIds = "bitcoin", fields = "price")
|
||||
} returns euApiResponse
|
||||
|
||||
// Act
|
||||
val actual = listOf(
|
||||
async {
|
||||
fetcher.fetch(
|
||||
fiatCurrencyId = "usd",
|
||||
currenciesIds = setOf("ethereum"),
|
||||
fields = setOf(Field.PRICE),
|
||||
)
|
||||
},
|
||||
async {
|
||||
fetcher.fetch(
|
||||
fiatCurrencyId = "eu",
|
||||
currenciesIds = setOf("bitcoin"),
|
||||
fields = setOf(Field.PRICE),
|
||||
)
|
||||
},
|
||||
)
|
||||
.awaitAll()
|
||||
|
||||
val actual1 = actual[0]
|
||||
val actual2 = actual[1]
|
||||
|
||||
val actualCacheData = fetcher.getCachedQuotes()
|
||||
|
||||
// Assert
|
||||
val expected1 = QuotesResponse(quotes = mapOf("ethereum" to apiQuote)).right()
|
||||
val expected2 = QuotesResponse(quotes = mapOf("bitcoin" to apiQuote)).right()
|
||||
Truth.assertThat(actual1).isEqualTo(expected1)
|
||||
Truth.assertThat(actual2).isEqualTo(expected2)
|
||||
|
||||
Truth.assertThat(actualCacheData.keys().toList().size).isEqualTo(2)
|
||||
Truth.assertThat(actualCacheData["usd"].toResponseQuotes())
|
||||
.isEqualTo(mapOf("ethereum" to apiQuote))
|
||||
Truth.assertThat(actualCacheData["eu"].toResponseQuotes())
|
||||
.isEqualTo(mapOf("bitcoin" to apiQuote))
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum", fields = "price")
|
||||
tangemTechApi.getQuotes(currencyId = "eu", coinIds = "bitcoin", fields = "price")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `two parallel fetch`() = runTest {
|
||||
// Arrange
|
||||
val apiQuote = MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE)
|
||||
|
||||
val firstApiResponse = ApiResponse.Success(
|
||||
data = QuotesResponse(
|
||||
quotes = mapOf("ethereum" to apiQuote, "solana" to apiQuote),
|
||||
),
|
||||
)
|
||||
|
||||
val secondApiResponse = ApiResponse.Success(
|
||||
data = QuotesResponse(
|
||||
quotes = mapOf("bitcoin" to apiQuote, "solana" to apiQuote),
|
||||
),
|
||||
)
|
||||
|
||||
fetcher.setCachedQuotes(
|
||||
fiatCurrencyId = "usd",
|
||||
quotes = setOf(
|
||||
QuoteMetadata(
|
||||
cryptoCurrencyId = "solana",
|
||||
timestamp = DateTime.now().millis - 10_000,
|
||||
value = MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum,solana", fields = "price")
|
||||
} returns firstApiResponse
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = "bitcoin", fields = "price")
|
||||
} returns secondApiResponse
|
||||
|
||||
// Act
|
||||
val actual = listOf(
|
||||
async {
|
||||
fetcher.fetch(
|
||||
fiatCurrencyId = "usd",
|
||||
currenciesIds = setOf("ethereum", "solana"),
|
||||
fields = setOf(Field.PRICE),
|
||||
)
|
||||
},
|
||||
async {
|
||||
fetcher.fetch(
|
||||
fiatCurrencyId = "usd",
|
||||
currenciesIds = setOf("bitcoin", "solana"),
|
||||
fields = setOf(Field.PRICE),
|
||||
)
|
||||
},
|
||||
)
|
||||
.awaitAll()
|
||||
|
||||
val actual1 = actual[0]
|
||||
val actual2 = actual[1]
|
||||
|
||||
val actualCacheData = fetcher.getCachedQuotes()
|
||||
|
||||
// Assert
|
||||
val expected1 = QuotesResponse(quotes = mapOf("ethereum" to apiQuote, "solana" to apiQuote)).right()
|
||||
val expected2 = QuotesResponse(quotes = mapOf("bitcoin" to apiQuote, "solana" to apiQuote)).right()
|
||||
Truth.assertThat(actual1).isEqualTo(expected1)
|
||||
Truth.assertThat(actual2).isEqualTo(expected2)
|
||||
|
||||
Truth.assertThat(actualCacheData.keys().toList().size).isEqualTo(1)
|
||||
Truth.assertThat(actualCacheData["usd"].toResponseQuotes()).isEqualTo(
|
||||
mapOf("ethereum" to apiQuote, "bitcoin" to apiQuote, "solana" to apiQuote),
|
||||
)
|
||||
|
||||
coVerifyOrder {
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum,solana", fields = "price")
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = "bitcoin", fields = "price")
|
||||
}
|
||||
}
|
||||
|
||||
private fun Iterable<QuoteMetadata>?.toResponseQuotes() = this!!.associate { it.cryptoCurrencyId to it.value }
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.data.markets
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.compatibility.applyL2Compatibility
|
||||
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
|
||||
|
|
@ -9,6 +10,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.data.common.quote.QuotesFetcher
|
||||
import com.tangem.data.common.utils.retryOnError
|
||||
import com.tangem.data.markets.analytics.MarketsDataAnalyticsEvent
|
||||
import com.tangem.data.markets.converters.*
|
||||
|
|
@ -16,8 +18,6 @@ import com.tangem.datasource.api.common.response.ApiResponseError
|
|||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.markets.TangemTechMarketsApi
|
||||
import com.tangem.datasource.api.markets.models.response.TokenMarketExchangesResponse
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi.Companion.marketsQuoteFields
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.markets.*
|
||||
|
|
@ -37,7 +37,7 @@ import java.util.concurrent.atomic.AtomicLong
|
|||
@Suppress("LongParameterList")
|
||||
internal class DefaultMarketsTokenRepository(
|
||||
private val marketsApi: TangemTechMarketsApi,
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val quotesFetcher: QuotesFetcher,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val dispatcherProvider: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
|
|
@ -112,7 +112,7 @@ internal class DefaultMarketsTokenRepository(
|
|||
nextBatchSize: Int,
|
||||
): BatchFlow<Int, List<TokenMarket>, TokenMarketUpdateRequest> {
|
||||
val tokenMarketsUpdateFetcher = MarketsBatchUpdateFetcher(
|
||||
tangemTechApi = tangemTechApi,
|
||||
quotesFetcher = quotesFetcher,
|
||||
marketsApi = marketsApi,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
onApiResponseError = {
|
||||
|
|
@ -225,15 +225,23 @@ internal class DefaultMarketsTokenRepository(
|
|||
withContext(dispatcherProvider.io) {
|
||||
// for second markets iteration we should use extended api method with all required fields
|
||||
|
||||
val result = catchDetailsErrorAndSendEvent(
|
||||
val result = quotesFetcher.fetch(
|
||||
fiatCurrencyId = fiatCurrencyCode,
|
||||
currencyId = tokenId.value,
|
||||
field = QuotesFetcher.Field.ALL_PRICES,
|
||||
)
|
||||
.getOrElse {
|
||||
val error = it as QuotesFetcher.Error.ApiOperationError
|
||||
|
||||
val errorEvent = createDetailsErrorEvent(
|
||||
error = error.apiError,
|
||||
request = MarketsDataAnalyticsEvent.Details.Error.Request.Info,
|
||||
tokenSymbol = tokenSymbol,
|
||||
) {
|
||||
tangemTechApi.getQuotes(
|
||||
currencyId = fiatCurrencyCode,
|
||||
coinIds = tokenId.value,
|
||||
fields = marketsQuoteFields.joinToString(separator = ","),
|
||||
).getOrThrow()
|
||||
)
|
||||
|
||||
analyticsEventHandler.send(errorEvent.toEvent())
|
||||
|
||||
throw error.apiError
|
||||
}
|
||||
|
||||
return@withContext TokenQuotesShortConverter.convert(tokenId, result).toFull()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.data.markets
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.data.common.quote.QuotesFetcher
|
||||
import com.tangem.data.common.utils.retryOnError
|
||||
import com.tangem.data.markets.analytics.MarketsDataAnalyticsEvent
|
||||
import com.tangem.data.markets.converters.TokenMarketChartsConverter
|
||||
|
|
@ -11,8 +13,6 @@ import com.tangem.datasource.api.common.response.catchApiResponseError
|
|||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.markets.TangemTechMarketsApi
|
||||
import com.tangem.datasource.api.markets.models.response.TokenMarketChartListResponse
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi.Companion.marketsQuoteFields
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.domain.markets.TokenMarketUpdateRequest
|
||||
import com.tangem.pagination.Batch
|
||||
|
|
@ -24,7 +24,7 @@ import kotlinx.coroutines.launch
|
|||
|
||||
internal class MarketsBatchUpdateFetcher(
|
||||
private val marketsApi: TangemTechMarketsApi,
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val quotesFetcher: QuotesFetcher,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val onApiResponseError: (ApiResponseError) -> Unit,
|
||||
) : BatchUpdateFetcher<Int, List<TokenMarket>, TokenMarketUpdateRequest> {
|
||||
|
|
@ -72,12 +72,23 @@ internal class MarketsBatchUpdateFetcher(
|
|||
}
|
||||
is TokenMarketUpdateRequest.UpdateQuotes -> {
|
||||
val quotesRes = retryOnError {
|
||||
catchApiResponseError(onApiResponseError) {
|
||||
tangemTechApi.getQuotes(
|
||||
currencyId = updateRequest.currencyId,
|
||||
coinIds = idsToUpdate.map { it.second }.flatten().joinToString(separator = ","),
|
||||
fields = marketsQuoteFields.joinToString(separator = ","),
|
||||
).getOrThrow()
|
||||
val currenciesIds = idsToUpdate.flatMapTo(hashSetOf()) { it.second.map { rawID -> rawID.value } }
|
||||
|
||||
quotesFetcher.fetch(
|
||||
fiatCurrencyId = updateRequest.currencyId,
|
||||
currenciesIds = currenciesIds,
|
||||
fields = setOf(QuotesFetcher.Field.ALL_PRICES),
|
||||
)
|
||||
.getOrElse {
|
||||
val exception = if (it is QuotesFetcher.Error.ApiOperationError) {
|
||||
onApiResponseError(it.apiError)
|
||||
|
||||
it.apiError
|
||||
} else {
|
||||
error("Cause: $it")
|
||||
}
|
||||
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
|||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.data.common.quote.QuotesFetcher
|
||||
import com.tangem.data.markets.DefaultMarketsTokenRepository
|
||||
import com.tangem.datasource.api.markets.TangemTechMarketsApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.markets.repositories.MarketsTokenRepository
|
||||
|
|
@ -25,7 +25,7 @@ internal object MarketsDataModule {
|
|||
@Singleton
|
||||
fun provideMarketsTokenRepository(
|
||||
marketsApi: TangemTechMarketsApi,
|
||||
tangemTechApi: TangemTechApi,
|
||||
quotesFetcher: QuotesFetcher,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
|
|
@ -35,7 +35,7 @@ internal object MarketsDataModule {
|
|||
): MarketsTokenRepository {
|
||||
return DefaultMarketsTokenRepository(
|
||||
marketsApi = marketsApi,
|
||||
tangemTechApi = tangemTechApi,
|
||||
quotesFetcher = quotesFetcher,
|
||||
dispatcherProvider = dispatchers,
|
||||
userWalletsStore = userWalletsStore,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
package com.tangem.data.quotes.multi
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.data.common.api.safeApiCallWithTimeout
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.data.common.quote.QuotesFetcher
|
||||
import com.tangem.data.common.quote.QuotesFetcher.Field
|
||||
import com.tangem.data.quotes.store.QuotesStatusesStore
|
||||
import com.tangem.data.quotes.store.setSourceAsCache
|
||||
import com.tangem.data.quotes.store.setSourceAsOnlyCache
|
||||
import com.tangem.data.quotes.utils.QuotesUnsupportedCurrenciesIdAdapter
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
|
||||
import com.tangem.domain.core.utils.catchOn
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -20,16 +20,16 @@ import javax.inject.Singleton
|
|||
/**
|
||||
* Default implementation of [MultiQuoteStatusFetcher]
|
||||
*
|
||||
* @property tangemTechApi tangemTech api
|
||||
* @property quotesFetcher quotes fetcher
|
||||
* @property appCurrencyResponseStore app currency response store
|
||||
* @property quotesStatusesStore quotes store
|
||||
* @property quotesStatusesStore quotes statuses store
|
||||
* @property dispatchers dispatchers
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Singleton
|
||||
internal class DefaultMultiQuoteStatusFetcher @Inject constructor(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val quotesFetcher: QuotesFetcher,
|
||||
private val appCurrencyResponseStore: AppCurrencyResponseStore,
|
||||
private val quotesStatusesStore: QuotesStatusesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -51,16 +51,13 @@ internal class DefaultMultiQuoteStatusFetcher @Inject constructor(
|
|||
)
|
||||
|
||||
val appCurrencyId = getAppCurrencyId(params = params)
|
||||
val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",")
|
||||
|
||||
val response = safeApiCallWithTimeout(
|
||||
call = {
|
||||
withContext(dispatchers.io) {
|
||||
tangemTechApi.getQuotes(currencyId = appCurrencyId, coinIds = coinIds).bind()
|
||||
}
|
||||
},
|
||||
onError = { error -> throw error },
|
||||
val response = quotesFetcher.fetch(
|
||||
fiatCurrencyId = appCurrencyId,
|
||||
currenciesIds = replacementIdsResult.idsForRequest,
|
||||
fields = setOf(Field.PRICE, Field.PRICE_CHANGE_24H),
|
||||
)
|
||||
.getOrElse { error("Cause: $it") }
|
||||
|
||||
val updatedResponse = QuotesUnsupportedCurrenciesIdAdapter.getResponseWithUnsupportedCurrencies(
|
||||
response = response,
|
||||
|
|
|
|||
|
|
@ -1,107 +1,125 @@
|
|||
package com.tangem.data.quotes.multi
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.data.quote.MockQuoteResponseFactory
|
||||
import com.tangem.common.test.utils.assertEither
|
||||
import com.tangem.data.common.quote.QuotesFetcher
|
||||
import com.tangem.data.quotes.store.QuotesStatusesStore
|
||||
import com.tangem.data.quotes.store.setSourceAsCache
|
||||
import com.tangem.data.quotes.store.setSourceAsOnlyCache
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.coVerifyOrder
|
||||
import io.mockk.mockk
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultMultiQuoteStatusFetcherTest {
|
||||
|
||||
private val tangemTechApi = mockk<TangemTechApi>(relaxed = true)
|
||||
private val appCurrencyResponseStore = mockk<AppCurrencyResponseStore>(relaxed = true)
|
||||
private val quotesFetcher = mockk<QuotesFetcher>()
|
||||
private val appCurrencyResponseStore = mockk<AppCurrencyResponseStore>()
|
||||
private val quotesStore = mockk<QuotesStatusesStore>(relaxed = true)
|
||||
|
||||
private val fetcher = DefaultMultiQuoteStatusFetcher(
|
||||
tangemTechApi = tangemTechApi,
|
||||
quotesFetcher = quotesFetcher,
|
||||
appCurrencyResponseStore = appCurrencyResponseStore,
|
||||
quotesStatusesStore = quotesStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(quotesFetcher, appCurrencyResponseStore, quotesStore)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch quotes successfully`() = runTest {
|
||||
fun `fetch successfully`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null)
|
||||
|
||||
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency
|
||||
|
||||
val coinIds = "BTC,ETH"
|
||||
coEvery {
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
|
||||
} returns ApiResponse.Success(successResponse)
|
||||
val currenciesIds = setOf("BTC", "ETH")
|
||||
|
||||
coEvery {
|
||||
quotesFetcher.fetch(fiatCurrencyId = "usd", currenciesIds = currenciesIds, fields = fields)
|
||||
} returns successResponse.right()
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = Unit.right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds)
|
||||
|
||||
appCurrencyResponseStore.getSyncOrNull()
|
||||
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
|
||||
|
||||
quotesFetcher.fetch(fiatCurrencyId = "usd", currenciesIds = currenciesIds, fields = fields)
|
||||
quotesStore.store(values = successResponse.quotes)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
quotesStore.setSourceAsOnlyCache(currenciesIds = any())
|
||||
}
|
||||
|
||||
Truth.assertThat(actual.isRight()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch quotes successfully if currenciesIds from params is empty`() = runTest {
|
||||
fun `fetch successfully if currenciesIds from params is empty`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiQuoteStatusFetcher.Params(currenciesIds = emptySet(), appCurrencyId = null)
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = Unit.right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerify(inverse = true) {
|
||||
quotesStore.setSourceAsCache(currenciesIds = any())
|
||||
appCurrencyResponseStore.getSyncOrNull()
|
||||
tangemTechApi.getQuotes(currencyId = any(), coinIds = any())
|
||||
quotesFetcher.fetch(fiatCurrencyId = any(), currenciesIds = any(), fields = any())
|
||||
quotesStore.store(values = any())
|
||||
quotesStore.setSourceAsOnlyCache(currenciesIds = any())
|
||||
}
|
||||
|
||||
Truth.assertThat(actual.isRight()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch quotes successfully if appCurrencyId from params is not null`() = runTest {
|
||||
fun `fetch successfully if appCurrencyId from params is not null`() = runTest {
|
||||
// Arrange
|
||||
val appCurrencyId = "usd"
|
||||
val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrencyId)
|
||||
|
||||
val coinIds = "BTC,ETH"
|
||||
coEvery {
|
||||
tangemTechApi.getQuotes(currencyId = appCurrencyId, coinIds = coinIds)
|
||||
} returns ApiResponse.Success(successResponse)
|
||||
val currenciesIds = setOf("BTC", "ETH")
|
||||
|
||||
coEvery {
|
||||
quotesFetcher.fetch(fiatCurrencyId = appCurrencyId, currenciesIds = currenciesIds, fields = fields)
|
||||
} returns successResponse.right()
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = Unit.right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds)
|
||||
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
|
||||
|
||||
quotesFetcher.fetch(fiatCurrencyId = appCurrencyId, currenciesIds = currenciesIds, fields = fields)
|
||||
quotesStore.store(values = successResponse.quotes)
|
||||
}
|
||||
|
||||
|
|
@ -109,17 +127,22 @@ internal class DefaultMultiQuoteStatusFetcherTest {
|
|||
appCurrencyResponseStore.getSyncOrNull()
|
||||
quotesStore.setSourceAsOnlyCache(currenciesIds = any())
|
||||
}
|
||||
|
||||
Truth.assertThat(actual.isRight()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch quotes failure because appCurrencyId from params is blank`() = runTest {
|
||||
fun `fetch failure because appCurrencyId from params is blank`() = runTest {
|
||||
// Arrange
|
||||
val appCurrencyId = ""
|
||||
val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrencyId)
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = IllegalStateException("Unable to get AppCurrency for updating quotes").left()
|
||||
|
||||
assertEither(actual, expected)
|
||||
|
||||
coVerifyOrder {
|
||||
quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds)
|
||||
quotesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds)
|
||||
|
|
@ -127,55 +150,59 @@ internal class DefaultMultiQuoteStatusFetcherTest {
|
|||
|
||||
coVerify(inverse = true) {
|
||||
appCurrencyResponseStore.getSyncOrNull()
|
||||
tangemTechApi.getQuotes(currencyId = any(), coinIds = any())
|
||||
quotesFetcher.fetch(fiatCurrencyId = any(), currenciesIds = any(), fields = any())
|
||||
quotesStore.store(values = any())
|
||||
}
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
Truth.assertThat(actual.leftOrNull()).isInstanceOf(IllegalStateException::class.java)
|
||||
Truth.assertThat(actual.leftOrNull()).hasMessageThat()
|
||||
.isEqualTo("Unable to get AppCurrency for updating quotes")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch quotes failure because api request failed`() = runTest {
|
||||
fun `fetch failure because api request failed`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null)
|
||||
|
||||
val currenciesIds = setOf("BTC", "ETH")
|
||||
val error = QuotesFetcher.Error.ApiOperationError(ApiResponseError.NetworkException)
|
||||
|
||||
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency
|
||||
|
||||
val coinIds = "BTC,ETH"
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val errorResponse = ApiResponse.Error(ApiResponseError.NetworkException) as ApiResponse<QuotesResponse>
|
||||
coEvery { tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) } returns errorResponse
|
||||
coEvery {
|
||||
quotesFetcher.fetch(fiatCurrencyId = "usd", currenciesIds = currenciesIds, fields = fields)
|
||||
} returns error.left()
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = IllegalStateException("Cause: ApiOperationError(apiError=NetworkException)").left()
|
||||
assertEither(actual, expected)
|
||||
|
||||
coVerifyOrder {
|
||||
quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds)
|
||||
|
||||
appCurrencyResponseStore.getSyncOrNull()
|
||||
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
|
||||
|
||||
quotesFetcher.fetch(fiatCurrencyId = "usd", currenciesIds = currenciesIds, fields = fields)
|
||||
quotesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
quotesStore.store(values = any())
|
||||
}
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch quotes failure because app currency not found`() = runTest {
|
||||
fun `fetch failure because app currency not found`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null)
|
||||
|
||||
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns null
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = IllegalStateException("Unable to get AppCurrency for updating quotes").left()
|
||||
|
||||
assertEither(actual, expected)
|
||||
|
||||
coVerifyOrder {
|
||||
quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds)
|
||||
appCurrencyResponseStore.getSyncOrNull()
|
||||
|
|
@ -183,11 +210,9 @@ internal class DefaultMultiQuoteStatusFetcherTest {
|
|||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
tangemTechApi.getQuotes(currencyId = any(), coinIds = any())
|
||||
quotesFetcher.fetch(fiatCurrencyId = any(), currenciesIds = any(), fields = any())
|
||||
quotesStore.store(values = any())
|
||||
}
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
|
@ -212,5 +237,7 @@ internal class DefaultMultiQuoteStatusFetcherTest {
|
|||
"ETH" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.TEN),
|
||||
),
|
||||
)
|
||||
|
||||
val fields = setOf(QuotesFetcher.Field.PRICE, QuotesFetcher.Field.PRICE_CHANGE_24H)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,176 +1,79 @@
|
|||
package com.tangem.data.quotes.single
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.data.quote.MockQuoteResponseFactory
|
||||
import com.tangem.data.quotes.multi.DefaultMultiQuoteStatusFetcher
|
||||
import com.tangem.data.quotes.store.QuotesStatusesStore
|
||||
import com.tangem.data.quotes.store.setSourceAsCache
|
||||
import com.tangem.data.quotes.store.setSourceAsOnlyCache
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.coVerifyOrder
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultSingleQuoteStatusFetcherTest {
|
||||
|
||||
private val tangemTechApi = mockk<TangemTechApi>(relaxed = true)
|
||||
private val appCurrencyResponseStore = mockk<AppCurrencyResponseStore>(relaxed = true)
|
||||
private val quotesStore = mockk<QuotesStatusesStore>(relaxed = true)
|
||||
|
||||
private val multiFetcher = DefaultMultiQuoteStatusFetcher(
|
||||
tangemTechApi = tangemTechApi,
|
||||
appCurrencyResponseStore = appCurrencyResponseStore,
|
||||
quotesStatusesStore = quotesStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
private val multiFetcher = mockk<MultiQuoteStatusFetcher>()
|
||||
private val singleFetcher = DefaultSingleQuoteStatusFetcher(multiFetcher)
|
||||
|
||||
@Test
|
||||
fun `fetch single quote successfully`() = runTest {
|
||||
val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null)
|
||||
|
||||
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency
|
||||
|
||||
val coinIds = "BTC"
|
||||
coEvery {
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
|
||||
} returns ApiResponse.Success(successResponse)
|
||||
|
||||
val actual = singleFetcher(params)
|
||||
|
||||
coVerifyOrder {
|
||||
quotesStore.setSourceAsCache(currenciesIds = setOf(params.rawCurrencyId))
|
||||
appCurrencyResponseStore.getSyncOrNull()
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
|
||||
quotesStore.store(values = successResponse.quotes)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
quotesStore.setSourceAsOnlyCache(currenciesIds = any())
|
||||
}
|
||||
|
||||
Truth.assertThat(actual.isRight()).isTrue()
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(multiFetcher)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch single quote successfully if appCurrencyId from params is not null`() = runTest {
|
||||
val appCurrencyId = "usd"
|
||||
val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId)
|
||||
fun `fetch successfully if multiFetcher returns success`() = runTest {
|
||||
// Arrange
|
||||
val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currencyId, appCurrencyId = null)
|
||||
|
||||
val coinIds = "BTC"
|
||||
coEvery {
|
||||
tangemTechApi.getQuotes(currencyId = appCurrencyId, coinIds = coinIds)
|
||||
} returns ApiResponse.Success(successResponse)
|
||||
val multiFetcherParams = MultiQuoteStatusFetcher.Params(
|
||||
currenciesIds = setOf(params.rawCurrencyId),
|
||||
appCurrencyId = params.appCurrencyId,
|
||||
)
|
||||
|
||||
coEvery { multiFetcher(multiFetcherParams) } returns Unit.right()
|
||||
|
||||
// Act
|
||||
val actual = singleFetcher(params)
|
||||
|
||||
coVerifyOrder {
|
||||
quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId))
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
|
||||
quotesStore.store(values = successResponse.quotes)
|
||||
}
|
||||
// Assert
|
||||
val expected = Unit.right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
Truth.assertThat(actual.isRight()).isTrue()
|
||||
coVerify(exactly = 1) { multiFetcher(multiFetcherParams) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch single quote failure because appCurrencyId from params is blank`() = runTest {
|
||||
val appCurrencyId = ""
|
||||
val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId)
|
||||
fun `fetch failure if multiFetcher returns erro`() = runTest {
|
||||
// Arrange
|
||||
val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currencyId, appCurrencyId = null)
|
||||
|
||||
val multiFetcherParams = MultiQuoteStatusFetcher.Params(
|
||||
currenciesIds = setOf(params.rawCurrencyId),
|
||||
appCurrencyId = params.appCurrencyId,
|
||||
)
|
||||
|
||||
val multiFetcherError = IllegalStateException("").left()
|
||||
|
||||
coEvery { multiFetcher(multiFetcherParams) } returns multiFetcherError
|
||||
|
||||
// Act
|
||||
val actual = singleFetcher(params)
|
||||
|
||||
coVerifyOrder {
|
||||
quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId))
|
||||
quotesStore.setSourceAsOnlyCache(currenciesIds = setOf(currenciesId))
|
||||
}
|
||||
// Assert
|
||||
val expected = multiFetcherError
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
Truth.assertThat(actual.leftOrNull()).isInstanceOf(IllegalStateException::class.java)
|
||||
Truth.assertThat(actual.leftOrNull()).hasMessageThat()
|
||||
.isEqualTo("Unable to get AppCurrency for updating quotes")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch single quote failure because api request failed`() = runTest {
|
||||
val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null)
|
||||
|
||||
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency
|
||||
|
||||
val coinIds = "BTC"
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val errorResponse = ApiResponse.Error(ApiResponseError.NetworkException) as ApiResponse<QuotesResponse>
|
||||
coEvery { tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) } returns errorResponse
|
||||
|
||||
val actual = singleFetcher(params)
|
||||
|
||||
coVerifyOrder {
|
||||
quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId))
|
||||
appCurrencyResponseStore.getSyncOrNull()
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
|
||||
quotesStore.setSourceAsOnlyCache(currenciesIds = setOf(currenciesId))
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
quotesStore.store(values = any())
|
||||
}
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch single quote failure because app currency not found`() = runTest {
|
||||
val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null)
|
||||
|
||||
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns null
|
||||
|
||||
val actual = singleFetcher(params)
|
||||
|
||||
coVerifyOrder {
|
||||
quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId))
|
||||
appCurrencyResponseStore.getSyncOrNull()
|
||||
quotesStore.setSourceAsOnlyCache(currenciesIds = setOf(currenciesId))
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
tangemTechApi.getQuotes(currencyId = any(), coinIds = any())
|
||||
quotesStore.store(values = any())
|
||||
}
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
coVerify(exactly = 1) { multiFetcher(multiFetcherParams) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val currenciesId = CryptoCurrency.RawID(value = "BTC")
|
||||
|
||||
val usdAppCurrency = CurrenciesResponse.Currency(
|
||||
id = "USD".lowercase(),
|
||||
code = "USD",
|
||||
name = "US Dollar",
|
||||
unit = "$",
|
||||
type = "fiat",
|
||||
rateBTC = "",
|
||||
)
|
||||
|
||||
val successResponse = QuotesResponse(
|
||||
quotes = mapOf(
|
||||
"BTC" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE),
|
||||
),
|
||||
)
|
||||
val currencyId = CryptoCurrency.RawID(value = "BTC")
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.data.visa
|
|||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import arrow.core.getOrElse
|
||||
import arrow.fx.coroutines.parZip
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
|
|
@ -10,10 +11,9 @@ import com.tangem.common.card.EllipticCurve
|
|||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.common.quote.QuotesFetcher
|
||||
import com.tangem.data.visa.config.VisaLibLoader
|
||||
import com.tangem.data.visa.utils.*
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.visa.TangemVisaApi
|
||||
import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
|
|
@ -38,7 +38,7 @@ import javax.inject.Singleton
|
|||
@Singleton
|
||||
internal class DefaultVisaRepository @Inject constructor(
|
||||
private val visaLibLoader: VisaLibLoader,
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val quotesFetcher: QuotesFetcher,
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -190,10 +190,13 @@ internal class DefaultVisaRepository @Inject constructor(
|
|||
|
||||
private suspend fun getFiatRate(): BigDecimal {
|
||||
val fiatCurrencyId = VisaConstants.fiatCurrency.code.lowercase()
|
||||
val quotes = tangemTechApi.getQuotes(
|
||||
currencyId = fiatCurrencyId,
|
||||
coinIds = VisaConstants.TOKEN_ID,
|
||||
).getOrThrow()
|
||||
|
||||
val quotes = quotesFetcher.fetch(
|
||||
fiatCurrencyId = fiatCurrencyId,
|
||||
currencyId = VisaConstants.TOKEN_ID,
|
||||
field = QuotesFetcher.Field.PRICE,
|
||||
)
|
||||
.getOrElse { error("Cause: $it") }
|
||||
|
||||
return quotes.quotes[VisaConstants.TOKEN_ID]?.price ?: error("No price found")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue