Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-11 21:41:39 +05:00
parent 083d246cc7
commit 904d69af90
11 changed files with 112 additions and 56 deletions

View file

@ -1,7 +1,7 @@
package com.tangem.tap.di
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
@ -49,7 +49,7 @@ internal object ActivityModule {
@Singleton
fun provideDefaultRampManager(
appStateHolder: AppStateHolder,
swapServiceLoader: SwapServiceLoader,
expressServiceLoader: ExpressServiceLoader,
currenciesRepository: CurrenciesRepository,
getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
excludedBlockchains: ExcludedBlockchains,
@ -61,7 +61,7 @@ internal object ActivityModule {
exchangeService = appStateHolder.exchangeService,
buyService = Provider { requireNotNull(appStateHolder.buyService) },
sellService = Provider { requireNotNull(appStateHolder.sellService) },
swapServiceLoader = swapServiceLoader,
expressServiceLoader = expressServiceLoader,
currenciesRepository = currenciesRepository,
getNetworkCoinStatusUseCase = getNetworkCoinStatusUseCase,
excludedBlockchains = excludedBlockchains,

View file

@ -3,7 +3,7 @@ package com.tangem.tap.network.exchangeServices
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.scan.ScanResponse
@ -25,7 +25,7 @@ internal class DefaultRampManager(
private val exchangeService: ExchangeService?,
private val buyService: Provider<ExchangeService>,
private val sellService: Provider<ExchangeService>,
private val swapServiceLoader: SwapServiceLoader,
private val expressServiceLoader: ExpressServiceLoader,
private val currenciesRepository: CurrenciesRepository,
private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
private val dispatchers: CoroutineDispatcherProvider,
@ -92,12 +92,12 @@ internal class DefaultRampManager(
}
override fun getSwapInitializationStatus(userWalletId: UserWalletId): Flow<ExchangeServiceInitializationStatus> {
return swapServiceLoader.getInitializationStatus(userWalletId)
return expressServiceLoader.getInitializationStatus(userWalletId)
}
private suspend fun getExchangeableFlag(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
return withContext(dispatchers.io) {
val asset = swapServiceLoader.getInitializationStatus(userWalletId)
val asset = expressServiceLoader.getInitializationStatus(userWalletId)
.value
.getOrNull()
?.find { cryptoCurrency.findAssetPredicate(it) }

View file

@ -1,7 +1,7 @@
package com.tangem.datasource.di.exchangeservice
import com.tangem.datasource.exchangeservice.swap.DefaultSwapServiceLoader
import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader
import com.tangem.datasource.exchangeservice.swap.DefaultExpressServiceLoader
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -14,5 +14,5 @@ internal interface ExchangeServiceLoaderModule {
@Binds
@Singleton
fun bindSwapServiceLoader(defaultSwapServiceLoader: DefaultSwapServiceLoader): SwapServiceLoader
fun bindExpressServiceLoader(defaultExpressServiceLoader: DefaultExpressServiceLoader): ExpressServiceLoader
}

View file

@ -2,11 +2,9 @@ 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
@ -23,39 +21,32 @@ import javax.inject.Inject
typealias InitializationStatusFlow = MutableStateFlow<Lce<Throwable, List<Asset>>>
/**
* Default implementation of [SwapServiceLoader]
* Default implementation of [ExpressServiceLoader]
*
* @property tangemExpressApi express api
* @property expressAssetsStore local storage
*
[REDACTED_AUTHOR]
*/
internal class DefaultSwapServiceLoader @Inject constructor(
internal class DefaultExpressServiceLoader @Inject constructor(
private val tangemExpressApi: TangemExpressApi,
private val expressAssetsStore: ExpressAssetsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : SwapServiceLoader {
) : ExpressServiceLoader {
private val initializationStatuses =
MutableStateFlow<Map<UserWalletId, InitializationStatusFlow>>(value = emptyMap())
override suspend fun update(userWalletId: UserWalletId, userTokens: UserTokensResponse) {
override suspend fun update(userWalletId: UserWalletId, userTokens: List<LeastTokenInfo>) {
withContext(dispatchers.io) {
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()) {
if (userTokens.isNotEmpty()) {
val response = tangemExpressApi.getAssets(
body = AssetsRequestBody(tokensList = tokensList),
body = AssetsRequestBody(tokensList = userTokens),
).getOrThrow()
expressAssetsStore.store(userWalletId, response)

View file

@ -1,20 +1,20 @@
package com.tangem.datasource.exchangeservice.swap
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.domain.core.lce.Lce
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.StateFlow
/**
* Swap service loader
* Express service loader
*
[REDACTED_AUTHOR]
*/
interface SwapServiceLoader {
interface ExpressServiceLoader {
/** Update service using [userWalletId] and [userTokens] */
suspend fun update(userWalletId: UserWalletId, userTokens: UserTokensResponse)
suspend fun update(userWalletId: UserWalletId, userTokens: List<LeastTokenInfo>)
/** Get initialization status by [userWalletId] */
fun getInitializationStatus(userWalletId: UserWalletId): StateFlow<Lce<Throwable, List<Asset>>>

View file

@ -4,7 +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.swap.SwapServiceLoader
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import com.tangem.datasource.local.network.NetworksStatusesStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.quote.QuotesStore
@ -31,7 +31,7 @@ internal object TokensDataModule {
walletManagersFacade: WalletManagersFacade,
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
swapServiceLoader: SwapServiceLoader,
expressServiceLoader: ExpressServiceLoader,
excludedBlockchains: ExcludedBlockchains,
): CurrenciesRepository {
return DefaultCurrenciesRepository(
@ -41,7 +41,7 @@ internal object TokensDataModule {
cacheRegistry = cacheRegistry,
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
swapServiceLoader = swapServiceLoader,
expressServiceLoader = expressServiceLoader,
excludedBlockchains = excludedBlockchains,
)
}

View file

@ -14,9 +14,11 @@ 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.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
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.exchangeservice.swap.ExpressServiceLoader
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject
@ -48,7 +50,7 @@ internal class DefaultCurrenciesRepository(
private val walletManagersFacade: WalletManagersFacade,
private val cacheRegistry: CacheRegistry,
private val appPreferencesStore: AppPreferencesStore,
private val swapServiceLoader: SwapServiceLoader,
private val expressServiceLoader: ExpressServiceLoader,
private val dispatchers: CoroutineDispatcherProvider,
excludedBlockchains: ExcludedBlockchains,
) : CurrenciesRepository {
@ -95,7 +97,7 @@ internal class DefaultCurrenciesRepository(
response = updatedResponse,
)
fetchExchangeableUserMarketCoinsByIds(userWalletId, updatedResponse)
fetchExpressAssetsByNetworkIds(userWalletId, updatedResponse)
}
}
@ -117,7 +119,7 @@ internal class DefaultCurrenciesRepository(
userWalletId = userWalletId,
response = updatedResponse,
)
fetchExchangeableUserMarketCoinsByIds(userWalletId, updatedResponse)
fetchExpressAssetsByNetworkIds(userWalletId, updatedResponse)
}
}
@ -214,21 +216,33 @@ internal class DefaultCurrenciesRepository(
}
}
override suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency {
override suspend fun getSingleCurrencyWalletPrimaryCurrency(
userWalletId: UserWalletId,
refresh: Boolean,
): CryptoCurrency {
return withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse)
val currency = cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse)
fetchExpressAssetsByNetworkIds(userWalletId, listOf(currency), refresh)
currency
}
}
override suspend fun getSingleCurrencyWalletWithCardCurrencies(userWalletId: UserWalletId): List<CryptoCurrency> {
override suspend fun getSingleCurrencyWalletWithCardCurrencies(
userWalletId: UserWalletId,
refresh: Boolean,
): List<CryptoCurrency> {
return withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
val currencies = cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(
userWallet.scanResponse,
)
fetchExpressAssetsByNetworkIds(userWalletId, currencies, refresh)
currencies
}
}
@ -243,6 +257,8 @@ internal class DefaultCurrenciesRepository(
val currency = cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
.find { it.id == id }
requireNotNull(currency) { "Unable to find currency with provided ID: $id" }
fetchExpressAssetsByNetworkIds(userWalletId, listOf(currency))
currency
}
}
@ -553,7 +569,7 @@ internal class DefaultCurrenciesRepository(
value = compatibleUserTokensResponse,
)
fetchExchangeableUserMarketCoinsByIds(userWalletId, compatibleUserTokensResponse)
fetchExpressAssetsByNetworkIds(userWalletId, compatibleUserTokensResponse)
}
private suspend fun checkIsEmptyDemoWallet(userWallet: UserWallet): Boolean {
@ -572,13 +588,37 @@ internal class DefaultCurrenciesRepository(
pushTokens(userWalletId, response)
}
private suspend fun fetchExchangeableUserMarketCoinsByIds(
userWalletId: UserWalletId,
userTokens: UserTokensResponse,
) {
swapServiceLoader.update(userWalletId, userTokens)
private suspend fun fetchExpressAssetsByNetworkIds(userWalletId: UserWalletId, userTokens: UserTokensResponse) {
val tokens = userTokens.tokens.map { token ->
LeastTokenInfo(
contractAddress = token.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE,
network = token.networkId,
)
}
expressServiceLoader.update(userWalletId, tokens)
}
private suspend fun fetchExpressAssetsByNetworkIds(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
refresh: Boolean = false,
) {
val tokens = cryptoCurrencies.map { currency ->
val tokenCurrency = currency as? CryptoCurrency.Token
LeastTokenInfo(
contractAddress = tokenCurrency?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE,
network = currency.network.backendId,
)
}
cacheRegistry.invokeOnExpire(
key = getAssetsCacheKey(userWalletId),
skipCache = refresh,
block = { expressServiceLoader.update(userWalletId, tokens) },
)
}
private fun getAssetsCacheKey(userWalletId: UserWalletId): String = "assets_cache_key_${userWalletId.stringValue}"
private suspend fun handleFetchTokensError(userWallet: UserWallet, e: ApiResponseError): UserTokensResponse {
val userWalletId = userWallet.walletId
val response = appPreferencesStore.getObjectSyncOrNull(

View file

@ -25,7 +25,7 @@ class FetchCardTokenListUseCase(
suspend operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Either<TokenListError, Unit> {
return either {
val currencies = fetchCurrencies(userWalletId = userWalletId)
val currencies = fetchCurrencies(userWalletId = userWalletId, refresh = refresh)
coroutineScope {
val fetchStatuses = async {
@ -53,9 +53,17 @@ class FetchCardTokenListUseCase(
}
}
private suspend fun Raise<TokenListError>.fetchCurrencies(userWalletId: UserWalletId): List<CryptoCurrency> {
private suspend fun Raise<TokenListError>.fetchCurrencies(
userWalletId: UserWalletId,
refresh: Boolean = false,
): List<CryptoCurrency> {
return catch(
block = { currenciesRepository.getSingleCurrencyWalletWithCardCurrencies(userWalletId = userWalletId) },
block = {
currenciesRepository.getSingleCurrencyWalletWithCardCurrencies(
userWalletId = userWalletId,
refresh = refresh,
)
},
catch = { raise(TokenListError.DataError(it)) },
)
}

View file

@ -65,7 +65,7 @@ class FetchCurrencyStatusUseCase(
refresh: Boolean = false,
): Either<CurrencyStatusError, Unit> {
return either {
val currency = getPrimaryCurrency(userWalletId)
val currency = getPrimaryCurrency(userWalletId, refresh)
fetchCurrencyStatus(userWalletId, currency, refresh)
}
@ -102,8 +102,11 @@ class FetchCurrencyStatusUseCase(
}
}
private suspend fun Raise<CurrencyStatusError>.getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency {
return catch({ currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) }) {
private suspend fun Raise<CurrencyStatusError>.getPrimaryCurrency(
userWalletId: UserWalletId,
refresh: Boolean = false,
): CryptoCurrency {
return catch({ currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId, refresh) }) {
raise(CurrencyStatusError.DataError(it))
}
}

View file

@ -86,21 +86,29 @@ interface CurrenciesRepository {
* Retrieves the primary cryptocurrency for a specific single-currency user wallet.
*
* @param userWalletId The unique identifier of the user wallet.
* @param refresh Indicates whether to force a refresh of the status data.
* @return The primary cryptocurrency associated with the user wallet.
* @throws DataError.UserWalletError.WrongUserWallet If multi-currency user wallet
* ID provided.
*/
suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency
suspend fun getSingleCurrencyWalletPrimaryCurrency(
userWalletId: UserWalletId,
refresh: Boolean = false,
): CryptoCurrency
/**
* Retrieves the cryptocurrencies for a specific single-currency user wallet with tokens on the card.
*
* @param userWalletId The unique identifier of the user wallet.
* @param refresh Indicates whether to force a refresh of the status data.
* @return The primary cryptocurrency associated with the user wallet.
* @throws DataError.UserWalletError.WrongUserWallet If multi-currency user wallet
* ID provided.
*/
suspend fun getSingleCurrencyWalletWithCardCurrencies(userWalletId: UserWalletId): List<CryptoCurrency>
suspend fun getSingleCurrencyWalletWithCardCurrencies(
userWalletId: UserWalletId,
refresh: Boolean = false,
): List<CryptoCurrency>
/**
* Retrieves the cryptocurrency for a specific single-currency user old wallet

View file

@ -70,11 +70,17 @@ internal class MockCurrenciesRepository(
return tokens.first().getOrElse { e -> throw e }
}
override suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency {
override suspend fun getSingleCurrencyWalletPrimaryCurrency(
userWalletId: UserWalletId,
refresh: Boolean,
): CryptoCurrency {
return token.getOrElse { e -> throw e }
}
override suspend fun getSingleCurrencyWalletWithCardCurrencies(userWalletId: UserWalletId): List<CryptoCurrency> {
override suspend fun getSingleCurrencyWalletWithCardCurrencies(
userWalletId: UserWalletId,
refresh: Boolean,
): List<CryptoCurrency> {
return tokens.first().getOrElse { e -> throw e }
}