Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-27 12:51:42 +03:00
commit 6b7a309a37
17 changed files with 389 additions and 5 deletions

View file

@ -31,6 +31,7 @@ dependencies {
implementation(projects.domain.staking.models)
implementation(projects.domain.onramp.models)
implementation(projects.domain.models)
implementation(projects.features.onramp.api)
/** Tangem libraries */
implementation(deps.tangem.blockchain)

View file

@ -122,6 +122,9 @@ interface TangemTechApi {
@Body body: SeedPhraseNotificationDTO,
): ApiResponse<Unit>
@GET("hot_crypto")
suspend fun getHotCrypto(@Query("currency") currencyId: String): ApiResponse<HotCryptoResponse>
companion object {
val marketsQuoteFields = listOf(
"price",

View file

@ -0,0 +1,24 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class HotCryptoResponse(
@Json(name = "imageHost") val imageHost: String? = null,
@Json(name = "tokens") val tokens: List<Token>,
) {
@JsonClass(generateAdapter = true)
data class Token(
@Json(name = "id") val id: String,
@Json(name = "symbol") val symbol: String,
@Json(name = "name") val name: String,
@Json(name = "network_id") val networkId: String,
@Json(name = "current_price") val currentPrice: BigDecimal,
@Json(name = "price_change_percentage_24h") val priceChangePercentage: BigDecimal,
@Json(name = "contractAddress") val contractAddress: String? = null, // specific for token
@Json(name = "decimal_count") val decimalCount: Int? = null, // specific for token
)
}

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.di.exchangeservice
import com.tangem.datasource.exchangeservice.hotcrypto.DefaultHotCryptoLoader
import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoLoader
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface HotCryptoLoaderModule {
@Binds
@Singleton
fun bindExpressServiceLoader(defaultHotCryptoLoader: DefaultHotCryptoLoader): HotCryptoLoader
}

View file

@ -0,0 +1,81 @@
package com.tangem.datasource.exchangeservice.hotcrypto
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import timber.log.Timber
import javax.inject.Inject
/**
* Default implementation of [HotCryptoLoader]
*
* @property tangemTechApi tangem tech api
* @property hotCryptoResponseStore store of [HotCryptoResponse]
* @property appPreferencesStore app preferences store
* @property dispatchers dispatchers
* @property onrampFeatureToggles onramp feature toggles
*
[REDACTED_AUTHOR]
*/
internal class DefaultHotCryptoLoader @Inject constructor(
private val tangemTechApi: TangemTechApi,
private val hotCryptoResponseStore: HotCryptoResponseStore,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
private val onrampFeatureToggles: OnrampFeatureToggles,
) : HotCryptoLoader {
override suspend fun fetch(tokens: List<UserTokensResponse.Token>) {
updateInternal { hotToken ->
tokens.none {
it.id == hotToken.id && it.contractAddress == hotToken.contractAddress &&
it.networkId == hotToken.networkId
}
}
}
override suspend fun update(currencies: List<CryptoCurrency>) {
updateInternal { hotToken ->
currencies.none {
it.id.rawCurrencyId == hotToken.id &&
(it as? CryptoCurrency.Token)?.contractAddress == hotToken.contractAddress &&
it.network.id.value == hotToken.networkId
}
}
}
private suspend fun updateInternal(filterPredicate: (HotCryptoResponse.Token) -> Boolean) {
if (!onrampFeatureToggles.isHotTokensEnabled) return
runCatching(dispatchers.io) {
tangemTechApi.getHotCrypto(currencyId = getAppCurrencyId()).getOrThrow()
}
.onSuccess {
Timber.d("HotCrypto is successfully updated")
hotCryptoResponseStore.store(
value = it.copy(tokens = it.tokens.filter(filterPredicate)),
)
}
.onFailure {
Timber.e(it, "Unable to fetch hot crypto")
}
}
private suspend fun getAppCurrencyId(): String {
return appPreferencesStore.getObjectSyncOrNull<CurrenciesResponse.Currency>(
key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY,
)
?.id
?: "usd"
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.datasource.exchangeservice.hotcrypto
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.tokens.model.CryptoCurrency
/**
* Hot crypto loader
*
[REDACTED_AUTHOR]
*/
interface HotCryptoLoader {
/**
* Fetch hot crypto
*
* @param tokens already added tokens
*/
suspend fun fetch(tokens: List<UserTokensResponse.Token>)
/**
* Update hot crypto
*
* @param currencies already added crypto currencies
*/
suspend fun update(currencies: List<CryptoCurrency>)
}

View file

@ -0,0 +1,16 @@
package com.tangem.datasource.exchangeservice.hotcrypto
import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse
import com.tangem.datasource.local.datastore.RuntimeStateStore
import javax.inject.Inject
import javax.inject.Singleton
/**
* Store of [HotCryptoResponse]
*
[REDACTED_AUTHOR]
*/
@Singleton
class HotCryptoResponseStore @Inject constructor() : RuntimeStateStore<HotCryptoResponse> by RuntimeStateStore(
defaultValue = HotCryptoResponse(imageHost = null, tokens = emptyList()),
)