Updated on 2026-08-14
This commit is contained in:
parent
46c19f2a3c
commit
850ecba894
12 changed files with 251 additions and 85 deletions
|
|
@ -4,9 +4,12 @@ import android.content.Context
|
||||||
import androidx.datastore.core.DataStoreFactory
|
import androidx.datastore.core.DataStoreFactory
|
||||||
import androidx.datastore.dataStoreFile
|
import androidx.datastore.dataStoreFile
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||||
|
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||||
import com.tangem.datasource.local.quote.DefaultQuotesStore
|
import com.tangem.datasource.local.quote.DefaultQuotesStore
|
||||||
import com.tangem.datasource.local.quote.QuotesStore
|
import com.tangem.datasource.local.quote.QuotesStore
|
||||||
import com.tangem.datasource.local.quote.utils.QuotesSerializer
|
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||||
|
import com.tangem.datasource.utils.mapWithStringKeyTypes
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
|
|
@ -29,11 +32,16 @@ internal object QuotesStoreModule {
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
): QuotesStore {
|
): QuotesStore {
|
||||||
return DefaultQuotesStore(
|
return DefaultQuotesStore(
|
||||||
dataStore = DataStoreFactory.create(
|
persistenceStore = DataStoreFactory.create(
|
||||||
serializer = QuotesSerializer(moshi),
|
serializer = MoshiDataStoreSerializer(
|
||||||
|
moshi = moshi,
|
||||||
|
types = mapWithStringKeyTypes<QuotesResponse.Quote>(),
|
||||||
|
defaultValue = emptyMap(),
|
||||||
|
),
|
||||||
produceFile = { context.dataStoreFile(fileName = "quotes") },
|
produceFile = { context.dataStoreFile(fileName = "quotes") },
|
||||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||||
),
|
),
|
||||||
|
runtimeStore = RuntimeSharedStore(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
package com.tangem.datasource.local.datastore
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime shared store
|
||||||
|
*
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
interface RuntimeSharedStore<T> {
|
||||||
|
|
||||||
|
/** Get flow of elements [T] */
|
||||||
|
fun get(): Flow<T>
|
||||||
|
|
||||||
|
/** Get element [T] synchronously or null */
|
||||||
|
suspend fun getSyncOrNull(): T?
|
||||||
|
|
||||||
|
/** Get element [T] synchronously or default value */
|
||||||
|
suspend fun getSyncOrDefault(default: T): T
|
||||||
|
|
||||||
|
/** Store [value] */
|
||||||
|
suspend fun store(value: T)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update
|
||||||
|
*
|
||||||
|
* @param default default value if store is empty
|
||||||
|
* @param function update function
|
||||||
|
*/
|
||||||
|
suspend fun update(default: T, function: (T) -> T)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create [RuntimeSharedStore]
|
||||||
|
*
|
||||||
|
* @param T type of stored value
|
||||||
|
*/
|
||||||
|
operator fun <T> invoke(): RuntimeSharedStore<T> = object : RuntimeSharedStore<T> {
|
||||||
|
|
||||||
|
private val flow = MutableSharedFlow<T?>(replay = 1)
|
||||||
|
|
||||||
|
init {
|
||||||
|
flow.tryEmit(value = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun get(): Flow<T> = flow.mapNotNull { it ?: return@mapNotNull null }
|
||||||
|
|
||||||
|
override suspend fun getSyncOrNull(): T? = flow.firstOrNull()
|
||||||
|
|
||||||
|
override suspend fun getSyncOrDefault(default: T): T = getSyncOrNull() ?: default
|
||||||
|
|
||||||
|
override suspend fun store(value: T) {
|
||||||
|
flow.emit(value = value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun update(default: T, function: (T) -> T) {
|
||||||
|
val storedData = flow.firstOrNull() ?: default
|
||||||
|
val updatedData = function(storedData)
|
||||||
|
|
||||||
|
flow.emit(value = updatedData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,54 +2,82 @@ package com.tangem.datasource.local.quote
|
||||||
|
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||||
import com.tangem.datasource.local.quote.model.QuoteDM
|
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||||
import com.tangem.datasource.local.quote.model.QuotesDM
|
import com.tangem.datasource.local.quote.converter.QuoteConverter
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
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.extensions.orZero
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
|
|
||||||
|
internal typealias QuotesByCurrencyId = Map<String, QuotesResponse.Quote>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of [QuotesStore]
|
||||||
|
*
|
||||||
|
* @property persistenceStore persistence quotes store
|
||||||
|
* @property runtimeStore runtime quotes store
|
||||||
|
*/
|
||||||
internal class DefaultQuotesStore(
|
internal class DefaultQuotesStore(
|
||||||
private val dataStore: DataStore<QuotesDM>,
|
private val persistenceStore: DataStore<QuotesByCurrencyId>,
|
||||||
|
private val runtimeStore: RuntimeSharedStore<Set<Quote>>,
|
||||||
) : QuotesStore {
|
) : QuotesStore {
|
||||||
|
|
||||||
override fun get(currenciesIds: Set<CryptoCurrency.RawID>): Flow<Set<Quote>> {
|
override fun get(currenciesIds: Set<CryptoCurrency.RawID>): Flow<Set<Quote>> = channelFlow {
|
||||||
return dataStore.data
|
val cachedQuotes = getCachedQuotes(currenciesIds = currenciesIds)
|
||||||
.map { quotes -> createQuotes(currenciesIds, quotes) }
|
|
||||||
|
if (cachedQuotes.isNotEmpty()) {
|
||||||
|
send(cachedQuotes)
|
||||||
|
}
|
||||||
|
|
||||||
|
runtimeStore.get()
|
||||||
|
.onEach {
|
||||||
|
val mergedQuotes = mergeQuotes(cachedQuotes = cachedQuotes, runtimeQuotes = it)
|
||||||
|
send(element = mergedQuotes)
|
||||||
|
}
|
||||||
|
.launchIn(scope = this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getSync(currenciesIds: Set<CryptoCurrency.RawID>): Set<Quote> {
|
override suspend fun getSync(currenciesIds: Set<CryptoCurrency.RawID>): Set<Quote> {
|
||||||
val quotes = dataStore.data.firstOrNull().orEmpty()
|
return runtimeStore.getSyncOrDefault(default = emptySet())
|
||||||
|
.filter { it.rawCurrencyId in currenciesIds }
|
||||||
return createQuotes(currenciesIds, quotes)
|
.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createQuotes(currenciesIds: Set<CryptoCurrency.RawID>, quoteEntities: Set<QuoteDM>): Set<Quote> =
|
|
||||||
currenciesIds.mapTo(mutableSetOf()) { id ->
|
|
||||||
quoteEntities.firstOrNull { it.rawCurrencyId == id }
|
|
||||||
?.let { quote ->
|
|
||||||
Quote.Value(
|
|
||||||
rawCurrencyId = id,
|
|
||||||
fiatRate = quote.fiatRate,
|
|
||||||
priceChange = quote.priceChange,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
?: Quote.Empty(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun store(response: QuotesResponse) {
|
override suspend fun store(response: QuotesResponse) {
|
||||||
val newQuotes = response.quotes.mapTo(mutableSetOf()) { (currencyId, quote) ->
|
coroutineScope {
|
||||||
QuoteDM(
|
launch { storeInRuntimeStore(response = response) }
|
||||||
rawCurrencyId = CryptoCurrency.RawID(currencyId),
|
launch { storeInPersistenceStore(response = response) }
|
||||||
fiatRate = quote.price.orZero(),
|
|
||||||
priceChange = quote.priceChange24h.orZero().movePointLeft(2),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
dataStore.updateData { storedQuotes ->
|
|
||||||
(newQuotes + storedQuotes).distinctBy { it.rawCurrencyId }.toSet()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getCachedQuotes(currenciesIds: Set<CryptoCurrency.RawID>): Set<Quote.Value> {
|
||||||
|
val ids = currenciesIds.map(CryptoCurrency.RawID::value).toSet()
|
||||||
|
val cachedQuotes = persistenceStore.data.firstOrNull().orEmpty().filterKeys { it in ids }
|
||||||
|
|
||||||
|
return QuoteConverter(isCached = true).convertSet(input = cachedQuotes.entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mergeQuotes(cachedQuotes: Set<Quote.Value>, runtimeQuotes: Set<Quote>): Set<Quote> {
|
||||||
|
return runtimeQuotes.map { runtimeQuote ->
|
||||||
|
if (runtimeQuote is Quote.Empty) {
|
||||||
|
cachedQuotes.firstOrNull { runtimeQuote.rawCurrencyId == it.rawCurrencyId } ?: runtimeQuote
|
||||||
|
} else {
|
||||||
|
runtimeQuote
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun storeInRuntimeStore(response: QuotesResponse) {
|
||||||
|
val new = QuoteConverter(isCached = false).convertSet(input = response.quotes.entries)
|
||||||
|
|
||||||
|
runtimeStore.update(default = emptySet()) { saved ->
|
||||||
|
(saved + new).distinctBy { it.rawCurrencyId }.toSet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun storeInPersistenceStore(response: QuotesResponse) {
|
||||||
|
persistenceStore.updateData { storedQuotes -> storedQuotes + response.quotes }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,11 +5,15 @@ import com.tangem.domain.tokens.model.CryptoCurrency
|
||||||
import com.tangem.domain.tokens.model.Quote
|
import com.tangem.domain.tokens.model.Quote
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/** Quotes store */
|
||||||
interface QuotesStore {
|
interface QuotesStore {
|
||||||
|
|
||||||
|
/** Get flow of quotes for [currenciesIds] */
|
||||||
fun get(currenciesIds: Set<CryptoCurrency.RawID>): Flow<Set<Quote>>
|
fun get(currenciesIds: Set<CryptoCurrency.RawID>): Flow<Set<Quote>>
|
||||||
|
|
||||||
|
/** Get quotes for [currenciesIds] synchronously */
|
||||||
suspend fun getSync(currenciesIds: Set<CryptoCurrency.RawID>): Set<Quote>
|
suspend fun getSync(currenciesIds: Set<CryptoCurrency.RawID>): Set<Quote>
|
||||||
|
|
||||||
|
/** Store [response] from remote */
|
||||||
suspend fun store(response: QuotesResponse)
|
suspend fun store(response: QuotesResponse)
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package com.tangem.datasource.local.quote.converter
|
||||||
|
|
||||||
|
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||||
|
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||||
|
import com.tangem.domain.tokens.model.Quote
|
||||||
|
import com.tangem.utils.converter.Converter
|
||||||
|
import com.tangem.utils.extensions.orZero
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converter from [QuotesResponse.Quote] to [Quote.Value]
|
||||||
|
*
|
||||||
|
* @property isCached flag that determines whether the quote is a cache
|
||||||
|
*
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
internal class QuoteConverter(private val isCached: Boolean) :
|
||||||
|
Converter<Map.Entry<String, QuotesResponse.Quote>, Quote.Value> {
|
||||||
|
|
||||||
|
override fun convert(value: Map.Entry<String, QuotesResponse.Quote>): Quote.Value {
|
||||||
|
val (currencyId, quote) = value
|
||||||
|
|
||||||
|
return Quote.Value(
|
||||||
|
rawCurrencyId = CryptoCurrency.RawID(currencyId),
|
||||||
|
fiatRate = quote.price.orZero(),
|
||||||
|
priceChange = quote.priceChange24h.orZero().movePointLeft(2),
|
||||||
|
isCached = isCached,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
package com.tangem.datasource.local.quote.model
|
|
||||||
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
|
||||||
import java.math.BigDecimal
|
|
||||||
|
|
||||||
internal typealias QuotesDM = Set<QuoteDM>
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
internal data class QuoteDM(
|
|
||||||
val rawCurrencyId: CryptoCurrency.RawID,
|
|
||||||
val fiatRate: BigDecimal,
|
|
||||||
val priceChange: BigDecimal,
|
|
||||||
)
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
package com.tangem.datasource.local.quote.utils
|
|
||||||
|
|
||||||
import androidx.datastore.core.Serializer
|
|
||||||
import com.squareup.moshi.Moshi
|
|
||||||
import com.squareup.moshi.Types
|
|
||||||
import com.tangem.datasource.local.quote.model.QuoteDM
|
|
||||||
import com.tangem.datasource.local.quote.model.QuotesDM
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
internal class QuotesSerializer(moshi: Moshi) : Serializer<QuotesDM> {
|
|
||||||
|
|
||||||
private val adapter by lazy {
|
|
||||||
val types = Types.newParameterizedType(Set::class.java, QuoteDM::class.java)
|
|
||||||
|
|
||||||
moshi.adapter<QuotesDM>(types)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val defaultValue: QuotesDM = emptySet()
|
|
||||||
|
|
||||||
override suspend fun readFrom(input: InputStream): QuotesDM {
|
|
||||||
return input.bufferedReader().use { reader ->
|
|
||||||
adapter.fromJson(reader.readText()) ?: defaultValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun writeTo(t: QuotesDM, output: OutputStream) {
|
|
||||||
output.bufferedWriter().use { write ->
|
|
||||||
write.write(adapter.toJson(t))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
package com.tangem.datasource.utils
|
||||||
|
|
||||||
|
import androidx.datastore.core.Serializer
|
||||||
|
import com.squareup.moshi.JsonAdapter
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.lang.reflect.ParameterizedType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moshi serializer [JsonAdapter] for [androidx.datastore.core.DataStore]
|
||||||
|
*
|
||||||
|
* @property defaultValue default value
|
||||||
|
* @property adapter moshi adapter
|
||||||
|
*
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
class MoshiDataStoreSerializer<T>(
|
||||||
|
override val defaultValue: T,
|
||||||
|
private val adapter: JsonAdapter<T>,
|
||||||
|
) : Serializer<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param moshi moshi for creating adapter
|
||||||
|
* @param types types of data
|
||||||
|
* @param defaultValue default value
|
||||||
|
*/
|
||||||
|
constructor(moshi: Moshi, types: ParameterizedType, defaultValue: T) : this(
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
adapter = moshi.adapter<T>(types),
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun readFrom(input: InputStream): T {
|
||||||
|
return input.bufferedReader().use { reader ->
|
||||||
|
adapter.fromJson(reader.readText()) ?: defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun writeTo(t: T, output: OutputStream) {
|
||||||
|
output.bufferedWriter().use { write ->
|
||||||
|
write.write(adapter.toJson(t))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.tangem.datasource.utils
|
||||||
|
|
||||||
|
import com.squareup.moshi.Types
|
||||||
|
import java.lang.reflect.ParameterizedType
|
||||||
|
|
||||||
|
inline fun <reified T> mapWithStringKeyTypes(): ParameterizedType {
|
||||||
|
return Types.newParameterizedType(Map::class.java, String::class.java, T::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T> listTypes(): ParameterizedType {
|
||||||
|
return Types.newParameterizedType(List::class.java, T::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T> setTypes(): ParameterizedType {
|
||||||
|
return Types.newParameterizedType(Set::class.java, T::class.java)
|
||||||
|
}
|
||||||
|
|
@ -94,6 +94,7 @@ internal class HotCryptoCurrencyConverter(
|
||||||
rawCurrencyId = rawCurrencyId,
|
rawCurrencyId = rawCurrencyId,
|
||||||
fiatRate = fiatRate,
|
fiatRate = fiatRate,
|
||||||
priceChange = priceChange.movePointLeft(2),
|
priceChange = priceChange.movePointLeft(2),
|
||||||
|
isCached = false, // It doesn't matter
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Quote.Empty(rawCurrencyId)
|
Quote.Empty(rawCurrencyId)
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,12 @@ sealed interface Quote {
|
||||||
* @property rawCurrencyId The unique identifier of the cryptocurrency for which the financial information is provided.
|
* @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 fiatRate The current fiat exchange rate for the cryptocurrency.
|
||||||
* @property priceChange The price change for the cryptocurrency.
|
* @property priceChange The price change for the cryptocurrency.
|
||||||
|
* @property isCached flag that determines whether the quote is a cache
|
||||||
*/
|
*/
|
||||||
data class Value(
|
data class Value(
|
||||||
override val rawCurrencyId: CryptoCurrency.RawID,
|
override val rawCurrencyId: CryptoCurrency.RawID,
|
||||||
val fiatRate: BigDecimal,
|
val fiatRate: BigDecimal,
|
||||||
val priceChange: BigDecimal,
|
val priceChange: BigDecimal,
|
||||||
|
val isCached: Boolean,
|
||||||
) : Quote
|
) : Quote
|
||||||
}
|
}
|
||||||
|
|
@ -12,60 +12,70 @@ internal object MockQuotes {
|
||||||
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"),
|
||||||
|
isCached = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
val quote2 = Quote.Value(
|
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"),
|
||||||
|
isCached = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
val quote3 = Quote.Value(
|
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"),
|
||||||
|
isCached = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
val quote4 = Quote.Value(
|
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"),
|
||||||
|
isCached = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
val quote5 = Quote.Value(
|
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"),
|
||||||
|
isCached = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
val quote6 = Quote.Value(
|
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"),
|
||||||
|
isCached = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
val quote7 = Quote.Value(
|
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"),
|
||||||
|
isCached = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
val quote8 = Quote.Value(
|
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"),
|
||||||
|
isCached = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
val quote9 = Quote.Value(
|
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"),
|
||||||
|
isCached = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
val quote10 = Quote.Value(
|
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"),
|
||||||
|
isCached = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
val quote11 = Quote.Empty(CryptoCurrency.RawID("null"))
|
val quote11 = Quote.Empty(CryptoCurrency.RawID("null"))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue