Updated on 2026-08-14
This commit is contained in:
parent
46ff455a6f
commit
e3eada68fd
35 changed files with 757 additions and 13 deletions
|
|
@ -0,0 +1,41 @@
|
||||||
|
package com.tangem.datasource.api.markets
|
||||||
|
|
||||||
|
import com.tangem.datasource.api.common.response.ApiResponse
|
||||||
|
import com.tangem.datasource.api.markets.models.response.*
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Path
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
interface TangemTechMarketsApi {
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
@GET("coins/list")
|
||||||
|
suspend fun getCoinsList(
|
||||||
|
@Query("currency") currency: String,
|
||||||
|
@Query("interval") interval: String,
|
||||||
|
@Query("offset") offset: Int,
|
||||||
|
@Query("limit") limit: Int,
|
||||||
|
@Query("order") order: String,
|
||||||
|
@Query("general_coins") generalCoins: Boolean,
|
||||||
|
@Query("search") search: String?,
|
||||||
|
): ApiResponse<TokenMarketListResponse>
|
||||||
|
|
||||||
|
@GET("coins/{coin_id}")
|
||||||
|
suspend fun getCoinMarketData(
|
||||||
|
@Path("coin_id") coinId: String,
|
||||||
|
@Query("currency") currency: String,
|
||||||
|
): ApiResponse<TokenMarketDetailsResponse>
|
||||||
|
|
||||||
|
@GET("coins/{coin_id}/history")
|
||||||
|
suspend fun getCoinChart(
|
||||||
|
@Query("currency") currency: String,
|
||||||
|
@Query("interval") interval: String,
|
||||||
|
): ApiResponse<TokenMarketChartResponse>
|
||||||
|
|
||||||
|
@GET("coins/history_preview")
|
||||||
|
suspend fun getCoinsListCharts(
|
||||||
|
@Query("coin_ids") coinIds: List<String>,
|
||||||
|
@Query("currency") currency: String,
|
||||||
|
@Query("interval") interval: String,
|
||||||
|
): ApiResponse<TokenMarketChartListResponse>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.tangem.datasource.api.markets.models.response
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
|
||||||
|
class TokenMarketChartListResponse(
|
||||||
|
@Json(name = "tokens")
|
||||||
|
val tokens: Map<String, TokenMarketChartResponse>,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.tangem.datasource.api.markets.models.response
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
data class TokenMarketChartResponse(
|
||||||
|
@Json(name = "prices")
|
||||||
|
val prices: Map<Long, BigDecimal>,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
package com.tangem.datasource.api.markets.models.response
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
data class TokenMarketDetailsResponse(
|
||||||
|
@Json(name = "id")
|
||||||
|
val id: String,
|
||||||
|
@Json(name = "name")
|
||||||
|
val name: String,
|
||||||
|
@Json(name = "symbol")
|
||||||
|
val symbol: String,
|
||||||
|
@Json(name = "active")
|
||||||
|
val active: Boolean,
|
||||||
|
@Json(name = "current_price")
|
||||||
|
val currentPrice: BigDecimal,
|
||||||
|
@Json(name = "price_change_percentage")
|
||||||
|
val priceChangePercentage: PriceChangePercentage,
|
||||||
|
@Json(name = "networks")
|
||||||
|
val networks: List<Network>,
|
||||||
|
@Json(name = "short_description")
|
||||||
|
val shortDescription: String?,
|
||||||
|
@Json(name = "full_description")
|
||||||
|
val fullDescription: String?,
|
||||||
|
@Json(name = "insights")
|
||||||
|
val insights: List<Insight>?,
|
||||||
|
@Json(name = "metrics")
|
||||||
|
val metrics: Metrics,
|
||||||
|
@Json(name = "links")
|
||||||
|
val links: Links,
|
||||||
|
@Json(name = "price_performance")
|
||||||
|
val pricePerformance: PricePerformance,
|
||||||
|
) {
|
||||||
|
data class PriceChangePercentage(
|
||||||
|
@Json(name = "24h")
|
||||||
|
val h24: BigDecimal,
|
||||||
|
@Json(name = "1w")
|
||||||
|
val week1: BigDecimal,
|
||||||
|
@Json(name = "1m")
|
||||||
|
val month1: BigDecimal,
|
||||||
|
@Json(name = "3m")
|
||||||
|
val month3: BigDecimal,
|
||||||
|
@Json(name = "6m")
|
||||||
|
val month6: BigDecimal,
|
||||||
|
@Json(name = "1y")
|
||||||
|
val year1: BigDecimal,
|
||||||
|
@Json(name = "all_time")
|
||||||
|
val allTime: BigDecimal,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Network(
|
||||||
|
@Json(name = "network_id")
|
||||||
|
val networkId: String,
|
||||||
|
@Json(name = "exchangeable")
|
||||||
|
val exchangeable: Boolean,
|
||||||
|
@Json(name = "contract_address")
|
||||||
|
val contractAddress: String,
|
||||||
|
@Json(name = "decimalCount")
|
||||||
|
val decimalCount: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Insight(
|
||||||
|
@Json(name = "holders_change")
|
||||||
|
val holdersChange: Change,
|
||||||
|
@Json(name = "liquidity_change")
|
||||||
|
val liquidityChange: Change,
|
||||||
|
@Json(name = "buy_pressure_change")
|
||||||
|
val buyPressureChange: Change,
|
||||||
|
@Json(name = "experienced_buyer_change")
|
||||||
|
val experiencedBuyerChange: Change,
|
||||||
|
) {
|
||||||
|
data class Change(
|
||||||
|
@Json(name = "1d")
|
||||||
|
val day1: Int,
|
||||||
|
@Json(name = "1w")
|
||||||
|
val week1: Int,
|
||||||
|
@Json(name = "1m")
|
||||||
|
val month1: Int,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Metrics(
|
||||||
|
@Json(name = "market_rating")
|
||||||
|
val marketRating: Int,
|
||||||
|
@Json(name = "circulating_supply")
|
||||||
|
val circulatingSupply: BigDecimal,
|
||||||
|
@Json(name = "market_cap")
|
||||||
|
val marketCap: BigDecimal,
|
||||||
|
@Json(name = "volume_24h")
|
||||||
|
val volume24h: BigDecimal,
|
||||||
|
@Json(name = "total_supply")
|
||||||
|
val totalSupply: BigDecimal,
|
||||||
|
@Json(name = "fully_diluted_valuation")
|
||||||
|
val fullyDilutedValuation: BigDecimal,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Links(
|
||||||
|
@Json(name = "official_links")
|
||||||
|
val officialLinks: List<Link> = emptyList(),
|
||||||
|
@Json(name = "social")
|
||||||
|
val social: List<Link> = emptyList(),
|
||||||
|
@Json(name = "repository")
|
||||||
|
val repository: List<Link> = emptyList(),
|
||||||
|
@Json(name = "blockchain_site")
|
||||||
|
val blockchainSite: List<Link> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Link(
|
||||||
|
@Json(name = "title")
|
||||||
|
val title: String?,
|
||||||
|
@Json(name = "id")
|
||||||
|
val id: String,
|
||||||
|
@Json(name = "link")
|
||||||
|
val url: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PricePerformance(
|
||||||
|
@Json(name = "high_price")
|
||||||
|
val highPrice: Price,
|
||||||
|
@Json(name = "low_price")
|
||||||
|
val lowPrice: Price,
|
||||||
|
) {
|
||||||
|
data class Price(
|
||||||
|
@Json(name = "24h")
|
||||||
|
val h24: BigDecimal,
|
||||||
|
@Json(name = "1m")
|
||||||
|
val month1: BigDecimal,
|
||||||
|
@Json(name = "all_time")
|
||||||
|
val allTime: BigDecimal,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package com.tangem.datasource.api.markets.models.response
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
data class TokenMarketListResponse(
|
||||||
|
@Json(name = "imageHost")
|
||||||
|
val imageHost: String,
|
||||||
|
@Json(name = "tokens")
|
||||||
|
val tokens: List<Token>,
|
||||||
|
@Json(name = "total")
|
||||||
|
val total: Int,
|
||||||
|
@Json(name = "limit")
|
||||||
|
val limit: Int,
|
||||||
|
@Json(name = "offset")
|
||||||
|
val offset: Int,
|
||||||
|
) {
|
||||||
|
data class Token(
|
||||||
|
@Json(name = "id")
|
||||||
|
val id: String,
|
||||||
|
@Json(name = "name")
|
||||||
|
val name: String,
|
||||||
|
@Json(name = "symbol")
|
||||||
|
val symbol: String,
|
||||||
|
@Json(name = "current_price")
|
||||||
|
val currentPrice: BigDecimal,
|
||||||
|
@Json(name = "price_change_percentage")
|
||||||
|
val priceChangePercentage: PriceChangePercentage,
|
||||||
|
@Json(name = "market_rating")
|
||||||
|
val marketRating: Int?,
|
||||||
|
@Json(name = "market_cap")
|
||||||
|
val marketCap: BigDecimal?,
|
||||||
|
) {
|
||||||
|
data class PriceChangePercentage(
|
||||||
|
@Json(name = "24h")
|
||||||
|
val h24: BigDecimal,
|
||||||
|
@Json(name = "1w")
|
||||||
|
val week1: BigDecimal,
|
||||||
|
@Json(name = "30d")
|
||||||
|
val day30: BigDecimal,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,10 @@ data class QuotesResponse(
|
||||||
@Json(name = "price")
|
@Json(name = "price")
|
||||||
val price: BigDecimal?,
|
val price: BigDecimal?,
|
||||||
@Json(name = "priceChange24h")
|
@Json(name = "priceChange24h")
|
||||||
val priceChange: BigDecimal?,
|
val priceChange24h: BigDecimal?,
|
||||||
|
@Json(name = "priceChange1w")
|
||||||
|
val priceChange1w: BigDecimal?,
|
||||||
|
@Json(name = "priceChange30d")
|
||||||
|
val priceChange30d: BigDecimal?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import com.squareup.moshi.Moshi
|
||||||
import com.tangem.datasource.BuildConfig
|
import com.tangem.datasource.BuildConfig
|
||||||
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
|
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
|
||||||
import com.tangem.datasource.api.express.TangemExpressApi
|
import com.tangem.datasource.api.express.TangemExpressApi
|
||||||
|
import com.tangem.datasource.api.markets.TangemTechMarketsApi
|
||||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||||
import com.tangem.datasource.api.tangemTech.TangemTechApiV2
|
import com.tangem.datasource.api.tangemTech.TangemTechApiV2
|
||||||
|
|
@ -128,6 +129,23 @@ class NetworkModule {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@DevTangemApi
|
||||||
|
@Singleton
|
||||||
|
fun provideCoinMarketsApi(
|
||||||
|
@NetworkMoshi moshi: Moshi,
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
appVersionProvider: AppVersionProvider,
|
||||||
|
): TangemTechMarketsApi {
|
||||||
|
return provideTangemTechApiInternal(
|
||||||
|
moshi = moshi,
|
||||||
|
context = context,
|
||||||
|
appVersionProvider = appVersionProvider,
|
||||||
|
baseUrl = DEV_V1_TANGEM_TECH_BASE_URL,
|
||||||
|
requestHeaders = listOf(AppVersionPlatformHeaders(appVersionProvider)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private inline fun <reified T> provideTangemTechApiInternal(
|
private inline fun <reified T> provideTangemTechApiInternal(
|
||||||
moshi: Moshi,
|
moshi: Moshi,
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ sealed class BatchFetchResult<out TData> {
|
||||||
*/
|
*/
|
||||||
data class Success<TData>(
|
data class Success<TData>(
|
||||||
val data: TData,
|
val data: TData,
|
||||||
val last: Boolean = false,
|
val last: Boolean,
|
||||||
) : BatchFetchResult<TData>()
|
) : BatchFetchResult<TData>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -269,10 +269,14 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
|
||||||
val batches = state.value.data
|
val batches = state.value.data
|
||||||
val batchesToUpdate = batches.filter { action.keys.contains(it.key) }
|
val batchesToUpdate = batches.filter { action.keys.contains(it.key) }
|
||||||
|
|
||||||
val result = updateFetcher.fetchUpdate(
|
val result = try {
|
||||||
toUpdate = batchesToUpdate,
|
updateFetcher.fetchUpdate(
|
||||||
updateRequest = action.updateRequest,
|
toUpdate = batchesToUpdate,
|
||||||
)
|
updateRequest = action.updateRequest,
|
||||||
|
)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
BatchUpdateResult.Error(t)
|
||||||
|
}
|
||||||
|
|
||||||
if (result is BatchUpdateResult.Success) {
|
if (result is BatchUpdateResult.Success) {
|
||||||
state.update { currentState ->
|
state.update { currentState ->
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.tangem.pagination
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
fun <TKey, TData, TUpdate> BatchListSource<TKey, TData, TUpdate>.toBatchFlow() =
|
||||||
|
object : BatchFlow<TKey, TData, TUpdate> {
|
||||||
|
override val state: StateFlow<BatchListState<TKey, TData>>
|
||||||
|
get() = this@toBatchFlow.state
|
||||||
|
override val updateResults: SharedFlow<Pair<TUpdate, BatchUpdateResult<TKey, TData>>>
|
||||||
|
get() = this@toBatchFlow.updateResults
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BatchFlow<TKey, TData, TUpdate> {
|
||||||
|
val state: StateFlow<BatchListState<TKey, TData>>
|
||||||
|
val updateResults: SharedFlow<Pair<TUpdate, BatchUpdateResult<TKey, TData>>>
|
||||||
|
}
|
||||||
|
|
@ -35,7 +35,10 @@ class LimitOffsetBatchFetcher<TRequestParams : Any, TData>(
|
||||||
request = requestParams,
|
request = requestParams,
|
||||||
)
|
)
|
||||||
|
|
||||||
val res = fetch(req)
|
val res = runCatching {
|
||||||
|
fetch(req)
|
||||||
|
}.getOrElse { BatchFetchResult.Error(it) }
|
||||||
|
|
||||||
lastRequest.value = req
|
lastRequest.value = req
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +64,10 @@ class LimitOffsetBatchFetcher<TRequestParams : Any, TData>(
|
||||||
last
|
last
|
||||||
}
|
}
|
||||||
|
|
||||||
val res = fetch(req)
|
val res = runCatching {
|
||||||
|
fetch(req)
|
||||||
|
}.getOrElse { BatchFetchResult.Error(it) }
|
||||||
|
|
||||||
lastRequest.value = req
|
lastRequest.value = req
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
data/markets/.gitignore
vendored
Normal file
1
data/markets/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
||||||
32
data/markets/build.gradle.kts
Normal file
32
data/markets/build.gradle.kts
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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."
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ internal class QuotesConverter : Converter<StoredQuote, Quote> {
|
||||||
return Quote(
|
return Quote(
|
||||||
rawCurrencyId = rawCurrencyId,
|
rawCurrencyId = rawCurrencyId,
|
||||||
fiatRate = responseQuote.price ?: BigDecimal.ZERO,
|
fiatRate = responseQuote.price ?: BigDecimal.ZERO,
|
||||||
priceChange = (responseQuote.priceChange ?: BigDecimal.ZERO).movePointLeft(2),
|
priceChange = (responseQuote.priceChange24h ?: BigDecimal.ZERO).movePointLeft(2),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1
domain/markets/.gitignore
vendored
Normal file
1
domain/markets/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
||||||
20
domain/markets/build.gradle.kts
Normal file
20
domain/markets/build.gradle.kts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
plugins {
|
||||||
|
alias(deps.plugins.android.library)
|
||||||
|
alias(deps.plugins.kotlin.android)
|
||||||
|
alias(deps.plugins.kotlin.serialization)
|
||||||
|
id("configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.tangem.domain.markets"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api(projects.domain.markets.models)
|
||||||
|
api(projects.domain.core)
|
||||||
|
api(projects.core.pagination)
|
||||||
|
|
||||||
|
implementation(deps.kotlin.serialization)
|
||||||
|
implementation(projects.domain.tokens.models)
|
||||||
|
}
|
||||||
1
domain/markets/models/.gitignore
vendored
Normal file
1
domain/markets/models/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
||||||
12
domain/markets/models/build.gradle.kts
Normal file
12
domain/markets/models/build.gradle.kts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
plugins {
|
||||||
|
alias(deps.plugins.kotlin.jvm)
|
||||||
|
alias(deps.plugins.kotlin.serialization)
|
||||||
|
id("configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.domain.core)
|
||||||
|
|
||||||
|
implementation(deps.kotlin.serialization)
|
||||||
|
implementation(deps.jodatime)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.tangem.domain.markets
|
||||||
|
|
||||||
|
enum class PriceChangeInterval {
|
||||||
|
H24, WEEK, MONTH, MONTH3, MONTH6, YEAR, ALL_TIME
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.tangem.domain.markets
|
||||||
|
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
data class TokenChart(
|
||||||
|
val interval: PriceChangeInterval,
|
||||||
|
val priceY: List<BigDecimal>,
|
||||||
|
val timeStamp: List<Long>,
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
require(priceY.size == timeStamp.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.tangem.domain.markets
|
||||||
|
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
data class TokenMarket(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val symbol: String,
|
||||||
|
val marketRating: Int?,
|
||||||
|
val marketCap: BigDecimal?,
|
||||||
|
val tokenQuotes: TokenQuotes,
|
||||||
|
val tokenCharts: Charts,
|
||||||
|
private val imageHost: String,
|
||||||
|
) {
|
||||||
|
|
||||||
|
data class Charts(
|
||||||
|
val h24: TokenChart?,
|
||||||
|
val week: TokenChart?,
|
||||||
|
val month: TokenChart?,
|
||||||
|
)
|
||||||
|
|
||||||
|
// 25x25
|
||||||
|
val imageUrlThumb =
|
||||||
|
"$imageHost/:thumb/:$id.png"
|
||||||
|
|
||||||
|
// 50x50
|
||||||
|
val imageUrlSmall =
|
||||||
|
"$imageHost/:small/:$id.png"
|
||||||
|
|
||||||
|
// 250x250
|
||||||
|
val imageUrlLarge =
|
||||||
|
"$imageHost/:large/:$id.png"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.tangem.domain.markets
|
||||||
|
|
||||||
|
data class TokenMarketListConfig(
|
||||||
|
val fiatPriceCurrency: String,
|
||||||
|
val searchText: String?,
|
||||||
|
val showUnder100kMarketCapTokens: Boolean,
|
||||||
|
val priceChangeInterval: Interval,
|
||||||
|
val order: Order,
|
||||||
|
) {
|
||||||
|
|
||||||
|
enum class Order {
|
||||||
|
ByRating, Trending, Buyers, TopGainers, TopLosers
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Interval {
|
||||||
|
H24, WEEK, MONTH,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.tangem.domain.markets
|
||||||
|
|
||||||
|
sealed class TokenMarketUpdateRequest {
|
||||||
|
|
||||||
|
data class UpdateQuotes(
|
||||||
|
val currencyId: String,
|
||||||
|
) : TokenMarketUpdateRequest()
|
||||||
|
|
||||||
|
data class UpdateChart(
|
||||||
|
val interval: PriceChangeInterval,
|
||||||
|
val currency: String,
|
||||||
|
) : TokenMarketUpdateRequest()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.tangem.domain.markets
|
||||||
|
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
data class TokenQuotes(
|
||||||
|
val currentPrice: BigDecimal,
|
||||||
|
val priceChanges: Map<PriceChangeInterval, BigDecimal>,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.tangem.domain.markets.repositories
|
||||||
|
|
||||||
|
import com.tangem.domain.markets.TokenMarket
|
||||||
|
import com.tangem.domain.markets.TokenMarketListConfig
|
||||||
|
import com.tangem.domain.markets.TokenMarketUpdateRequest
|
||||||
|
import com.tangem.pagination.BatchFlow
|
||||||
|
import com.tangem.pagination.BatchingContext
|
||||||
|
|
||||||
|
interface MarketsTokenRepository {
|
||||||
|
|
||||||
|
suspend fun getTokenListFlow(
|
||||||
|
batchingContext: BatchingContext<Int, TokenMarketListConfig, TokenMarketUpdateRequest>,
|
||||||
|
): BatchFlow<Int, List<TokenMarket>, TokenMarketUpdateRequest>
|
||||||
|
}
|
||||||
|
|
@ -8,12 +8,9 @@ import java.math.BigDecimal
|
||||||
* @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 values The values representing the cryptocurrency's price changes over a 24-hour period,
|
|
||||||
* suitable for chart plotting.
|
|
||||||
*/
|
*/
|
||||||
data class Quote(
|
data class Quote(
|
||||||
val rawCurrencyId: String,
|
val rawCurrencyId: String,
|
||||||
val fiatRate: BigDecimal,
|
val fiatRate: BigDecimal,
|
||||||
val priceChange: BigDecimal,
|
val priceChange: BigDecimal,
|
||||||
val values: List<Double>? = null,
|
|
||||||
)
|
)
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
org.gradle.jvmargs = -Xmx4096m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
org.gradle.jvmargs = -Xmx6144m -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
# This option should only be used with decoupled projects. More details, visit
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,8 @@ include(":domain:qr-scanning:models")
|
||||||
include(":domain:staking")
|
include(":domain:staking")
|
||||||
include(":domain:staking:models")
|
include(":domain:staking:models")
|
||||||
include(":domain:wallet-connect")
|
include(":domain:wallet-connect")
|
||||||
|
include(":domain:markets")
|
||||||
|
include(":domain:markets:models")
|
||||||
// endregion Domain modules
|
// endregion Domain modules
|
||||||
|
|
||||||
// region Data modules
|
// region Data modules
|
||||||
|
|
@ -245,4 +247,5 @@ include(":data:feedback")
|
||||||
include(":data:qr-scanning")
|
include(":data:qr-scanning")
|
||||||
include(":data:staking")
|
include(":data:staking")
|
||||||
include(":data:wallet-connect")
|
include(":data:wallet-connect")
|
||||||
|
include(":data:markets")
|
||||||
// endregion Data modules
|
// endregion Data modules
|
||||||
Loading…
Add table
Add a link
Reference in a new issue