Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-09 12:45:17 +03:00
parent 46ff455a6f
commit e3eada68fd
35 changed files with 757 additions and 13 deletions

1
data/markets/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,32 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.data.markets"
}
dependencies {
implementation(projects.core.datasource)
implementation(projects.core.utils)
implementation(projects.core.pagination)
implementation(projects.domain.tokens.models)
implementation(projects.domain.markets)
// region DI
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
// endregion
// region Others dependencies
implementation(deps.kotlin.coroutines)
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(projects.libs.blockchainSdk)
// endregion
}

View file

@ -0,0 +1,121 @@
package com.tangem.data.markets
import com.tangem.data.markets.converters.*
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.domain.markets.*
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
internal class DefaultMarketsTokenRepository(
private val marketsApi: TangemTechMarketsApi,
private val tangemTechApi: TangemTechApi,
private val dispatcherProvider: CoroutineDispatcherProvider,
) : 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()
val last = res.tokens.size < params.limit
BatchFetchResult.Success(
data = tokenListConverter.convert(res),
last = last,
)
}
},
)
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(
batchingContext: BatchingContext<Int, TokenMarketListConfig, TokenMarketUpdateRequest>,
): BatchFlow<Int, List<TokenMarket>, TokenMarketUpdateRequest> {
return BatchListSource(
fetchDispatcher = dispatcherProvider.io,
context = batchingContext,
generateNewKey = { it.size },
batchFetcher = tokenMarketsFetcher,
updateFetcher = tokenMarketsUpdateFetcher,
).toBatchFlow()
}
companion object {
private val quoteFields = listOf(
"price",
"priceChange24h",
"priceChange1w",
"priceChange30d",
)
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.data.markets.converters
import com.tangem.datasource.api.markets.models.response.TokenMarketChartResponse
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenChart
class TokenListChartConverter {
fun convert(interval: PriceChangeInterval, value: TokenMarketChartResponse): TokenChart {
return TokenChart(
interval = interval,
priceY = value.prices.values.toList(),
timeStamp = value.prices.keys.toList(),
)
}
}

View file

@ -0,0 +1,28 @@
package com.tangem.data.markets.converters
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenMarketListConfig
fun TokenMarketListConfig.Interval.toRequestParam(): String = when (this) {
TokenMarketListConfig.Interval.H24 -> "24h"
TokenMarketListConfig.Interval.WEEK -> "1w"
TokenMarketListConfig.Interval.MONTH -> "30d"
}
fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) {
TokenMarketListConfig.Order.ByRating -> "rating"
TokenMarketListConfig.Order.Trending -> "trending"
TokenMarketListConfig.Order.Buyers -> "buyers"
TokenMarketListConfig.Order.TopGainers -> "gainers"
TokenMarketListConfig.Order.TopLosers -> "losers"
}
fun PriceChangeInterval.toRequestParam(): String = when (this) {
PriceChangeInterval.H24 -> "24h"
PriceChangeInterval.WEEK -> "1w"
PriceChangeInterval.MONTH -> "1m"
PriceChangeInterval.MONTH3 -> "3m"
PriceChangeInterval.MONTH6 -> "6m"
PriceChangeInterval.YEAR -> "1y"
PriceChangeInterval.ALL_TIME -> "all"
}

View file

@ -0,0 +1,33 @@
package com.tangem.data.markets.converters
import com.tangem.datasource.api.markets.models.response.TokenMarketChartListResponse
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenMarket
class TokenMarketChartsConverter(
private val tokenListChartConverter: TokenListChartConverter,
) {
fun convert(
chartsToCopy: TokenMarket.Charts,
tokenId: String,
interval: PriceChangeInterval,
value: TokenMarketChartListResponse,
): TokenMarket.Charts {
val prices = requireNotNull(value.tokens[tokenId]) {
"$tokenId is not found in the response. This shouldn't have happened."
}
return when (interval) {
PriceChangeInterval.H24 -> chartsToCopy.copy(
h24 = tokenListChartConverter.convert(interval, prices),
)
PriceChangeInterval.WEEK -> chartsToCopy.copy(
week = tokenListChartConverter.convert(interval, prices),
)
PriceChangeInterval.MONTH -> chartsToCopy.copy(
month = tokenListChartConverter.convert(interval, prices),
)
else -> error("unsupported interval=$interval. This shouldn't have happened.")
}
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.data.markets.converters
import com.tangem.datasource.api.markets.models.response.TokenMarketListResponse
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenMarket
import com.tangem.domain.markets.TokenQuotes
import com.tangem.utils.converter.Converter
class TokenMarketListConverter : Converter<TokenMarketListResponse, List<TokenMarket>> {
override fun convert(value: TokenMarketListResponse): List<TokenMarket> {
return value.tokens.map { token ->
TokenMarket(
id = token.id,
name = token.name,
symbol = token.symbol,
marketRating = token.marketRating,
marketCap = token.marketCap,
imageHost = value.imageHost,
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,
),
),
tokenCharts = TokenMarket.Charts(null, null, null),
)
}
}
}

View file

@ -0,0 +1,30 @@
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
class TokenQuotesConverter {
fun convert(tokenId: String, value: QuotesResponse): TokenQuotes {
val quote = requireNotNull(value.quotes[tokenId]) {
"$tokenId is not found in the response. This shouldn't have happened."
}
return TokenQuotes(
currentPrice = requireNotNull(quote.price) {
"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."
},
),
)
}
}

View file

@ -0,0 +1,31 @@
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.domain.markets.repositories.MarketsTokenRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object MarketsDataModule {
@Provides
@Singleton
fun provideMarketsRepository(
marketsApi: TangemTechMarketsApi,
tangemTechApi: TangemTechApi,
dispatchers: CoroutineDispatcherProvider,
): MarketsTokenRepository {
return DefaultMarketsTokenRepository(
marketsApi = marketsApi,
tangemTechApi = tangemTechApi,
dispatcherProvider = dispatchers,
)
}
}