Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-12 15:33:35 +04:00
parent 516d72cb0c
commit debf9ccd90
14 changed files with 158 additions and 150 deletions

View file

@ -236,4 +236,10 @@ internal object OnrampDomainModule {
fun provideGetHotCryptoTokensUseCase(hotCryptoRepository: HotCryptoRepository): GetHotCryptoUseCase {
return GetHotCryptoUseCase(hotCryptoRepository)
}
@Provides
@Singleton
fun provideFetchHotCryptoUseCase(hotCryptoRepository: HotCryptoRepository): FetchHotCryptoUseCase {
return FetchHotCryptoUseCase(hotCryptoRepository)
}
}

View file

@ -21,6 +21,7 @@ import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.onramp.FetchHotCryptoUseCase
import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.promo.models.StoryContentIds
import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase
@ -28,6 +29,7 @@ import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase
import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase
import com.tangem.domain.staking.FetchStakingTokensUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.tap.common.extensions.setContext
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
@ -55,6 +57,8 @@ internal class MainViewModel @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
private val getStoryContentUseCase: GetStoryContentUseCase,
private val imagePreloader: ImagePreloader,
private val fetchHotCryptoUseCase: FetchHotCryptoUseCase,
onrampFeatureToggles: OnrampFeatureToggles,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel() {
@ -75,6 +79,10 @@ internal class MainViewModel @Inject constructor(
}
}
if (onrampFeatureToggles.isHotTokensEnabled) {
viewModelScope.launch { fetchHotCryptoUseCase() }
}
updateAppCurrencies()
observeFlips()
displayBalancesHidingStatusToast()

View file

@ -32,7 +32,6 @@ 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

@ -1,18 +0,0 @@
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

@ -1,91 +0,0 @@
package com.tangem.datasource.exchangeservice.hotcrypto
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.datasource.api.common.response.ApiResponseError
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,
private val analyticsEventHandler: AnalyticsEventHandler,
) : 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?.value == 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")
analyticsEventHandler.send(
event = MainScreenAnalyticsEvent.HotTokenError(
errorCode = (it as? ApiResponseError.HttpException)?.code?.code?.toString().orEmpty(),
),
)
}
}
private suspend fun getAppCurrencyId(): String {
return appPreferencesStore.getObjectSyncOrNull<CurrenciesResponse.Currency>(
key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY,
)
?.id
?: "usd"
}
}

View file

@ -1,26 +0,0 @@
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

@ -2,15 +2,15 @@ package com.tangem.datasource.exchangeservice.hotcrypto
import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse
import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.domain.wallets.models.UserWalletId
import javax.inject.Inject
import javax.inject.Singleton
/**
* Store of [HotCryptoResponse]
* Store of pair of [UserWalletId] and [HotCryptoResponse]
*
[REDACTED_AUTHOR]
*/
@Singleton
class HotCryptoResponseStore @Inject constructor() : RuntimeStateStore<HotCryptoResponse> by RuntimeStateStore(
defaultValue = HotCryptoResponse(imageHost = null, tokens = emptyList()),
)
class HotCryptoResponseStore @Inject constructor() :
RuntimeStateStore<Map<UserWalletId, HotCryptoResponse>> by RuntimeStateStore(defaultValue = emptyMap())

View file

@ -1,14 +1,32 @@
package com.tangem.data.onramp
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.data.onramp.converters.HotCryptoCurrencyConverter
import com.tangem.datasource.api.common.response.ApiResponseError
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.exchangeservice.hotcrypto.HotCryptoResponseStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject
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
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.runCatching
import com.tangem.utils.coroutines.saveIn
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.*
import timber.log.Timber
/**
* Default implementation of [HotCryptoRepository]
@ -16,17 +34,49 @@ import kotlinx.coroutines.flow.map
* @property excludedBlockchains excluded blockchains
* @property hotCryptoResponseStore store of `HotCryptoResponse`
* @property userWalletsStore store of `UserWallet`
* @property tangemTechApi tangem tech api
* @property appPreferencesStore app preferences store
* @property dispatchers dispatchers
* @property analyticsEventHandler analytics event handler
*
[REDACTED_AUTHOR]
*/
@Suppress("LongParameterList")
@OptIn(ExperimentalCoroutinesApi::class)
internal class DefaultHotCryptoRepository(
private val excludedBlockchains: ExcludedBlockchains,
private val hotCryptoResponseStore: HotCryptoResponseStore,
private val userWalletsStore: UserWalletsStore,
private val tangemTechApi: TangemTechApi,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
) : HotCryptoRepository {
private val coroutineScope = CoroutineScope(dispatchers.main + SupervisorJob())
private val hotCryptoJobHolder = JobHolder()
override fun fetchHotCrypto() {
combine(
flow = getWalletsWithTokensFlow(),
flow2 = getHotCryptoFlow(),
) { walletsWithTokens, hotCrypto ->
hotCrypto ?: return@combine emptyMap()
walletsWithTokens.mapValues { (_, tokens) ->
hotCrypto.filterTokens(tokens)
}
}
.onEach(hotCryptoResponseStore::store)
.launchIn(scope = coroutineScope)
.saveIn(jobHolder = hotCryptoJobHolder)
}
override fun getCurrencies(userWalletId: UserWalletId): Flow<List<HotCryptoCurrency>> {
return hotCryptoResponseStore.get()
.map { it[userWalletId] }
.filterNotNull()
.map {
val userWallet = userWalletsStore.getSyncOrNull(userWalletId)
?: error("UserWalletId [$userWalletId] not found")
@ -40,4 +90,53 @@ internal class DefaultHotCryptoRepository(
.filterNotNull()
}
}
private fun getWalletsWithTokensFlow(): Flow<Map<UserWalletId, List<UserTokensResponse.Token>>> {
return userWalletsStore.userWallets.flatMapLatest { userWallets ->
val flows = userWallets.map { userWallet ->
appPreferencesStore.getObject<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWallet.walletId.stringValue),
)
.map { userWallet.walletId to it?.tokens.orEmpty() }
}
combine(flows) { it.toMap() }
}
}
private fun getHotCryptoFlow(): Flow<HotCryptoResponse?> {
return appPreferencesStore
.getObject<CurrenciesResponse.Currency>(key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY)
.map { it?.id ?: "usd" }
.distinctUntilChanged()
.map { getHotCrypto(appCurrencyId = it).getOrNull() }
.distinctUntilChanged()
}
private suspend fun getHotCrypto(appCurrencyId: String): Result<HotCryptoResponse> {
return runCatching(dispatchers.io) {
tangemTechApi.getHotCrypto(currencyId = appCurrencyId).getOrThrow()
}
.onSuccess { Timber.d("HotCrypto is successfully updated") }
.onFailure {
Timber.e(it, "Unable to fetch hot crypto")
analyticsEventHandler.send(
event = MainScreenAnalyticsEvent.HotTokenError(
errorCode = (it as? ApiResponseError.HttpException)?.code?.code?.toString().orEmpty(),
),
)
}
}
private fun HotCryptoResponse.filterTokens(tokens: List<UserTokensResponse.Token>): HotCryptoResponse {
return copy(
tokens = this.tokens.filter { hotToken ->
tokens.none {
it.id == hotToken.id && it.contractAddress == hotToken.contractAddress &&
it.networkId == hotToken.networkId
}
},
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.data.onramp.di
import com.squareup.moshi.Moshi
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.onramp.DefaultHotCryptoRepository
import com.tangem.data.onramp.DefaultOnrampErrorResolver
import com.tangem.data.onramp.DefaultOnrampRepository
@ -10,6 +11,7 @@ import com.tangem.data.onramp.converters.error.OnrampErrorConverter
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore
@ -95,11 +97,19 @@ internal object OnrampDataModule {
excludedBlockchains: ExcludedBlockchains,
hotCryptoResponseStore: HotCryptoResponseStore,
userWalletsStore: UserWalletsStore,
tangemTechApi: TangemTechApi,
appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider,
analyticsEventHandler: AnalyticsEventHandler,
): HotCryptoRepository {
return DefaultHotCryptoRepository(
excludedBlockchains = excludedBlockchains,
hotCryptoResponseStore = hotCryptoResponseStore,
userWalletsStore = userWalletsStore,
tangemTechApi = tangemTechApi,
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
analyticsEventHandler = analyticsEventHandler,
)
}
}

View file

@ -4,7 +4,6 @@ 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
@ -33,7 +32,6 @@ internal object TokensDataModule {
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
expressServiceLoader: ExpressServiceLoader,
hotCryptoLoader: HotCryptoLoader,
excludedBlockchains: ExcludedBlockchains,
): CurrenciesRepository {
return DefaultCurrenciesRepository(
@ -42,9 +40,8 @@ internal object TokensDataModule {
walletManagersFacade = walletManagersFacade,
cacheRegistry = cacheRegistry,
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
expressServiceLoader = expressServiceLoader,
hotCryptoLoader = hotCryptoLoader,
dispatchers = dispatchers,
excludedBlockchains = excludedBlockchains,
)
}

View file

@ -15,7 +15,6 @@ 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
@ -47,7 +46,6 @@ 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 {
@ -606,7 +604,6 @@ internal class DefaultCurrenciesRepository(
coroutineScope {
launch { expressServiceLoader.update(userWalletId, tokens) }
launch { hotCryptoLoader.fetch(tokens = userTokens.tokens) }
}
}
@ -628,7 +625,6 @@ internal class DefaultCurrenciesRepository(
block = {
coroutineScope {
launch { expressServiceLoader.update(userWalletId, tokens) }
launch { hotCryptoLoader.update(cryptoCurrencies) }
}
},
)

View file

@ -0,0 +1,19 @@
package com.tangem.domain.onramp
import com.tangem.domain.onramp.repositories.HotCryptoRepository
/**
* Fetch hot crypto use case
*
* @property hotCryptoRepository hot crypto repository
*
[REDACTED_AUTHOR]
*/
class FetchHotCryptoUseCase(
private val hotCryptoRepository: HotCryptoRepository,
) {
operator fun invoke() {
hotCryptoRepository.fetchHotCrypto()
}
}

View file

@ -11,6 +11,9 @@ import kotlinx.coroutines.flow.Flow
*/
interface HotCryptoRepository {
/** Fetch hot crypto */
fun fetchHotCrypto()
/** Get currencies by [userWalletId] */
fun getCurrencies(userWalletId: UserWalletId): Flow<List<HotCryptoCurrency>>
}

View file

@ -4,6 +4,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.extenstions.unwrap
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.onramp.FetchHotCryptoUseCase
import com.tangem.domain.settings.NeverToShowWalletsScrollPreview
import com.tangem.domain.tokens.FetchCardTokenListUseCase
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
@ -48,6 +49,7 @@ internal class WalletClickIntents @Inject constructor(
private val rampStateManager: RampStateManager,
private val dispatchers: CoroutineDispatcherProvider,
private val onrampFeatureToggles: OnrampFeatureToggles,
private val fetchHotCryptoUseCase: FetchHotCryptoUseCase,
) : BaseWalletClickIntents(),
WalletCardClickIntents by walletCardClickIntentsImplementor,
WalletWarningsClickIntents by warningsClickIntentsImplementer,
@ -133,6 +135,10 @@ internal class WalletClickIntents @Inject constructor(
}
async { rampStateManager.fetchSellServiceData() }.let(::add)
if (onrampFeatureToggles.isHotTokensEnabled) {
async { fetchHotCryptoUseCase() }.let(::add)
}
}
.awaitAll()