Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-21 20:15:54 +03:00
parent 613e07734e
commit 3fc34fac64
103 changed files with 403 additions and 297 deletions

View file

@ -7,6 +7,7 @@ import com.tangem.data.tokens.repository.DefaultNetworksRepository
import com.tangem.data.tokens.repository.DefaultQuotesRepository
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore
import com.tangem.datasource.local.network.NetworksStatusesStore
import com.tangem.datasource.local.quote.QuotesStore
import com.tangem.datasource.local.token.UserMarketCoinsStore
import com.tangem.datasource.local.token.UserTokensStore
@ -68,6 +69,7 @@ internal object TokensDataModule {
@Provides
@Singleton
fun provideNetworksRepository(
networksStatusesStore: NetworksStatusesStore,
walletManagersFacade: WalletManagersFacade,
userWalletsStore: UserWalletsStore,
userTokensStore: UserTokensStore,
@ -75,6 +77,7 @@ internal object TokensDataModule {
dispatchers: CoroutineDispatcherProvider,
): NetworksRepository {
return DefaultNetworksRepository(
networksStatusesStore = networksStatusesStore,
walletManagersFacade = walletManagersFacade,
userWalletsStore = userWalletsStore,
userTokensStore = userTokensStore,

View file

@ -8,24 +8,23 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.token.UserMarketCoinsStore
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.core.error.DataError
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.HttpException
import timber.log.Timber
import java.net.ConnectException
import java.net.UnknownHostException
internal class DefaultCurrenciesRepository(
private val tangemTechApi: TangemTechApi,
@ -110,14 +109,6 @@ internal class DefaultCurrenciesRepository(
}
}
private fun List<UserTokensResponse.Token>.hasCoinForToken(token: CryptoCurrency.Token): Boolean {
return any {
val blockchain = getBlockchain(networkId = token.network.id)
it.id == blockchain.toCoinId()
}
}
override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) =
withContext(dispatchers.io) {
val savedCurrencies = requireNotNull(
@ -166,13 +157,14 @@ internal class DefaultCurrenciesRepository(
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
launch(dispatchers.io) {
getMultiCurrencyWalletCurrencies(userWallet).collect(::send)
getMultiCurrencyWalletCurrencies(userWallet)
.collectLatest(::send)
}
launch(dispatchers.io) {
withContext(dispatchers.io) {
fetchTokensIfCacheExpired(userWallet, refresh = false)
}
}
}.cancellable()
}
override suspend fun getMultiCurrencyWalletCurrenciesSync(
@ -232,7 +224,7 @@ internal class DefaultCurrenciesRepository(
.map { it.group == UserTokensResponse.GroupType.NETWORK }
.collect(::send)
}
}
}.cancellable()
}
override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow<Boolean> {
@ -244,7 +236,7 @@ internal class DefaultCurrenciesRepository(
.map { it.sort == UserTokensResponse.SortType.BALANCE }
.collect(::send)
}
}
}.cancellable()
}
private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow<List<CryptoCurrency>> {
@ -272,8 +264,8 @@ internal class DefaultCurrenciesRepository(
// The response may contain repeated tokens
copy(tokens = tokens.distinct())
}
} catch (e: HttpException) {
handleCurrenciesNotFoundOrThrow(userWallet, e)
} catch (e: Throwable) {
handleFetchTokensError(userWallet, e)
}
userTokensStore.store(userWallet.walletId, response)
@ -296,19 +288,8 @@ internal class DefaultCurrenciesRepository(
}
}
private suspend fun handleCurrenciesNotFoundOrThrow(
userWallet: UserWallet,
httpException: HttpException,
): UserTokensResponse {
private suspend fun handleFetchTokensError(userWallet: UserWallet, throwable: Throwable): UserTokensResponse {
val userWalletId = userWallet.walletId
if (httpException.code() != NOT_FOUND_HTTP_CODE) {
Timber.e(httpException, "Unable to fetch currencies for: $userWalletId")
throw httpException
}
Timber.d("Requested currencies could not be found in the remote store for: $userWalletId")
val response = userTokensStore.getSyncOrNull(userWalletId)
?: userTokensResponseFactory.createUserTokensResponse(
currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse),
@ -316,7 +297,28 @@ internal class DefaultCurrenciesRepository(
isSortedByBalance = false,
)
tangemTechApi.saveUserTokens(userWalletId.stringValue, response)
when (throwable) {
is ConnectException,
is UnknownHostException,
-> {
Timber.e("Unable to fetch currencies due to lack of internet connection")
}
is HttpException -> {
if (throwable.code() == NOT_FOUND_HTTP_CODE) {
Timber.w(
throwable,
"Requested currencies could not be found in the remote store for: $userWalletId",
)
tangemTechApi.saveUserTokens(userWalletId.stringValue, response)
} else {
Timber.e(throwable, "Unable to fetch currencies for: $userWalletId")
}
}
else -> {
throw throwable
}
}
return response
}

View file

@ -3,7 +3,7 @@ package com.tangem.data.tokens.repository
import com.tangem.blockchain.common.Blockchain
import com.tangem.datasource.local.token.UserMarketCoinsStore
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
import com.tangem.domain.wallets.models.UserWalletId

View file

@ -4,21 +4,27 @@ import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory
import com.tangem.data.tokens.utils.NetworkStatusFactory
import com.tangem.data.tokens.utils.ResponseCryptoCurrenciesFactory
import com.tangem.datasource.local.network.NetworksStatusesStore
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import timber.log.Timber
internal class DefaultNetworksRepository(
private val networksStatusesStore: NetworksStatusesStore,
private val walletManagersFacade: WalletManagersFacade,
private val userWalletsStore: UserWalletsStore,
private val userTokensStore: UserTokensStore,
@ -31,20 +37,19 @@ internal class DefaultNetworksRepository(
private val responseCurrenciesFactory by lazy { ResponseCryptoCurrenciesFactory(demoConfig) }
private val networkStatusFactory by lazy { NetworkStatusFactory() }
private val networksStatuses: MutableStateFlow<Set<NetworkStatus>> = MutableStateFlow(hashSetOf())
override fun getNetworkStatusesUpdates(
userWalletId: UserWalletId,
networks: Set<Network>,
): Flow<Set<NetworkStatus>> = channelFlow {
launch(dispatchers.io) {
networksStatuses.collect(::send)
networksStatusesStore.get(userWalletId)
.collectLatest(::send)
}
launch(dispatchers.io) {
withContext(dispatchers.io) {
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh = false)
}
}
}.cancellable()
override suspend fun getNetworkStatusesSync(
userWalletId: UserWalletId,
@ -52,7 +57,7 @@ internal class DefaultNetworksRepository(
refresh: Boolean,
): Set<NetworkStatus> = withContext(dispatchers.io) {
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh)
networksStatuses.first().toSet()
networksStatusesStore.getSyncOrNull(userWalletId).orEmpty()
}
private suspend fun fetchNetworksStatusesIfCacheExpired(
@ -92,17 +97,15 @@ internal class DefaultNetworksRepository(
extraTokens = currencies.filterIsInstance<CryptoCurrency.Token>().toSet(),
)
invalidateCacheKeyIfNeeded(userWalletId, network, result)
val networkStatus = networkStatusFactory.createNetworkStatus(
network = network,
result = result,
currencies = currencies.toSet(),
)
networksStatuses.update { statuses ->
statuses.addOrReplace(networkStatus) { it.network == networkStatus.network }
}
invalidateCacheKeyIfNeeded(userWalletId, networkStatus)
networksStatusesStore.store(userWalletId, networkStatus)
}
private suspend fun getCurrencies(userWalletId: UserWalletId, network: Network): Sequence<CryptoCurrency> {
@ -125,18 +128,32 @@ internal class DefaultNetworksRepository(
return currencies.filter { it.network == network }
}
private suspend fun invalidateCacheKeyIfNeeded(userWalletId: UserWalletId, networkStatus: NetworkStatus) {
when (networkStatus.value) {
is NetworkStatus.Verified,
is NetworkStatus.NoAccount,
private suspend fun invalidateCacheKeyIfNeeded(
userWalletId: UserWalletId,
network: Network,
result: UpdateWalletManagerResult,
) {
when (result) {
is UpdateWalletManagerResult.Verified,
is UpdateWalletManagerResult.NoAccount,
-> Unit
is NetworkStatus.Unreachable,
is NetworkStatus.MissedDerivation,
-> cacheRegistry.invalidate(getNetworksStatusesCacheKey(userWalletId, networkStatus.network))
is UpdateWalletManagerResult.Unreachable,
is UpdateWalletManagerResult.MissedDerivation,
-> {
Timber.w(
"""
Invalidate network cache key
|- User wallet ID: $userWalletId
|- Network: ${network.id}
""".trimIndent(),
)
cacheRegistry.invalidate(getNetworksStatusesCacheKey(userWalletId, network))
}
}
}
private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId, network: Network): String {
return "network_status_${userWalletId}_${network.id}_${network.derivationPath.value}"
return "network_status_${userWalletId}_${network.id.value}_${network.derivationPath.value}"
}
}

View file

@ -5,8 +5,8 @@ import com.tangem.data.tokens.utils.QuotesConverter
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore
import com.tangem.datasource.local.quote.QuotesStore
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.Quote
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
@ -24,23 +24,22 @@ internal class DefaultQuotesRepository(
private val quotesConverter = QuotesConverter()
@Volatile
private var quotesFetchedForAppCurrency: String? = null
override fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<Quote>> {
return channelFlow {
launch(dispatchers.io) {
quotesStore.get(currenciesIds)
.map(quotesConverter::convertSet)
.collect(::send)
}
override fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<Quote>> = channelFlow {
launch(dispatchers.io) {
quotesStore.get(currenciesIds)
.map(quotesConverter::convertSet)
.collectLatest(::send)
}
launch(dispatchers.io) {
selectedAppCurrencyStore.get().collectLatest { appCurrency ->
fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = false)
}
withContext(dispatchers.io) {
selectedAppCurrencyStore.get().collectLatest { appCurrency ->
fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = false)
}
}
}
}.cancellable()
override suspend fun getQuotesSync(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Set<Quote> {
return withContext(dispatchers.io) {
@ -73,17 +72,15 @@ internal class DefaultQuotesRepository(
}
private suspend fun fetchQuotes(rawCurrenciesIds: Set<String>, appCurrencyId: String) {
try {
val response = tangemTechApi.getQuotes(
currencyId = appCurrencyId,
coinIds = rawCurrenciesIds.joinToString(separator = ","),
)
quotesStore.store(response)
val response = try {
val coinIds = rawCurrenciesIds.joinToString(separator = ",")
tangemTechApi.getQuotes(appCurrencyId, coinIds)
} catch (e: Throwable) {
Timber.e(e, "Unable to fetch quotes for: $rawCurrenciesIds")
throw e
}
quotesStore.store(response)
}
private suspend fun filterExpiredCurrenciesIds(

View file

@ -6,7 +6,7 @@ import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
internal class CardCryptoCurrenciesFactory(private val demoConfig: DemoConfig) {

View file

@ -3,7 +3,7 @@ package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import timber.log.Timber
import com.tangem.blockchain.common.Token as SdkToken

View file

@ -2,7 +2,7 @@ package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.tokens.model.Network
import timber.log.Timber
internal fun getBlockchain(networkId: Network.ID): Blockchain {

View file

@ -1,9 +1,9 @@
package com.tangem.data.tokens.utils
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount
import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction

View file

@ -1,7 +1,7 @@
package com.tangem.data.tokens.utils
import com.tangem.datasource.local.quote.model.StoredQuote
import com.tangem.domain.tokens.models.Quote
import com.tangem.domain.tokens.model.Quote
import com.tangem.utils.converter.Converter
internal class QuotesConverter : Converter<StoredQuote, Quote> {

View file

@ -9,7 +9,7 @@ import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import timber.log.Timber
import com.tangem.blockchain.common.Token as SdkToken

View file

@ -2,16 +2,18 @@ package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.IconsUtil
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.tokens.models.CryptoCurrency.ID
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrency.ID
import com.tangem.domain.tokens.model.Network
import com.tangem.blockchain.common.Token as SdkToken
import com.tangem.domain.tokens.models.CryptoCurrency.ID.Body as CurrencyIdBody
import com.tangem.domain.tokens.models.CryptoCurrency.ID.Prefix.COIN_PREFIX as COIN_ID_PREFIX
import com.tangem.domain.tokens.models.CryptoCurrency.ID.Prefix.TOKEN_PREFIX as TOKEN_ID_PREFIX
import com.tangem.domain.tokens.models.CryptoCurrency.ID.Suffix.ContractAddress as CustomCurrencyIdSuffix
import com.tangem.domain.tokens.models.CryptoCurrency.ID.Suffix.RawID as CurrencyIdSuffix
import com.tangem.domain.tokens.model.CryptoCurrency.ID.Body as CurrencyIdBody
import com.tangem.domain.tokens.model.CryptoCurrency.ID.Prefix.COIN_PREFIX as COIN_ID_PREFIX
import com.tangem.domain.tokens.model.CryptoCurrency.ID.Prefix.TOKEN_PREFIX as TOKEN_ID_PREFIX
import com.tangem.domain.tokens.model.CryptoCurrency.ID.Suffix.ContractAddress as CustomCurrencyIdSuffix
import com.tangem.domain.tokens.model.CryptoCurrency.ID.Suffix.RawID as CurrencyIdSuffix
private const val DEFAULT_TOKENS_ICONS_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins"
private const val TOKEN_ICON_SIZE = "large"
@ -60,6 +62,14 @@ internal fun getCoinIconUrl(blockchain: Blockchain): String? {
return coinId?.let(::getTokenIconUrlFromDefaultHost)
}
internal fun List<UserTokensResponse.Token>.hasCoinForToken(token: CryptoCurrency.Token): Boolean {
return any {
val blockchain = getBlockchain(networkId = token.network.id)
it.id == blockchain.toCoinId()
}
}
private fun getCurrencyIdBody(network: Network): CurrencyIdBody {
return when (val path = network.derivationPath) {
is Network.DerivationPath.Custom -> CurrencyIdBody.NetworkIdWithDerivationPath(

View file

@ -2,7 +2,7 @@ package com.tangem.data.tokens.utils
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
internal class UserTokensResponseFactory {