Updated on 2026-08-14

This commit is contained in:
Tangem 2024-09-16 18:24:55 +03:00
parent b32cc4a928
commit 2ba0f3d4a1
42 changed files with 581 additions and 74 deletions

View file

@ -14,6 +14,8 @@ dependencies {
implementation(projects.core.datasource)
implementation(projects.core.utils)
implementation(projects.core.pagination)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.domain.legacy)
implementation(projects.domain.markets)

View file

@ -4,13 +4,16 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.compatibility.applyL2Compatibility
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.currency.getNetwork
import com.tangem.data.common.utils.retryOnError
import com.tangem.data.markets.analytics.MarketsDataAnalyticsEvent
import com.tangem.data.markets.converters.TokenChartConverter
import com.tangem.data.markets.converters.TokenMarketInfoConverter
import com.tangem.data.markets.converters.TokenMarketListConverter
import com.tangem.data.markets.converters.toRequestParam
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
@ -31,6 +34,7 @@ internal class DefaultMarketsTokenRepository(
private val tangemTechApi: TangemTechApi,
private val userWalletsStore: UserWalletsStore,
private val dispatcherProvider: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
) : MarketsTokenRepository {
private fun createTokenMarketsFetcher(firstBatchSize: Int, nextBatchSize: Int) = LimitOffsetBatchFetcher(
@ -62,10 +66,14 @@ internal class DefaultMarketsTokenRepository(
// we shouldn't infinitely retry on the first batch request
val res = if (isFirstBatchFetching) {
requestCall()
catchApiErrorAndSendEvent(errorEvent = MarketsDataAnalyticsEvent.List.Error) {
requestCall()
}
} else {
retryOnError(priority = true) {
requestCall()
catchApiErrorAndSendEvent(errorEvent = MarketsDataAnalyticsEvent.List.Error) {
requestCall()
}
}
}
@ -92,6 +100,9 @@ internal class DefaultMarketsTokenRepository(
val tokenMarketsUpdateFetcher = MarketsBatchUpdateFetcher(
tangemTechApi = tangemTechApi,
marketsApi = marketsApi,
onApiError = {
analyticsEventHandler.send(MarketsDataAnalyticsEvent.List.Error.toEvent())
},
)
val atomicInteger = AtomicInteger(0)
@ -109,6 +120,7 @@ internal class DefaultMarketsTokenRepository(
fiatCurrencyCode: String,
interval: PriceChangeInterval,
tokenId: String,
tokenSymbol: String,
): TokenChart {
val mappedTokenId = getTokenIdIfL2Network(tokenId)
val response = marketsApi.getCoinChart(
@ -117,13 +129,23 @@ internal class DefaultMarketsTokenRepository(
interval = interval.toRequestParam(),
)
return TokenChartConverter.convert(interval, response.getOrThrow())
val result = catchApiErrorAndSendEvent(
errorEvent = MarketsDataAnalyticsEvent.Details.Error(
request = MarketsDataAnalyticsEvent.Details.Error.Request.Chart,
tokenSymbol = tokenSymbol,
),
) {
response.getOrThrow()
}
return TokenChartConverter.convert(interval, result)
}
override suspend fun getChartPreview(
fiatCurrencyCode: String,
interval: PriceChangeInterval,
tokenId: String,
tokenSymbol: String,
): TokenChart {
val mappedTokenId = getTokenIdIfL2Network(tokenId)
val response = marketsApi.getCoinsListCharts(
@ -132,7 +154,11 @@ internal class DefaultMarketsTokenRepository(
interval = interval.toRequestParam(),
)
val chart = response.getOrThrow()[mappedTokenId] ?: error("No chart preview data for token $mappedTokenId")
val chart = catchApiErrorAndSendEvent(errorEvent = MarketsDataAnalyticsEvent.List.Error) {
response.getOrThrow()[mappedTokenId] ?: error(
"No chart preview data for the token $mappedTokenId",
)
}
return TokenChartConverter.convert(interval, chart)
}
@ -140,6 +166,7 @@ internal class DefaultMarketsTokenRepository(
override suspend fun getTokenInfo(
fiatCurrencyCode: String,
tokenId: String,
tokenSymbol: String,
languageCode: String,
): TokenMarketInfo {
val response = marketsApi.getCoinMarketData(
@ -148,12 +175,22 @@ internal class DefaultMarketsTokenRepository(
language = languageCode,
)
val resultResponse = response.getOrThrow().applyL2Compatibility(tokenId)
val result = catchApiErrorAndSendEvent(
errorEvent = MarketsDataAnalyticsEvent.Details.Error(
request = MarketsDataAnalyticsEvent.Details.Error.Request.Info,
tokenSymbol = tokenSymbol,
),
) {
response.getOrThrow()
}
val resultResponse = result.applyL2Compatibility(tokenId)
return TokenMarketInfoConverter.convert(resultResponse)
}
override suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String): TokenQuotes {
// TODO change method when backend is ready
// add error analytics event
val response = marketsApi.getCoinMarketData(
currency = fiatCurrencyCode,
coinId = tokenId,
@ -194,4 +231,16 @@ internal class DefaultMarketsTokenRepository(
)
}
}
private inline fun <T> catchApiErrorAndSendEvent(errorEvent: MarketsDataAnalyticsEvent, block: () -> T): T {
return try {
block()
} catch (e: ApiResponseError.HttpException) {
analyticsEventHandler.send(errorEvent.toEvent())
throw e
} catch (e: ApiResponseError.TimeoutException) {
analyticsEventHandler.send(errorEvent.toEvent())
throw e
}
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.data.common.utils.retryOnError
import com.tangem.data.markets.converters.TokenMarketChartsConverter
import com.tangem.data.markets.converters.TokenQuotesShortConverter
import com.tangem.data.markets.converters.toRequestParam
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.markets.models.response.TokenMarketChartListResponse
@ -20,6 +21,7 @@ import kotlinx.coroutines.launch
internal class MarketsBatchUpdateFetcher(
private val marketsApi: TangemTechMarketsApi,
private val tangemTechApi: TangemTechApi,
private val onApiError: () -> Unit,
) : BatchUpdateFetcher<Int, List<TokenMarket>, TokenMarketUpdateRequest> {
override suspend fun BatchUpdateFetcher.UpdateContext<Int, List<TokenMarket>>.fetchUpdateAsync(
@ -35,11 +37,13 @@ internal class MarketsBatchUpdateFetcher(
val updateTasks = idsToUpdate.map { batchIds ->
async {
retryOnError {
marketsApi.getCoinsListCharts(
coinIds = batchIds.second.joinToString(separator = ","),
interval = updateRequest.interval.toRequestParam(),
currency = updateRequest.currency,
).getOrThrow()
catchApiError(onApiError) {
marketsApi.getCoinsListCharts(
coinIds = batchIds.second.joinToString(separator = ","),
interval = updateRequest.interval.toRequestParam(),
currency = updateRequest.currency,
).getOrThrow()
}
}
}
}
@ -62,11 +66,13 @@ internal class MarketsBatchUpdateFetcher(
}
is TokenMarketUpdateRequest.UpdateQuotes -> {
val quotesRes = retryOnError {
tangemTechApi.getQuotes(
currencyId = updateRequest.currencyId,
coinIds = idsToUpdate.map { it.second }.flatten().joinToString(separator = ","),
fields = quoteFields.joinToString(separator = ","),
).getOrThrow()
catchApiError(onApiError) {
tangemTechApi.getQuotes(
currencyId = updateRequest.currencyId,
coinIds = idsToUpdate.map { it.second }.flatten().joinToString(separator = ","),
fields = quoteFields.joinToString(separator = ","),
).getOrThrow()
}
}
update {
@ -106,6 +112,15 @@ internal class MarketsBatchUpdateFetcher(
)
}
private inline fun <T> catchApiError(onError: () -> Unit, block: () -> T): T {
return try {
block()
} catch (e: ApiResponseError) {
onError()
throw e
}
}
companion object {
private val quoteFields = listOf(
"price",

View file

@ -0,0 +1,42 @@
package com.tangem.data.markets.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
sealed interface MarketsDataAnalyticsEvent {
sealed class List(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Markets", event = event, params = params), MarketsDataAnalyticsEvent {
data object Error : List(event = "Data Error")
}
sealed class Details(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Markets / Chart", event = event, params = params), MarketsDataAnalyticsEvent {
data class Error(
val request: Request,
val tokenSymbol: String,
) : Details(
event = "Data Error",
params = mapOf(
"Source" to request.source,
"Token" to tokenSymbol,
),
) {
enum class Request(val source: String) {
Chart("Chart"),
Info("Blocks"),
}
}
}
fun toEvent(): AnalyticsEvent = when (this) {
is List -> this
is Details -> this
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.data.markets.di
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.markets.DefaultMarketsTokenRepository
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
@ -24,12 +25,14 @@ internal object MarketsDataModule {
@DevTangemApi tangemTechApi: TangemTechApi,
userWalletsStore: UserWalletsStore,
dispatchers: CoroutineDispatcherProvider,
analyticsEventHandler: AnalyticsEventHandler,
): MarketsTokenRepository {
return DefaultMarketsTokenRepository(
marketsApi = marketsApi,
tangemTechApi = tangemTechApi,
dispatcherProvider = dispatchers,
userWalletsStore = userWalletsStore,
analyticsEventHandler = analyticsEventHandler,
)
}
}