Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-24 10:06:29 +03:00
parent 9d49f37de6
commit 10bb769916
17 changed files with 389 additions and 5 deletions

View file

@ -1,6 +1,7 @@
package com.tangem.tap.di.domain
import com.tangem.domain.onramp.*
import com.tangem.domain.onramp.repositories.HotCryptoRepository
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
@ -229,4 +230,10 @@ internal object OnrampDomainModule {
): FetchOnrampCountriesUseCase {
return FetchOnrampCountriesUseCase(onrampRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideGetHotCryptoTokensUseCase(hotCryptoRepository: HotCryptoRepository): GetHotCryptoUseCase {
return GetHotCryptoUseCase(hotCryptoRepository)
}
}

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()),
)

View file

@ -26,6 +26,7 @@ dependencies {
implementation(projects.domain.onramp)
implementation(projects.domain.legacy)
implementation(projects.domain.appTheme.models)
implementation(projects.domain.models)
// region DI

View file

@ -0,0 +1,41 @@
package com.tangem.data.onramp
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.onramp.converters.HotCryptoCurrencyConverter
import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.onramp.model.HotCryptoCurrency
import com.tangem.domain.onramp.repositories.HotCryptoRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
/**
* Default implementation of [HotCryptoRepository]
*
* @property excludedBlockchains excluded blockchains
* @property hotCryptoResponseStore store of `HotCryptoResponse`
* @property userWalletsStore store of `UserWallet`
*
[REDACTED_AUTHOR]
*/
internal class DefaultHotCryptoRepository(
private val excludedBlockchains: ExcludedBlockchains,
private val hotCryptoResponseStore: HotCryptoResponseStore,
private val userWalletsStore: UserWalletsStore,
) : HotCryptoRepository {
override fun getCurrencies(userWalletId: UserWalletId): Flow<List<HotCryptoCurrency>> {
return hotCryptoResponseStore.get()
.map {
val userWallet = userWalletsStore.getSyncOrNull(userWalletId)
?: error("UserWalletId [$userWalletId] not found")
HotCryptoCurrencyConverter(
scanResponse = userWallet.scanResponse,
imageHost = it.imageHost,
excludedBlockchains = excludedBlockchains,
).convertList(input = it.tokens)
}
}
}

View file

@ -0,0 +1,78 @@
package com.tangem.data.onramp.converters
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.currency.getNetwork
import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.onramp.model.HotCryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.utils.converter.Converter
/**
* Converter from [HotCryptoResponse.Token] to [HotCryptoCurrency]
*
* @property scanResponse scan response
* @property imageHost image host
* @property excludedBlockchains excluded blockchains
*
[REDACTED_AUTHOR]
*/
internal class HotCryptoCurrencyConverter(
private val scanResponse: ScanResponse,
private val imageHost: String?,
private val excludedBlockchains: ExcludedBlockchains,
) : Converter<HotCryptoResponse.Token, HotCryptoCurrency> {
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory(excludedBlockchains) }
override fun convert(value: HotCryptoResponse.Token): HotCryptoCurrency {
val network = createNetwork(value.networkId)
val contractAddress = value.contractAddress
val decimals = value.decimalCount
val currency = if (contractAddress != null && decimals != null) {
cryptoCurrencyFactory.createToken(
network = network,
rawId = value.id,
name = value.name,
symbol = value.symbol,
decimals = decimals,
contractAddress = contractAddress,
)
} else {
cryptoCurrencyFactory.createCoin(network = network)
}
return HotCryptoCurrency(
cryptoCurrency = currency.setupIconUrl(id = value.id),
fiatRate = value.currentPrice,
priceChange = value.priceChangePercentage,
)
}
private fun createNetwork(networkId: String): Network {
return requireNotNull(
getNetwork(
blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)),
extraDerivationPath = null,
scanResponse = scanResponse,
excludedBlockchains = excludedBlockchains,
),
)
}
private fun CryptoCurrency.setupIconUrl(id: String): CryptoCurrency {
imageHost ?: return this
val iconUrl = "${imageHost}large/$id.png"
return when (this) {
is CryptoCurrency.Coin -> copy(iconUrl = iconUrl)
is CryptoCurrency.Token -> copy(iconUrl = iconUrl)
}
}
}

View file

@ -1,6 +1,8 @@
package com.tangem.data.onramp.di
import com.squareup.moshi.Moshi
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.onramp.DefaultHotCryptoRepository
import com.tangem.data.onramp.DefaultOnrampErrorResolver
import com.tangem.data.onramp.DefaultOnrampRepository
import com.tangem.data.onramp.DefaultOnrampTransactionRepository
@ -10,12 +12,15 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore
import com.tangem.datasource.local.onramp.countries.OnrampCountriesStore
import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.onramp.repositories.HotCryptoRepository
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
@ -83,4 +88,18 @@ internal object OnrampDataModule {
OnrampErrorConverter(jsonAdapter),
)
}
@Provides
@Singleton
fun provideHotCryptoRepository(
excludedBlockchains: ExcludedBlockchains,
hotCryptoResponseStore: HotCryptoResponseStore,
userWalletsStore: UserWalletsStore,
): HotCryptoRepository {
return DefaultHotCryptoRepository(
excludedBlockchains = excludedBlockchains,
hotCryptoResponseStore = hotCryptoResponseStore,
userWalletsStore = userWalletsStore,
)
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.tokens.repository.*
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoLoader
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import com.tangem.datasource.local.network.NetworksStatusesStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
@ -32,6 +33,7 @@ internal object TokensDataModule {
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
expressServiceLoader: ExpressServiceLoader,
hotCryptoLoader: HotCryptoLoader,
excludedBlockchains: ExcludedBlockchains,
): CurrenciesRepository {
return DefaultCurrenciesRepository(
@ -42,6 +44,7 @@ internal object TokensDataModule {
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
expressServiceLoader = expressServiceLoader,
hotCryptoLoader = hotCryptoLoader,
excludedBlockchains = excludedBlockchains,
)
}

View file

@ -15,6 +15,7 @@ import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRA
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoLoader
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
@ -33,10 +34,8 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext
import timber.log.Timber
import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency
@ -48,6 +47,7 @@ internal class DefaultCurrenciesRepository(
private val cacheRegistry: CacheRegistry,
private val appPreferencesStore: AppPreferencesStore,
private val expressServiceLoader: ExpressServiceLoader,
private val hotCryptoLoader: HotCryptoLoader,
private val dispatchers: CoroutineDispatcherProvider,
excludedBlockchains: ExcludedBlockchains,
) : CurrenciesRepository {
@ -601,7 +601,11 @@ internal class DefaultCurrenciesRepository(
network = token.networkId,
)
}
expressServiceLoader.update(userWalletId, tokens)
coroutineScope {
launch { expressServiceLoader.update(userWalletId, tokens) }
launch { hotCryptoLoader.fetch(tokens = userTokens.tokens) }
}
}
private suspend fun fetchExpressAssetsByNetworkIds(
@ -619,7 +623,12 @@ internal class DefaultCurrenciesRepository(
cacheRegistry.invokeOnExpire(
key = getAssetsCacheKey(userWalletId),
skipCache = refresh,
block = { expressServiceLoader.update(userWalletId, tokens) },
block = {
coroutineScope {
launch { expressServiceLoader.update(userWalletId, tokens) }
launch { hotCryptoLoader.update(cryptoCurrencies) }
}
},
)
}

View file

@ -0,0 +1,19 @@
package com.tangem.domain.onramp.model
import com.tangem.domain.tokens.model.CryptoCurrency
import java.math.BigDecimal
/**
* Hot crypto currency
*
* @property cryptoCurrency crypto currency
* @property fiatRate fiat rate
* @property priceChange price change
*
[REDACTED_AUTHOR]
*/
data class HotCryptoCurrency(
val cryptoCurrency: CryptoCurrency,
val fiatRate: BigDecimal,
val priceChange: BigDecimal,
)

View file

@ -0,0 +1,22 @@
package com.tangem.domain.onramp
import com.tangem.domain.onramp.model.HotCryptoCurrency
import com.tangem.domain.onramp.repositories.HotCryptoRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
/**
* Get hot crypto currencies
*
* @property hotCryptoRepository hot crypto repository
*
[REDACTED_AUTHOR]
*/
class GetHotCryptoUseCase(
private val hotCryptoRepository: HotCryptoRepository,
) {
operator fun invoke(userWalletId: UserWalletId): Flow<List<HotCryptoCurrency>> {
return hotCryptoRepository.getCurrencies(userWalletId)
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.domain.onramp.repositories
import com.tangem.domain.onramp.model.HotCryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
/**
* Hot crypto repository
*
[REDACTED_AUTHOR]
*/
interface HotCryptoRepository {
/** Get currencies by [userWalletId] */
fun getCurrencies(userWalletId: UserWalletId): Flow<List<HotCryptoCurrency>>
}