From e3eada68fd54aa589ea62e8787c4612417b81d83 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Jul 2024 12:45:17 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../api/markets/TangemTechMarketsApi.kt | 41 ++++++ .../response/TokenMarketChartListResponse.kt | 8 ++ .../response/TokenMarketChartResponse.kt | 9 ++ .../response/TokenMarketDetailsResponse.kt | 132 ++++++++++++++++++ .../response/TokenMarketListResponse.kt | 43 ++++++ .../api/tangemTech/models/QuotesResponse.kt | 6 +- .../com/tangem/datasource/di/NetworkModule.kt | 18 +++ .../com/tangem/pagination/BatchFetchResult.kt | 2 +- .../com/tangem/pagination/BatchListSource.kt | 12 +- .../tangem/pagination/BatchListSourceFlow.kt | 17 +++ .../fetcher/LimitOffsetBatchFetcher.kt | 10 +- data/markets/.gitignore | 1 + data/markets/build.gradle.kts | 32 +++++ .../markets/DefaultMarketsTokenRepository.kt | 121 ++++++++++++++++ .../converters/TokenListChartConverter.kt | 16 +++ .../converters/TokenListConfigConverters.kt | 28 ++++ .../converters/TokenMarketChartsConverter.kt | 33 +++++ .../converters/TokenMarketListConverter.kt | 32 +++++ .../converters/TokenQuotesConverter.kt | 30 ++++ .../data/markets/di/MarketsDataModule.kt | 31 ++++ .../data/tokens/utils/QuotesConverter.kt | 2 +- domain/markets/.gitignore | 1 + domain/markets/build.gradle.kts | 20 +++ domain/markets/models/.gitignore | 1 + domain/markets/models/build.gradle.kts | 12 ++ .../domain/markets/PriceChangeInterval.kt | 5 + .../com/tangem/domain/markets/TokenChart.kt | 13 ++ .../com/tangem/domain/markets/TokenMarket.kt | 33 +++++ .../domain/markets/TokenMarketListConfig.kt | 18 +++ .../markets/TokenMarketUpdateRequest.kt | 13 ++ .../com/tangem/domain/markets/TokenQuotes.kt | 8 ++ .../repositories/MarketsTokenRepository.kt | 14 ++ .../com/tangem/domain/tokens/model/Quote.kt | 3 - gradle.properties | 2 +- settings.gradle.kts | 3 + 35 files changed, 757 insertions(+), 13 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartListResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketDetailsResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchListSourceFlow.kt create mode 100644 data/markets/.gitignore create mode 100644 data/markets/build.gradle.kts create mode 100644 data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt create mode 100644 data/markets/src/main/java/com/tangem/data/markets/converters/TokenListChartConverter.kt create mode 100644 data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt create mode 100644 data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt create mode 100644 data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt create mode 100644 data/markets/src/main/java/com/tangem/data/markets/converters/TokenQuotesConverter.kt create mode 100644 data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt create mode 100644 domain/markets/.gitignore create mode 100644 domain/markets/build.gradle.kts create mode 100644 domain/markets/models/.gitignore create mode 100644 domain/markets/models/build.gradle.kts create mode 100644 domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PriceChangeInterval.kt create mode 100644 domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenChart.kt create mode 100644 domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt create mode 100644 domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt create mode 100644 domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketUpdateRequest.kt create mode 100644 domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt create mode 100644 domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt new file mode 100644 index 0000000000..0ded78dbab --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt @@ -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 + + @GET("coins/{coin_id}") + suspend fun getCoinMarketData( + @Path("coin_id") coinId: String, + @Query("currency") currency: String, + ): ApiResponse + + @GET("coins/{coin_id}/history") + suspend fun getCoinChart( + @Query("currency") currency: String, + @Query("interval") interval: String, + ): ApiResponse + + @GET("coins/history_preview") + suspend fun getCoinsListCharts( + @Query("coin_ids") coinIds: List, + @Query("currency") currency: String, + @Query("interval") interval: String, + ): ApiResponse +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartListResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartListResponse.kt new file mode 100644 index 0000000000..0d1667ee24 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartListResponse.kt @@ -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, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartResponse.kt new file mode 100644 index 0000000000..777bbc1228 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartResponse.kt @@ -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, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketDetailsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketDetailsResponse.kt new file mode 100644 index 0000000000..227496a66e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketDetailsResponse.kt @@ -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, + @Json(name = "short_description") + val shortDescription: String?, + @Json(name = "full_description") + val fullDescription: String?, + @Json(name = "insights") + val insights: List?, + @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 = emptyList(), + @Json(name = "social") + val social: List = emptyList(), + @Json(name = "repository") + val repository: List = emptyList(), + @Json(name = "blockchain_site") + val blockchainSite: List = 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, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt new file mode 100644 index 0000000000..4157bd2bd6 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt @@ -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, + @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, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/QuotesResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/QuotesResponse.kt index 2155d0bf77..e1e9e07e40 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/QuotesResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/QuotesResponse.kt @@ -12,6 +12,10 @@ data class QuotesResponse( @Json(name = "price") val price: BigDecimal?, @Json(name = "priceChange24h") - val priceChange: BigDecimal?, + val priceChange24h: BigDecimal?, + @Json(name = "priceChange1w") + val priceChange1w: BigDecimal?, + @Json(name = "priceChange30d") + val priceChange30d: BigDecimal?, ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index ed7b6b5621..d2b395d5f9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -5,6 +5,7 @@ import com.squareup.moshi.Moshi import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory 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.tangemTech.TangemTechApi 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 provideTangemTechApiInternal( moshi: Moshi, context: Context, diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt index a247310b9c..a358df8bf2 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt @@ -16,7 +16,7 @@ sealed class BatchFetchResult { */ data class Success( val data: TData, - val last: Boolean = false, + val last: Boolean, ) : BatchFetchResult() /** diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt index fc1d0bd1c2..77e58eafcf 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt @@ -269,10 +269,14 @@ private class DefaultBatchListSource val batches = state.value.data val batchesToUpdate = batches.filter { action.keys.contains(it.key) } - val result = updateFetcher.fetchUpdate( - toUpdate = batchesToUpdate, - updateRequest = action.updateRequest, - ) + val result = try { + updateFetcher.fetchUpdate( + toUpdate = batchesToUpdate, + updateRequest = action.updateRequest, + ) + } catch (t: Throwable) { + BatchUpdateResult.Error(t) + } if (result is BatchUpdateResult.Success) { state.update { currentState -> diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListSourceFlow.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListSourceFlow.kt new file mode 100644 index 0000000000..258a20b8d0 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSourceFlow.kt @@ -0,0 +1,17 @@ +package com.tangem.pagination + +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow + +fun BatchListSource.toBatchFlow() = + object : BatchFlow { + override val state: StateFlow> + get() = this@toBatchFlow.state + override val updateResults: SharedFlow>> + get() = this@toBatchFlow.updateResults + } + +interface BatchFlow { + val state: StateFlow> + val updateResults: SharedFlow>> +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt index 01059c4676..3dd04c24c7 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt @@ -35,7 +35,10 @@ class LimitOffsetBatchFetcher( request = requestParams, ) - val res = fetch(req) + val res = runCatching { + fetch(req) + }.getOrElse { BatchFetchResult.Error(it) } + lastRequest.value = req return res } @@ -61,7 +64,10 @@ class LimitOffsetBatchFetcher( last } - val res = fetch(req) + val res = runCatching { + fetch(req) + }.getOrElse { BatchFetchResult.Error(it) } + lastRequest.value = req return res } diff --git a/data/markets/.gitignore b/data/markets/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/markets/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/markets/build.gradle.kts b/data/markets/build.gradle.kts new file mode 100644 index 0000000000..8e9347ad54 --- /dev/null +++ b/data/markets/build.gradle.kts @@ -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 +} diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt new file mode 100644 index 0000000000..65ebc717bb --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -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>( + 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, 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, + ): BatchFlow, 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", + ) + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListChartConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListChartConverter.kt new file mode 100644 index 0000000000..0b4cf678c6 --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListChartConverter.kt @@ -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(), + ) + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt new file mode 100644 index 0000000000..eee63ef4e9 --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt @@ -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" +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt new file mode 100644 index 0000000000..aade2d2073 --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt @@ -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.") + } + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt new file mode 100644 index 0000000000..ea8587e123 --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt @@ -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> { + + override fun convert(value: TokenMarketListResponse): List { + 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), + ) + } + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenQuotesConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenQuotesConverter.kt new file mode 100644 index 0000000000..a2b61460d0 --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenQuotesConverter.kt @@ -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." + }, + ), + ) + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt b/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt new file mode 100644 index 0000000000..be8a925029 --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt index d76681efa4..85c903ceec 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt @@ -13,7 +13,7 @@ internal class QuotesConverter : Converter { return Quote( rawCurrencyId = rawCurrencyId, fiatRate = responseQuote.price ?: BigDecimal.ZERO, - priceChange = (responseQuote.priceChange ?: BigDecimal.ZERO).movePointLeft(2), + priceChange = (responseQuote.priceChange24h ?: BigDecimal.ZERO).movePointLeft(2), ) } } \ No newline at end of file diff --git a/domain/markets/.gitignore b/domain/markets/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/markets/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/markets/build.gradle.kts b/domain/markets/build.gradle.kts new file mode 100644 index 0000000000..5529922be2 --- /dev/null +++ b/domain/markets/build.gradle.kts @@ -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) +} \ No newline at end of file diff --git a/domain/markets/models/.gitignore b/domain/markets/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/markets/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/markets/models/build.gradle.kts b/domain/markets/models/build.gradle.kts new file mode 100644 index 0000000000..7558c5ca61 --- /dev/null +++ b/domain/markets/models/build.gradle.kts @@ -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) +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PriceChangeInterval.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PriceChangeInterval.kt new file mode 100644 index 0000000000..ff9b0ba89a --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PriceChangeInterval.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.markets + +enum class PriceChangeInterval { + H24, WEEK, MONTH, MONTH3, MONTH6, YEAR, ALL_TIME +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenChart.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenChart.kt new file mode 100644 index 0000000000..d239ae395a --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenChart.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.markets + +import java.math.BigDecimal + +data class TokenChart( + val interval: PriceChangeInterval, + val priceY: List, + val timeStamp: List, +) { + init { + require(priceY.size == timeStamp.size) + } +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt new file mode 100644 index 0000000000..c9587db1f9 --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt @@ -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" +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt new file mode 100644 index 0000000000..ad952091b6 --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt @@ -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, + } +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketUpdateRequest.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketUpdateRequest.kt new file mode 100644 index 0000000000..bcac52fddb --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketUpdateRequest.kt @@ -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() +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt new file mode 100644 index 0000000000..e26b6abb9f --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.markets + +import java.math.BigDecimal + +data class TokenQuotes( + val currentPrice: BigDecimal, + val priceChanges: Map, +) \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt new file mode 100644 index 0000000000..97aeb80c43 --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt @@ -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, + ): BatchFlow, TokenMarketUpdateRequest> +} \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt index 9f5dfbe387..3db9280b76 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt @@ -8,12 +8,9 @@ import java.math.BigDecimal * @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 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( val rawCurrencyId: String, val fiatRate: BigDecimal, val priceChange: BigDecimal, - val values: List? = null, ) \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 7218026460..b2ef2745d2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # 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. # 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 diff --git a/settings.gradle.kts b/settings.gradle.kts index 6d1f411939..0d6be44673 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -224,6 +224,8 @@ include(":domain:qr-scanning:models") include(":domain:staking") include(":domain:staking:models") include(":domain:wallet-connect") +include(":domain:markets") +include(":domain:markets:models") // endregion Domain modules // region Data modules @@ -245,4 +247,5 @@ include(":data:feedback") include(":data:qr-scanning") include(":data:staking") include(":data:wallet-connect") +include(":data:markets") // endregion Data modules \ No newline at end of file