Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-16 11:07:58 +03:00
parent f7412dfd86
commit 0cd5575681
20 changed files with 437 additions and 162 deletions

View file

@ -16,6 +16,7 @@ dependencies {
implementation(projects.core.pagination)
implementation(projects.domain.tokens.models)
implementation(projects.domain.markets)
implementation(projects.data.common)
// region DI
implementation(deps.hilt.android)
@ -26,6 +27,7 @@ dependencies {
implementation(deps.kotlin.coroutines)
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.timber)
implementation(projects.libs.blockchainSdk)
// endregion

View file

@ -1,6 +1,7 @@
package com.tangem.data.markets
import com.tangem.data.markets.converters.*
import com.tangem.data.markets.utils.retryOnError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
@ -9,7 +10,7 @@ import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.pagination.*
import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import kotlinx.coroutines.*
internal class DefaultMarketsTokenRepository(
private val marketsApi: TangemTechMarketsApi,
@ -18,28 +19,34 @@ internal class DefaultMarketsTokenRepository(
) : MarketsTokenRepository {
private val tokenListConverter = TokenMarketListConverter()
private val tokenListChartsConverter = TokenMarketChartsConverter(TokenListChartConverter())
private val tokenQuotesConverter = TokenQuotesConverter()
private val tokenMarketsFetcher
get() = LimitOffsetBatchFetcher<TokenMarketListConfig, List<TokenMarket>>(
prefetchDistance = 50,
batchSize = 30,
fetch = { params ->
withContext(dispatcherProvider.io) {
val res = marketsApi.getCoinsList(
currency = params.request.fiatPriceCurrency,
interval = params.request.priceChangeInterval.toRequestParam(),
order = params.request.priceChangeInterval.toRequestParam(),
search = params.request.searchText,
generalCoins = params.request.showUnder100kMarketCapTokens.not(),
offset = params.offset,
limit = params.limit,
).getOrThrow()
get() = LimitOffsetBatchFetcher(
prefetchDistance = 150,
batchSize = 100,
subFetcher = object : LimitOffsetBatchFetcher.SubFetcher<TokenMarketListConfig, List<TokenMarket>> {
val last = res.tokens.size < params.limit
var requestTimeStamp: Long? = null // TODO when backend is ready
BatchFetchResult.Success(
override suspend fun fetch(
request: LimitOffsetBatchFetcher.Request<TokenMarketListConfig>,
lastResult: BatchFetchResult<List<TokenMarket>>?,
): BatchFetchResult<List<TokenMarket>> {
val res = retryOnError(priority = true) {
marketsApi.getCoinsList(
currency = request.params.fiatPriceCurrency,
interval = request.params.priceChangeInterval.toRequestParam(),
order = request.params.order.toRequestParam(),
search = request.params.searchText,
generalCoins = request.params.showUnder100kMarketCapTokens.not(),
offset = request.offset,
limit = request.limit,
).getOrThrow()
}
val last = res.tokens.size < request.limit
return BatchFetchResult.Success(
data = tokenListConverter.convert(res),
last = last,
)
@ -47,60 +54,14 @@ internal class DefaultMarketsTokenRepository(
},
)
private val tokenMarketsUpdateFetcher
get() = BatchUpdateFetcher<Int, List<TokenMarket>, TokenMarketUpdateRequest> { toUpdate, updateRequest ->
withContext(dispatcherProvider.io) {
val idsToUpdate = toUpdate.map { batch ->
batch.data.map { it.id }
}.flatten()
val updatedBatches = when (updateRequest) {
is TokenMarketUpdateRequest.UpdateChart -> {
val res = marketsApi.getCoinsListCharts(
coinIds = idsToUpdate,
interval = updateRequest.interval.toRequestParam(),
currency = updateRequest.currency,
).getOrThrow()
toUpdate.map { batch ->
batch.copy(
data = batch.data.map {
it.copy(
tokenCharts = tokenListChartsConverter.convert(
chartsToCopy = it.tokenCharts,
tokenId = it.id,
interval = updateRequest.interval,
value = res,
),
)
},
)
}
}
is TokenMarketUpdateRequest.UpdateQuotes -> {
val quotesRes = tangemTechApi.getQuotes(
currencyId = updateRequest.currencyId,
coinIds = idsToUpdate.joinToString(separator = ","),
fields = quoteFields.joinToString(separator = ","),
).getOrThrow()
toUpdate.map { batch ->
batch.copy(
data = batch.data.map {
it.copy(tokenQuotes = tokenQuotesConverter.convert(it.id, quotesRes))
},
)
}
}
}
BatchUpdateResult.Success(updatedBatches)
}
}
override suspend fun getTokenListFlow(
override fun getTokenListFlow(
batchingContext: BatchingContext<Int, TokenMarketListConfig, TokenMarketUpdateRequest>,
): BatchFlow<Int, List<TokenMarket>, TokenMarketUpdateRequest> {
val tokenMarketsUpdateFetcher = MarketsBatchUpdateFetcher(
tangemTechApi = tangemTechApi,
marketsApi = marketsApi,
)
return BatchListSource(
fetchDispatcher = dispatcherProvider.io,
context = batchingContext,
@ -109,13 +70,4 @@ internal class DefaultMarketsTokenRepository(
updateFetcher = tokenMarketsUpdateFetcher,
).toBatchFlow()
}
companion object {
private val quoteFields = listOf(
"price",
"priceChange24h",
"priceChange1w",
"priceChange30d",
)
}
}

View file

@ -0,0 +1,117 @@
package com.tangem.data.markets
import com.tangem.data.markets.converters.TokenListChartConverter
import com.tangem.data.markets.converters.TokenMarketChartsConverter
import com.tangem.data.markets.converters.TokenQuotesConverter
import com.tangem.data.markets.converters.toRequestParam
import com.tangem.data.markets.utils.retryOnError
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.domain.markets.TokenMarket
import com.tangem.domain.markets.TokenMarketUpdateRequest
import com.tangem.pagination.Batch
import com.tangem.pagination.BatchUpdateFetcher
import com.tangem.pagination.BatchUpdateResult
import kotlinx.coroutines.*
internal class MarketsBatchUpdateFetcher(
private val marketsApi: TangemTechMarketsApi,
private val tangemTechApi: TangemTechApi,
) : BatchUpdateFetcher<Int, List<TokenMarket>, TokenMarketUpdateRequest> {
private val tokenListChartsConverter = TokenMarketChartsConverter(TokenListChartConverter())
private val tokenQuotesConverter = TokenQuotesConverter()
override suspend fun BatchUpdateFetcher.UpdateContext<Int, List<TokenMarket>>.fetchUpdateAsync(
toUpdate: List<Batch<Int, List<TokenMarket>>>,
updateRequest: TokenMarketUpdateRequest,
) {
val idsToUpdate = toUpdate.map { batch ->
batch.key to batch.data.map { it.id }
}
when (updateRequest) {
is TokenMarketUpdateRequest.UpdateChart -> coroutineScope {
val updateTasks = idsToUpdate.map { batchIds ->
async {
retryOnError {
marketsApi.getCoinsListCharts(
coinIds = batchIds.second.joinToString(separator = ","),
interval = updateRequest.interval.toRequestParam(),
currency = updateRequest.currency,
).getOrThrow()
}
}
}
updateTasks.forEachIndexed { index, deferred ->
launch {
val res = deferred.await()
val batchToUpdate = toUpdate[index]
update {
val resBatch = changeChartsInBatches(
updateRequest = updateRequest,
batchToUpdate = batchToUpdate,
update = res,
)
BatchUpdateResult.Success(resBatch)
}
}
}
}
is TokenMarketUpdateRequest.UpdateQuotes -> {
val quotesRes = tangemTechApi.getQuotes(
currencyId = updateRequest.currencyId,
coinIds = idsToUpdate.joinToString(separator = ","),
fields = quoteFields.joinToString(separator = ","),
).getOrThrow()
update {
val res = toUpdate.map { batch ->
batch.copy(
data = batch.data.map {
it.copy(tokenQuotes = tokenQuotesConverter.convert(it.id, quotesRes))
},
)
}
BatchUpdateResult.Success(res)
}
}
}
}
private fun List<Batch<Int, List<TokenMarket>>>.changeChartsInBatches(
updateRequest: TokenMarketUpdateRequest.UpdateChart,
batchToUpdate: Batch<Int, List<TokenMarket>>,
update: TokenMarketChartListResponse,
): List<Batch<Int, List<TokenMarket>>> = mapNotNull { resultBatch ->
if (batchToUpdate.key != resultBatch.key) return@mapNotNull null
Batch(
key = batchToUpdate.key,
data = batchToUpdate.data.map {
it.copy(
tokenCharts = tokenListChartsConverter.convert(
chartsToCopy = it.tokenCharts,
tokenId = it.id,
interval = updateRequest.interval,
value = update,
),
)
},
)
}
companion object {
private val quoteFields = listOf(
"price",
"priceChange24h",
"priceChange1w",
"priceChange30d",
)
}
}

View file

@ -20,7 +20,7 @@ fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) {
fun PriceChangeInterval.toRequestParam(): String = when (this) {
PriceChangeInterval.H24 -> "24h"
PriceChangeInterval.WEEK -> "1w"
PriceChangeInterval.MONTH -> "1m"
PriceChangeInterval.MONTH -> "30d"
PriceChangeInterval.MONTH3 -> "3m"
PriceChangeInterval.MONTH6 -> "6m"
PriceChangeInterval.YEAR -> "1y"

View file

@ -14,7 +14,7 @@ class TokenMarketChartsConverter(
interval: PriceChangeInterval,
value: TokenMarketChartListResponse,
): TokenMarket.Charts {
val prices = requireNotNull(value.tokens[tokenId]) {
val prices = requireNotNull(value[tokenId]) {
"$tokenId is not found in the response. This shouldn't have happened."
}
return when (interval) {

View file

@ -20,9 +20,9 @@ class TokenMarketListConverter : Converter<TokenMarketListResponse, List<TokenMa
tokenQuotes = TokenQuotes(
currentPrice = token.currentPrice,
priceChanges = mapOf(
PriceChangeInterval.H24 to token.priceChangePercentage.h24,
PriceChangeInterval.WEEK to token.priceChangePercentage.week1,
PriceChangeInterval.MONTH to token.priceChangePercentage.day30,
PriceChangeInterval.H24 to token.priceChangePercentage.h24.movePointLeft(2),
PriceChangeInterval.WEEK to token.priceChangePercentage.week1.movePointLeft(2),
PriceChangeInterval.MONTH to token.priceChangePercentage.day30.movePointLeft(2),
),
),
tokenCharts = TokenMarket.Charts(null, null, null),

View file

@ -3,6 +3,7 @@ package com.tangem.data.markets.converters
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenQuotes
import java.math.BigDecimal
class TokenQuotesConverter {
@ -15,15 +16,9 @@ class TokenQuotesConverter {
"Price is not found in the QuotesResponse. This shouldn't have happened."
},
priceChanges = mapOf(
PriceChangeInterval.H24 to requireNotNull(quote.priceChange1w) {
"priceChange1w is not found in the QuotesResponse. This shouldn't have happened."
},
PriceChangeInterval.WEEK to requireNotNull(quote.priceChange1w) {
"priceChange1w is not found in the QuotesResponse. This shouldn't have happened."
},
PriceChangeInterval.MONTH to requireNotNull(quote.priceChange30d) {
"priceChange30d is not found in the QuotesResponse. This shouldn't have happened."
},
PriceChangeInterval.H24 to (quote.priceChange24h ?: BigDecimal.ZERO).movePointLeft(2),
PriceChangeInterval.WEEK to (quote.priceChange1w ?: BigDecimal.ZERO).movePointLeft(2),
PriceChangeInterval.MONTH to (quote.priceChange30d ?: BigDecimal.ZERO).movePointLeft(2),
),
)
}

View file

@ -3,6 +3,7 @@ package com.tangem.data.markets.di
import com.tangem.data.markets.DefaultMarketsTokenRepository
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.di.DevTangemApi
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -18,8 +19,8 @@ internal object MarketsDataModule {
@Provides
@Singleton
fun provideMarketsRepository(
marketsApi: TangemTechMarketsApi,
tangemTechApi: TangemTechApi,
@DevTangemApi marketsApi: TangemTechMarketsApi,
@DevTangemApi tangemTechApi: TangemTechApi,
dispatchers: CoroutineDispatcherProvider,
): MarketsTokenRepository {
return DefaultMarketsTokenRepository(

View file

@ -0,0 +1,27 @@
package com.tangem.data.markets.utils
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.yield
import timber.log.Timber
import kotlin.coroutines.cancellation.CancellationException
@Suppress("UnconditionalJumpStatementInLoop")
internal suspend fun <T> retryOnError(priority: Boolean = false, call: suspend () -> T): T {
while (true) {
return try {
call()
} catch (e: Exception) {
if (e is CancellationException) {
currentCoroutineContext().ensureActive()
}
Timber.e(e)
if (priority.not()) {
yield()
delay(timeMillis = 500)
}
continue
}
}
}