Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-06 16:20:11 +04:00
parent 8966448261
commit 9c120b9914
14 changed files with 183 additions and 109 deletions

View file

@ -1,17 +1,18 @@
package com.tangem.tap.di
import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository
import com.tangem.tap.network.exchangeServices.DefaultRampManager
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -43,13 +44,15 @@ internal object ActivityModule {
@Singleton
fun provideDefaultRampManager(
appStateHolder: AppStateHolder,
marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
swapServiceLoader: SwapServiceLoader,
dispatchers: CoroutineDispatcherProvider,
): RampStateManager {
return DefaultRampManager(
exchangeService = appStateHolder.exchangeService,
buyService = Provider { requireNotNull(appStateHolder.buyService) },
sellService = Provider { requireNotNull(appStateHolder.sellService) },
marketsCryptoCurrencyRepository = marketCryptoCurrencyRepository,
swapServiceLoader = swapServiceLoader,
dispatchers = dispatchers,
)
}

View file

@ -118,26 +118,26 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
swapRepository: SwapRepository,
currencyChecksRepository: CurrencyChecksRepository,
showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
promoRepository: PromoRepository,
stakingRepository: StakingRepository,
dispatchers: CoroutineDispatcherProvider,
rampStateManager: RampStateManager,
): GetCurrencyWarningsUseCase {
return GetCurrencyWarningsUseCase(
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
marketCryptoCurrencyRepository = marketCryptoCurrencyRepository,
currencyChecksRepository = currencyChecksRepository,
swapRepository = swapRepository,
showSwapPromoTokenUseCase = showSwapPromoTokenUseCase,
promoRepository = promoRepository,
stakingRepository = stakingRepository,
dispatchers = dispatchers,
rampStateManager = rampStateManager,
)
}
@ -215,7 +215,6 @@ internal object TokensDomainModule {
fun provideGetCryptoCurrencyActionsUseCase(
rampStateManager: RampStateManager,
walletManagersFacade: WalletManagersFacade,
marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
@ -225,7 +224,6 @@ internal object TokensDomainModule {
return GetCryptoCurrencyActionsUseCase(
rampManager = rampStateManager,
walletManagersFacade = walletManagersFacade,
marketCryptoCurrencyRepository = marketCryptoCurrencyRepository,
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,

View file

@ -1,25 +1,29 @@
package com.tangem.tap.network.exchangeServices
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
class DefaultRampManager(
internal class DefaultRampManager(
private val exchangeService: ExchangeService?,
private val buyService: Provider<ExchangeService>,
private val sellService: Provider<ExchangeService>,
private val marketsCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
private val swapServiceLoader: SwapServiceLoader,
private val dispatchers: CoroutineDispatcherProvider,
) : RampStateManager {
private val cryptoCurrencyConverter = CryptoCurrencyConverter()
override fun availableForBuy(scanResponse: ScanResponse, cryptoCurrency: CryptoCurrency): Boolean {
return exchangeService?.availableForBuy(
scanResponse,
scanResponse = scanResponse,
currency = cryptoCurrencyConverter.convertBack(cryptoCurrency),
) ?: false
}
@ -31,14 +35,44 @@ class DefaultRampManager(
}
override suspend fun availableForSwap(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
return marketsCryptoCurrencyRepository.isExchangeable(userWalletId, cryptoCurrency)
return getExchangeableFlag(userWalletId, cryptoCurrency) && !cryptoCurrency.isCustom
}
override fun getBuyInitializationStatus(): Flow<ExchangeServiceInitializationStatus> {
return buyService.invoke().initializationStatus
}
override suspend fun fetchBuyServiceData() {
withContext(dispatchers.io) {
buyService.invoke().update()
}
}
override fun getSellInitializationStatus(): Flow<ExchangeServiceInitializationStatus> {
return sellService.invoke().initializationStatus
}
override suspend fun fetchSellServiceData() {
withContext(dispatchers.io) {
sellService.invoke().update()
}
}
override fun getSwapInitializationStatus(userWalletId: UserWalletId): Flow<ExchangeServiceInitializationStatus> {
return swapServiceLoader.getInitializationStatus(userWalletId)
}
private suspend fun getExchangeableFlag(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
return withContext(dispatchers.io) {
val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress
?: EMPTY_CONTRACT_ADDRESS_VALUE
val asset = swapServiceLoader.getInitializationStatus(userWalletId).value.getOrNull()?.find {
it.network == cryptoCurrency.network.backendId &&
it.contractAddress.equals(contractAddress, ignoreCase = true)
}
asset?.exchangeAvailable ?: false
}
}
}

View file

@ -21,6 +21,7 @@ dependencies {
implementation(projects.core.utils)
implementation(projects.libs.auth)
implementation(projects.domain.appTheme.models)
implementation(projects.domain.core)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.balanceHiding.models)

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.di.exchangeservice
import com.tangem.datasource.exchangeservice.swap.DefaultSwapServiceLoader
import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader
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 ExchangeServiceLoaderModule {
@Binds
@Singleton
fun bindSwapServiceLoader(defaultSwapServiceLoader: DefaultSwapServiceLoader): SwapServiceLoader
}

View file

@ -0,0 +1,76 @@
package com.tangem.datasource.exchangeservice.swap
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
import com.tangem.datasource.api.express.models.request.AssetsRequestBody
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import timber.log.Timber
import javax.inject.Inject
typealias InitializationStatusFlow = MutableStateFlow<Lce<Throwable, List<Asset>>>
/**
* Default implementation of [SwapServiceLoader]
*
* @property tangemExpressApi express api
* @property expressAssetsStore local storage
*
[REDACTED_AUTHOR]
*/
internal class DefaultSwapServiceLoader @Inject constructor(
private val tangemExpressApi: TangemExpressApi,
private val expressAssetsStore: ExpressAssetsStore,
) : SwapServiceLoader {
private val initializationStatuses: Map<UserWalletId, InitializationStatusFlow> = mapOf()
override suspend fun update(userWalletId: UserWalletId, userTokens: UserTokensResponse) {
val initializationStatus = getInitializationStatusInternal(userWalletId)
initializationStatus.update { lceLoading() }
try {
val tokensList = userTokens.tokens.map {
LeastTokenInfo(
contractAddress = it.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE,
network = it.networkId,
)
}
if (tokensList.isNotEmpty()) {
val response = tangemExpressApi.getAssets(
body = AssetsRequestBody(tokensList = tokensList),
).getOrThrow()
expressAssetsStore.store(userWalletId, response)
initializationStatus.update { response.lceContent() }
}
} catch (e: Throwable) {
initializationStatus.update { e.lceError() }
Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}")
}
}
override fun getInitializationStatus(userWalletId: UserWalletId): InitializationStatusFlow {
return getInitializationStatusInternal(userWalletId)
}
private fun getInitializationStatusInternal(userWalletId: UserWalletId): InitializationStatusFlow {
return initializationStatuses.getOrDefault(
key = userWalletId,
defaultValue = MutableStateFlow(value = lceLoading()),
)
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.exchangeservice.swap
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.StateFlow
/**
* Swap service loader
*
[REDACTED_AUTHOR]
*/
interface SwapServiceLoader {
/** Update service using [userWalletId] and [userTokens] */
suspend fun update(userWalletId: UserWalletId, userTokens: UserTokensResponse)
/** Get initialization status by [userWalletId] */
fun getInitializationStatus(userWalletId: UserWalletId): StateFlow<Lce<Throwable, List<Asset>>>
}

View file

@ -2,12 +2,11 @@ package com.tangem.data.tokens.di
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.tokens.repository.*
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader
import com.tangem.datasource.local.network.NetworksStatusesStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.quote.QuotesStore
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -26,23 +25,21 @@ internal object TokensDataModule {
@Singleton
fun provideCurrenciesRepository(
tangemTechApi: TangemTechApi,
tangemExpressApi: TangemExpressApi,
appPreferencesStore: AppPreferencesStore,
userWalletsStore: UserWalletsStore,
walletManagersFacade: WalletManagersFacade,
expressAssetsStore: ExpressAssetsStore,
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
swapServiceLoader: SwapServiceLoader,
): CurrenciesRepository {
return DefaultCurrenciesRepository(
tangemTechApi = tangemTechApi,
tangemExpressApi = tangemExpressApi,
appPreferencesStore = appPreferencesStore,
walletManagersFacade = walletManagersFacade,
userWalletsStore = userWalletsStore,
expressAssetsStore = expressAssetsStore,
walletManagersFacade = walletManagersFacade,
cacheRegistry = cacheRegistry,
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
swapServiceLoader = swapServiceLoader,
)
}
@ -84,15 +81,6 @@ internal object TokensDataModule {
)
}
@Provides
@Singleton
fun provideDefaultMarketCoinsRepository(
expressAssetsStore: ExpressAssetsStore,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
): MarketCryptoCurrencyRepository {
return DefaultMarketCryptoCurrencyRepository(expressAssetsStore, coroutineDispatcherProvider)
}
@Provides
@Singleton
fun provideCurrencyChecksRepository(walletManagersFacade: WalletManagersFacade): CurrencyChecksRepository {

View file

@ -12,18 +12,14 @@ import com.tangem.data.tokens.utils.CustomTokensMerger
import com.tangem.data.tokens.utils.UserTokensBackwardCompatibility
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
import com.tangem.datasource.api.express.models.request.AssetsRequestBody
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.swap.SwapServiceLoader
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.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.core.error.DataError
import com.tangem.domain.demo.DemoConfig
@ -46,12 +42,11 @@ import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
internal class DefaultCurrenciesRepository(
private val tangemTechApi: TangemTechApi,
private val tangemExpressApi: TangemExpressApi,
private val userWalletsStore: UserWalletsStore,
private val walletManagersFacade: WalletManagersFacade,
private val expressAssetsStore: ExpressAssetsStore,
private val cacheRegistry: CacheRegistry,
private val appPreferencesStore: AppPreferencesStore,
private val swapServiceLoader: SwapServiceLoader,
private val dispatchers: CoroutineDispatcherProvider,
) : CurrenciesRepository {
@ -565,27 +560,7 @@ internal class DefaultCurrenciesRepository(
userWalletId: UserWalletId,
userTokens: UserTokensResponse,
) {
try {
val tokensList = userTokens.tokens
.map {
LeastTokenInfo(
contractAddress = it.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE,
network = it.networkId,
)
}
if (tokensList.isNotEmpty()) {
val response = tangemExpressApi.getAssets(
AssetsRequestBody(
tokensList = tokensList,
),
)
expressAssetsStore.store(userWalletId, response.getOrThrow())
}
} catch (e: Throwable) {
Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}")
}
swapServiceLoader.update(userWalletId, userTokens)
}
private suspend fun handleFetchTokensError(userWallet: UserWallet, e: ApiResponseError): UserTokensResponse {

View file

@ -1,33 +0,0 @@
package com.tangem.data.tokens.repository
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
class DefaultMarketCryptoCurrencyRepository(
private val expressAssetsStore: ExpressAssetsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : MarketCryptoCurrencyRepository {
override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
return getExchangeableFlag(userWalletId, cryptoCurrency) && !cryptoCurrency.isCustom
}
private suspend fun getExchangeableFlag(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
return withContext(dispatchers.io) {
val contractAddress =
(cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE
val asset = expressAssetsStore.getSyncOrNull(userWalletId)?.find {
it.network == cryptoCurrency.network.backendId &&
it.contractAddress.equals(contractAddress, ignoreCase = true)
}
asset?.exchangeAvailable ?: false
}
}
}

View file

@ -17,7 +17,13 @@ interface RampStateManager {
suspend fun availableForSwap(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean
suspend fun fetchBuyServiceData()
fun getBuyInitializationStatus(): Flow<Lce<Throwable, Any>>
suspend fun fetchSellServiceData()
fun getSellInitializationStatus(): Flow<Lce<Throwable, Any>>
fun getSwapInitializationStatus(userWalletId: UserWalletId): Flow<Lce<Throwable, Any>>
}

View file

@ -7,7 +7,6 @@ import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -26,7 +25,6 @@ import kotlinx.coroutines.flow.*
class GetCryptoCurrencyActionsUseCase(
private val rampManager: RampStateManager,
private val walletManagersFacade: WalletManagersFacade,
private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
@ -168,7 +166,7 @@ class GetCryptoCurrencyActionsUseCase(
// swap
if (userWallet.isMultiCurrency) {
if (
marketCryptoCurrencyRepository.isExchangeable(userWallet.walletId, cryptoCurrency) &&
rampManager.availableForSwap(userWallet.walletId, cryptoCurrency) &&
cryptoCurrencyStatus.value !is CryptoCurrencyStatus.NoQuote
) {
activeList.add(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.None))
@ -300,7 +298,7 @@ class GetCryptoCurrencyActionsUseCase(
return networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty()
}
private suspend fun isStakingAvailable(userWallet: UserWallet, cryptoCurrency: CryptoCurrency): Boolean {
private fun isStakingAvailable(userWallet: UserWallet, cryptoCurrency: CryptoCurrency): Boolean {
return stakingRepository.getStakingAvailability(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrency,

View file

@ -1,5 +1,6 @@
package com.tangem.domain.tokens
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.*
@ -26,7 +27,7 @@ class GetCurrencyWarningsUseCase(
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val swapRepository: SwapRepository,
private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
private val rampStateManager: RampStateManager,
private val stakingRepository: StakingRepository,
private val promoRepository: PromoRepository,
private val showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
@ -90,10 +91,10 @@ class GetCurrencyWarningsUseCase(
val cryptoStatuses = operations.getCurrenciesStatusesSync()
val promoBanner = promoRepository.getOkxPromoBanner()
return combine(
showSwapPromoTokenUseCase()
flow = showSwapPromoTokenUseCase()
.conflate()
.distinctUntilChanged(),
flowOf(marketCryptoCurrencyRepository.isExchangeable(userWalletId, currency))
flow2 = flowOf(rampStateManager.availableForSwap(userWalletId, currency))
.conflate()
.distinctUntilChanged(),
) { shouldShowSwapPromo, isExchangeable ->

View file

@ -1,12 +0,0 @@
package com.tangem.domain.tokens.repository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
/**
* MarketCryptoCurrencyRepository works with data from Tangem coins backend, CoinMarketCap etc
*/
interface MarketCryptoCurrencyRepository {
suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean
}