Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-29 17:08:18 +03:00
commit 1bac8466b1
990 changed files with 22183 additions and 7489 deletions

View file

@ -14,44 +14,52 @@ android {
dependencies {
/** Project - Domain */
// region Project - Data
implementation(projects.data.common)
implementation(projects.data.networks)
// endregion
// region Project - Domain
implementation(projects.domain.core)
implementation(projects.domain.demo)
implementation(projects.domain.legacy)
implementation(projects.domain.models)
implementation(projects.domain.staking)
implementation(projects.domain.staking.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.staking.models)
implementation(projects.domain.staking)
// endregion
/** Project - Data */
// region Project - Utils
implementation(projects.core.datasource)
implementation(projects.data.common)
/** Project - Utils */
implementation(projects.core.utils)
implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
// endregion
/** Tangem SDKs */
// region Tangem SDKs
implementation(tangemDeps.blockchain)
implementation(tangemDeps.card.core)
// endregion
/** AndroidX */
// region AndroidX
implementation(deps.androidx.datastore)
implementation(deps.androidx.paging.runtime)
// endregion
/** DI */
// region DI
implementation(deps.hilt.core)
kapt(deps.hilt.kapt)
// endregion
/** Other */
// region Other
implementation(deps.jodatime)
implementation(deps.kotlin.coroutines)
implementation(deps.moshi.kotlin)
implementation(deps.jodatime)
implementation(deps.timber)
implementation(deps.retrofit) // For HttpException
implementation(deps.androidx.paging.runtime)
implementation(deps.timber)
ksp(deps.moshi.kotlin.codegen)
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
// endregion
}

View file

@ -2,14 +2,21 @@ package com.tangem.data.tokens.di
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.tokens.repository.*
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.data.tokens.repository.DefaultCurrenciesRepository
import com.tangem.data.tokens.repository.DefaultCurrencyChecksRepository
import com.tangem.data.tokens.repository.DefaultPolkadotAccountHealthCheckRepository
import com.tangem.data.tokens.repository.DefaultQuotesRepository
import com.tangem.datasource.api.tangemTech.TangemTechApi
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
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -33,6 +40,8 @@ internal object TokensDataModule {
dispatchers: CoroutineDispatcherProvider,
expressServiceLoader: ExpressServiceLoader,
excludedBlockchains: ExcludedBlockchains,
cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
tokensSaver: UserTokensSaver,
): CurrenciesRepository {
return DefaultCurrenciesRepository(
tangemTechApi = tangemTechApi,
@ -43,6 +52,8 @@ internal object TokensDataModule {
expressServiceLoader = expressServiceLoader,
dispatchers = dispatchers,
excludedBlockchains = excludedBlockchains,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
userTokensSaver = tokensSaver,
)
}
@ -64,28 +75,6 @@ internal object TokensDataModule {
)
}
@Provides
@Singleton
fun provideNetworksRepository(
networksStatusesStore: NetworksStatusesStore,
walletManagersFacade: WalletManagersFacade,
userWalletsStore: UserWalletsStore,
appPreferencesStore: AppPreferencesStore,
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
excludedBlockchains: ExcludedBlockchains,
): NetworksRepository {
return DefaultNetworksRepository(
networksStatusesStore = networksStatusesStore,
walletManagersFacade = walletManagersFacade,
userWalletsStore = userWalletsStore,
appPreferencesStore = appPreferencesStore,
cacheRegistry = cacheRegistry,
dispatchers = dispatchers,
excludedBlockchains = excludedBlockchains,
)
}
@Provides
@Singleton
fun provideCurrencyChecksRepository(

View file

@ -6,9 +6,7 @@ import com.tangem.blockchainsdk.utils.*
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.common.currency.*
import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory
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
@ -20,14 +18,13 @@ 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.userwallet.UserWalletsStore
import com.tangem.domain.core.error.DataError
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.FeePaidCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
@ -47,16 +44,20 @@ internal class DefaultCurrenciesRepository(
private val appPreferencesStore: AppPreferencesStore,
private val expressServiceLoader: ExpressServiceLoader,
private val dispatchers: CoroutineDispatcherProvider,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
private val userTokensSaver: UserTokensSaver,
excludedBlockchains: ExcludedBlockchains,
) : CurrenciesRepository {
private val demoConfig = DemoConfig()
private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains)
private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains)
private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig, excludedBlockchains)
private val userTokensResponseFactory = UserTokensResponseFactory()
private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility()
private val customTokensMerger = CustomTokensMerger(tangemTechApi, dispatchers)
private val customTokensMerger = CustomTokensMerger(
tangemTechApi = tangemTechApi,
dispatchers = dispatchers,
userTokensSaver = userTokensSaver,
)
override suspend fun saveTokens(
userWalletId: UserWalletId,
@ -71,8 +72,7 @@ internal class DefaultCurrenciesRepository(
isGroupedByNetwork = isGroupedByNetwork,
isSortedByBalance = isSortedByBalance,
)
storeAndPushTokens(userWalletId, response)
userTokensSaver.storeAndPush(userWalletId, response)
}
override suspend fun saveNewCurrenciesList(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
@ -87,7 +87,7 @@ internal class DefaultCurrenciesRepository(
val updatedResponse = savedResponse.copy(
tokens = newCurrencies.map(userTokensResponseFactory::createResponseToken),
)
storeAndPushTokens(
userTokensSaver.storeAndPush(
userWalletId = userWalletId,
response = updatedResponse,
)
@ -111,7 +111,7 @@ internal class DefaultCurrenciesRepository(
val updatedResponse = savedCurrencies.copy(
tokens = savedCurrencies.tokens + currenciesToAdd.map(userTokensResponseFactory::createResponseToken),
)
storeAndPushTokens(
userTokensSaver.storeAndPush(
userWalletId = userWalletId,
response = updatedResponse,
)
@ -124,8 +124,7 @@ internal class DefaultCurrenciesRepository(
currenciesToAdd: List<CryptoCurrency>,
): List<CryptoCurrency> {
return currenciesToAdd.filter { currency ->
val blockchain = getBlockchain(networkId = currency.network.id)
val networkId = blockchain.toNetworkId()
val networkId = currency.network.toBlockchain().toNetworkId()
val contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress
savedCurrencies.none { token ->
@ -175,9 +174,10 @@ internal class DefaultCurrenciesRepository(
)
val token = userTokensResponseFactory.createResponseToken(currency)
storeAndPushTokens(
val updatedResponse = savedCurrencies.copy(tokens = savedCurrencies.tokens.filterNot { it == token })
userTokensSaver.storeAndPush(
userWalletId = userWalletId,
response = savedCurrencies.copy(tokens = savedCurrencies.tokens.filterNot { it == token }),
response = updatedResponse,
)
}
@ -189,11 +189,12 @@ internal class DefaultCurrenciesRepository(
)
val tokens = currencies.map(userTokensResponseFactory::createResponseToken)
storeAndPushTokens(
val updatedResponse = savedCurrencies.copy(
tokens = savedCurrencies.tokens.filterNot(tokens::contains),
)
userTokensSaver.storeAndPush(
userWalletId = userWalletId,
response = savedCurrencies.copy(
tokens = savedCurrencies.tokens.filterNot(tokens::contains),
),
response = updatedResponse,
)
}
}
@ -220,7 +221,7 @@ internal class DefaultCurrenciesRepository(
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
val currency = cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse)
val currency = cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse)
fetchExpressAssetsByNetworkIds(userWalletId, listOf(currency), refresh)
currency
}
@ -231,12 +232,16 @@ internal class DefaultCurrenciesRepository(
refresh: Boolean,
): List<CryptoCurrency> {
return withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
val scanResponse = getUserWallet(userWalletId).scanResponse
val currencies = if (scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = scanResponse)
} else {
cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = scanResponse).run {
listOf(this)
}
}
val currencies = cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(
userWallet.scanResponse,
)
fetchExpressAssetsByNetworkIds(userWalletId, currencies, refresh)
currencies
}
@ -250,7 +255,9 @@ internal class DefaultCurrenciesRepository(
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
val currency = cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
val currency = cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(
scanResponse = userWallet.scanResponse,
)
.find { it.id == id }
requireNotNull(currency) { "Unable to find currency with provided ID: $id" }
fetchExpressAssetsByNetworkIds(userWalletId, listOf(currency))
@ -350,7 +357,7 @@ internal class DefaultCurrenciesRepository(
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
},
)
val blockchain = Blockchain.fromId(networkId.value)
val blockchain = networkId.toBlockchain()
val blockchainNetworkId = blockchain.toNetworkId()
val coinId = blockchain.toCoinId()
@ -403,7 +410,7 @@ internal class DefaultCurrenciesRepository(
cryptoCurrencyStatus: CryptoCurrencyStatus,
coinStatus: CryptoCurrencyStatus?,
): Boolean {
val blockchain = Blockchain.fromId(cryptoCurrencyStatus.currency.network.id.value)
val blockchain = cryptoCurrencyStatus.currency.network.toBlockchain()
val isBitcoinBlockchain = blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet
return when {
cryptoCurrencyStatus.currency is CryptoCurrency.Coin && isBitcoinBlockchain -> {
@ -418,7 +425,7 @@ internal class DefaultCurrenciesRepository(
override suspend fun getFeePaidCurrency(userWalletId: UserWalletId, network: Network): FeePaidCurrency {
return withContext(dispatchers.io) {
val blockchain = Blockchain.fromId(network.id.value)
val blockchain = network.toBlockchain()
when (val feePaidCurrency = blockchain.feePaidCurrency()) {
FeePaidSdkCurrency.Coin -> FeePaidCurrency.Coin
FeePaidSdkCurrency.SameCurrency -> FeePaidCurrency.SameCurrency
@ -530,6 +537,16 @@ internal class DefaultCurrenciesRepository(
return blockchain?.isNetworkFeeZero() ?: false
}
override suspend fun syncTokens(userWalletId: UserWalletId) {
val savedCurrencies = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform add currencies action" },
)
userTokensSaver.push(userWalletId, savedCurrencies, onFailSend = {
throw IllegalStateException("Unable to push tokens")
},)
}
private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow<List<CryptoCurrency>> {
return getSavedUserTokensResponse(userWallet.walletId).map { storedTokens ->
responseCurrenciesFactory.createCurrencies(
@ -567,13 +584,9 @@ internal class DefaultCurrenciesRepository(
val compatibleUserTokensResponse = response
.let { it.copy(tokens = it.tokens.distinct()) }
.let { customTokensMerger.mergeIfPresented(userWalletId, response) }
.let(userTokensBackwardCompatibility::applyCompatibilityAndGetUpdated)
.let { customTokensMerger.mergeIfPresented(userWalletId, it) }
appPreferencesStore.storeObject(
key = PreferencesKeys.getUserTokensKey(userWalletId = userWallet.walletId.stringValue),
value = compatibleUserTokensResponse,
)
userTokensSaver.store(userWalletId, compatibleUserTokensResponse)
fetchExpressAssetsByNetworkIds(userWalletId, compatibleUserTokensResponse)
}
@ -584,16 +597,6 @@ internal class DefaultCurrenciesRepository(
return demoConfig.isDemoCardId(userWallet.cardId) && response == null
}
private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) {
val compatibleUserTokensResponse = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(response)
appPreferencesStore.storeObject(
key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue),
value = compatibleUserTokensResponse,
)
pushTokens(userWalletId, response)
}
private suspend fun fetchExpressAssetsByNetworkIds(userWalletId: UserWalletId, userTokens: UserTokensResponse) {
val tokens = userTokens.tokens.map { token ->
LeastTokenInfo(
@ -641,7 +644,7 @@ internal class DefaultCurrenciesRepository(
if (e is ApiResponseError.HttpException && e.code == ApiResponseError.HttpException.Code.NOT_FOUND) {
Timber.w(e, "Requested currencies could not be found in the remote store for: $userWalletId")
pushTokens(userWalletId, response)
userTokensSaver.push(userWalletId, response)
} else {
cacheRegistry.invalidate(getTokensCacheKey(userWalletId))
}
@ -649,15 +652,9 @@ internal class DefaultCurrenciesRepository(
return response
}
private suspend fun pushTokens(userWalletId: UserWalletId, response: UserTokensResponse) {
safeApiCall({ tangemTechApi.saveUserTokens(userWalletId.stringValue, response).bind() }) {
Timber.e(it, "Unable to save user tokens for: ${userWalletId.stringValue}")
}
}
private fun createDefaultUserTokensResponse(userWallet: UserWallet) =
userTokensResponseFactory.createUserTokensResponse(
currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse),
currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse),
isGroupedByNetwork = false,
isSortedByBalance = false,
)

View file

@ -6,12 +6,12 @@ import com.tangem.blockchain.common.MinimumSendAmountProvider
import com.tangem.blockchain.common.ReserveAmountProvider
import com.tangem.blockchain.common.UtxoAmountLimitProvider
import com.tangem.data.tokens.converters.UtxoConverter
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.utils.getTotalStakingBalance
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.CurrencyAmount
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
@ -136,7 +136,7 @@ internal class DefaultCurrencyChecksRepository(
val balanceValue = currencyStatus.value as? CryptoCurrencyStatus.Loaded ?: return null
val stakingBalance = balanceValue.yieldBalance as? YieldBalance.Data
val stakingTotalBalance = stakingBalance?.getTotalStakingBalance(
blockchainId = currencyStatus.currency.network.id.value,
blockchainId = currencyStatus.currency.network.rawId,
).orZero()
return when {
balanceValue.amount.isZero() && stakingTotalBalance.isZero() -> null

View file

@ -1,271 +0,0 @@
package com.tangem.data.tokens.repository
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory
import com.tangem.data.tokens.utils.NetworkStatusFactory
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.network.NetworksStatusesStore
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.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
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 kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import timber.log.Timber
@Suppress("LongParameterList")
internal class DefaultNetworksRepository(
private val networksStatusesStore: NetworksStatusesStore,
private val walletManagersFacade: WalletManagersFacade,
private val userWalletsStore: UserWalletsStore,
private val appPreferencesStore: AppPreferencesStore,
private val cacheRegistry: CacheRegistry,
private val dispatchers: CoroutineDispatcherProvider,
excludedBlockchains: ExcludedBlockchains,
) : NetworksRepository {
private val demoConfig = DemoConfig()
private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig, excludedBlockchains)
private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains)
private val networkStatusFactory = NetworkStatusFactory()
override fun getNetworkStatusesUpdates(
userWalletId: UserWalletId,
networks: Set<Network>,
): Flow<Set<NetworkStatus>> {
return networksStatusesStore.get(userWalletId, networks)
.distinctUntilChanged()
.flowOn(dispatchers.io)
}
override suspend fun fetchNetworkStatuses(userWalletId: UserWalletId, networks: Set<Network>, refresh: Boolean) {
withContext(dispatchers.io) {
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh)
}
}
override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set<Network>) {
val currencies = getCurrencies(userWalletId, networks)
withContext(dispatchers.io) {
fetchNetworksPendingTransactions(userWalletId, networks, currencies)
}
}
override suspend fun getNetworkStatusesSync(
userWalletId: UserWalletId,
networks: Set<Network>,
refresh: Boolean,
): Set<NetworkStatus> = withContext(dispatchers.io) {
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh)
networksStatusesStore.getSyncOrNull(userWalletId).orEmpty()
}
override fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean {
val blockchain = Blockchain.fromNetworkId(network.id.value)
return REQUIRED_ACCOUNT_WITHOUT_RESERVE_BLOCKCHAINS.contains(blockchain)
}
override suspend fun getNetworkAddresses(
userWalletId: UserWalletId,
network: Network,
): List<CryptoCurrencyAddress> = withContext(dispatchers.io) {
// Get list of currencies matching [network]
val currencies = getCurrencies(userWalletId)
.filter { currency -> network.id == currency.network.id }
// There is no currencies matching given [networks] in [userWalletId]
if (currencies.toList().isEmpty()) return@withContext emptyList()
currencies.toList().map { currency ->
CryptoCurrencyAddress(
cryptoCurrency = currency,
address = walletManagersFacade.getAddresses(userWalletId, currency.network)
.firstOrNull { it.type == AddressType.Default }
?.value.orEmpty(),
)
}
}
private suspend fun fetchNetworksStatusesIfCacheExpired(
userWalletId: UserWalletId,
networks: Set<Network>,
refresh: Boolean,
) = coroutineScope {
if (refresh) {
networksStatusesStore.refresh(key = userWalletId, networks = networks)
}
val currencies = getCurrencies(userWalletId, networks)
val networksDeferred = networks.mapNotNull { network ->
coroutineScope {
val key = getNetworksStatusesCacheKey(userWalletId, network)
if (refresh || cacheRegistry.isExpired(key)) {
async {
cacheRegistry.invokeOnExpire(
key = key,
skipCache = refresh,
block = { fetchNetworkStatus(userWalletId, network, currencies) },
)
}
} else {
null
}
}
}
networksDeferred.awaitAll()
}
private suspend fun fetchNetworksPendingTransactions(
userWalletId: UserWalletId,
networks: Set<Network>,
currencies: Sequence<CryptoCurrency>,
) {
coroutineScope {
networks
.map { network ->
async {
fetchNetworkPendingTransactions(userWalletId, network, currencies)
}
}
.awaitAll()
}
}
private suspend fun fetchNetworkStatus(
userWalletId: UserWalletId,
network: Network,
currencies: Sequence<CryptoCurrency>,
) {
val networkCurrencies = currencies.filter { it.network == network }
val result = walletManagersFacade.update(
userWalletId = userWalletId,
network = network,
extraTokens = networkCurrencies
.filterIsInstance<CryptoCurrency.Token>()
.toSet(),
)
withContext(NonCancellable) {
invalidateCacheKeyIfNeeded(userWalletId, network, result)
}
val networkStatus = networkStatusFactory.createNetworkStatus(
network = network,
result = result,
currencies = networkCurrencies.toSet(),
)
networksStatusesStore.store(userWalletId, networkStatus)
}
private suspend fun fetchNetworkPendingTransactions(
userWalletId: UserWalletId,
network: Network,
currencies: Sequence<CryptoCurrency>,
) {
val result = walletManagersFacade.updatePendingTransactions(
userWalletId = userWalletId,
network = network,
)
withContext(NonCancellable) {
invalidateCacheKeyIfNeeded(userWalletId, network, result)
}
val networkStatus = networkStatusFactory.createNetworkStatus(
network = network,
result = result,
currencies = currencies.toSet(),
)
networksStatusesStore.store(userWalletId, networkStatus)
}
private suspend fun getCurrencies(userWalletId: UserWalletId, networks: Set<Network>): Sequence<CryptoCurrency> {
val currencies = getCurrencies(userWalletId)
return currencies.filter { it.network in networks }
}
private suspend fun getCurrencies(userWalletId: UserWalletId): Sequence<CryptoCurrency> {
val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"Unable to find user wallet with provided ID: $userWalletId"
}
return if (userWallet.isMultiCurrency) {
val response = requireNotNull(
value = appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue),
),
lazyMessage = {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
},
)
responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse).asSequence()
} else {
if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
.asSequence()
} else {
val currency = cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse)
sequenceOf(currency)
}
}
}
private suspend fun invalidateCacheKeyIfNeeded(
userWalletId: UserWalletId,
network: Network,
result: UpdateWalletManagerResult,
) {
when (result) {
is UpdateWalletManagerResult.Verified,
is UpdateWalletManagerResult.NoAccount,
-> Unit
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.value}_${network.derivationPath.value}"
}
private companion object {
val REQUIRED_ACCOUNT_WITHOUT_RESERVE_BLOCKCHAINS = listOf(Blockchain.Aptos, Blockchain.Filecoin)
}
}

View file

@ -5,13 +5,14 @@ import com.tangem.blockchain.blockchains.polkadot.AccountCheckProvider
import com.tangem.blockchain.blockchains.polkadot.network.accounthealthcheck.ExtrinsicListItemResponse
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys.POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
import com.tangem.datasource.local.preferences.utils.getObjectSetSync
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
@ -39,7 +40,7 @@ internal class DefaultPolkadotAccountHealthCheckRepository(
override suspend fun runCheck(userWalletId: UserWalletId, network: Network) {
// Run Polkadot account health check
if (Blockchain.fromId(network.id.value) != Blockchain.Polkadot) return
if (network.toBlockchain() != Blockchain.Polkadot) return
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network)
val address = requireNotNull(walletManager?.wallet?.address) {

View file

@ -10,7 +10,7 @@ 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.quote.QuotesStore
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider

View file

@ -1,86 +0,0 @@
package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
class CardCryptoCurrenciesFactory(
private val demoConfig: DemoConfig,
excludedBlockchains: ExcludedBlockchains,
) {
private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains)
fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List<CryptoCurrency.Coin> {
val card = scanResponse.card
var blockchains = if (demoConfig.isDemoCardId(card.cardId)) {
demoConfig.demoBlockchains
} else {
listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}
if (card.isTestCard) {
blockchains = blockchains.mapNotNull { it.getTestnetVersion() }
}
return blockchains.mapNotNull {
cryptoCurrencyFactory.createCoin(
blockchain = it,
extraDerivationPath = null,
scanResponse = scanResponse,
)
}
}
fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency {
val resolver = scanResponse.cardTypesResolver
val blockchain = resolver.getBlockchain()
val coin = cryptoCurrencyFactory.createCoin(
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,
)
requireNotNull(coin) { "Coin for the single currency card cannot be null" }
val primaryToken = resolver.getPrimaryToken()?.let { token ->
cryptoCurrencyFactory.createToken(
sdkToken = token,
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,
)
}
return primaryToken ?: coin
}
fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List<CryptoCurrency> {
val resolver = scanResponse.cardTypesResolver
val blockchain = resolver.getBlockchain()
val coin = cryptoCurrencyFactory.createCoin(
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,
)
requireNotNull(coin) { "Coin for the single currency card cannot be null" }
val primaryToken = resolver.getPrimaryToken()?.let { token ->
cryptoCurrencyFactory.createToken(
sdkToken = token,
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,
)
}
return listOfNotNull(coin, primaryToken)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.data.tokens.utils
import arrow.atomic.AtomicBoolean
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.wallets.models.UserWalletId
@ -18,6 +19,7 @@ import timber.log.Timber
internal class CustomTokensMerger(
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
private val userTokensSaver: UserTokensSaver,
) {
/**
@ -47,7 +49,7 @@ internal class CustomTokensMerger(
// previously here was used compare response.tokens, but it's not working correctly
// because Token.equals() skip some fields
if (wasMerged.value) {
pushTokens(userWalletId, updatedResponse)
userTokensSaver.push(userWalletId, updatedResponse)
}
return updatedResponse
@ -102,10 +104,4 @@ internal class CustomTokensMerger(
},
)
}
private suspend fun pushTokens(userWalletId: UserWalletId, response: UserTokensResponse) {
safeApiCall({ tangemTechApi.saveUserTokens(userWalletId.stringValue, response).bind() }) {
Timber.e(it, "Unable to save user tokens for: $userWalletId")
}
}
}

View file

@ -1,134 +0,0 @@
package com.tangem.data.tokens.utils
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.*
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.walletmanager.model.Address
import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount
import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import timber.log.Timber
class NetworkStatusFactory {
fun createNetworkStatus(
network: Network,
result: UpdateWalletManagerResult,
currencies: Set<CryptoCurrency>,
): NetworkStatus {
return NetworkStatus(
network = network,
value = when (result) {
is UpdateWalletManagerResult.MissedDerivation -> NetworkStatus.MissedDerivation
is UpdateWalletManagerResult.Unreachable -> NetworkStatus.Unreachable(
address = getNetworkAddressOrNull(result.selectedAddress, result.addresses),
)
is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount(
address = getNetworkAddress(result.selectedAddress, result.addresses),
amountToCreateAccount = result.amountToCreateAccount,
errorMessage = result.errorMessage,
source = StatusSource.ACTUAL,
)
is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified(
address = getNetworkAddress(result.selectedAddress, result.addresses),
amounts = formatAmounts(result.currenciesAmounts, currencies),
pendingTransactions = formatTransactions(
transactions = result.currentTransactions,
currencies = currencies,
),
source = StatusSource.ACTUAL,
)
},
)
}
private fun formatAmounts(
amounts: Set<CryptoCurrencyAmount>,
currencies: Set<CryptoCurrency>,
): Map<CryptoCurrency.ID, CryptoCurrencyAmountStatus> {
return currencies.associate { currency ->
val amount = when (currency) {
is CryptoCurrency.Coin -> {
amounts.singleOrNull { it is CryptoCurrencyAmount.Coin }
}
is CryptoCurrency.Token -> {
amounts.firstOrNull { amount ->
amount is CryptoCurrencyAmount.Token &&
currency.id.rawCurrencyId == amount.tokenId &&
currency.contractAddress.equals(amount.tokenContractAddress, ignoreCase = true)
}
}
}
if (amount == null) {
Timber.w("Unable to find amount for cryptocurrency: $currency")
currency.id to CryptoCurrencyAmountStatus.NotFound
} else {
currency.id to CryptoCurrencyAmountStatus.Loaded(amount.value)
}
}
}
private fun formatTransactions(
transactions: Set<CryptoCurrencyTransaction>,
currencies: Set<CryptoCurrency>,
): Map<CryptoCurrency.ID, Set<TxHistoryItem>> {
if (transactions.isEmpty()) return emptyMap()
return currencies
.asSequence()
.map { currency ->
val currencyTransactions = when (currency) {
is CryptoCurrency.Coin -> transactions.filterTo(hashSetOf()) { transaction ->
transaction is CryptoCurrencyTransaction.Coin
}
is CryptoCurrency.Token -> transactions.filterTo(hashSetOf()) { transaction ->
transaction is CryptoCurrencyTransaction.Token &&
transaction.tokenContractAddress.equals(currency.contractAddress, ignoreCase = true)
}
}
currency.id to createCurrentTransactions(currencyTransactions)
}
.toMap()
}
private fun createCurrentTransactions(transactions: Set<CryptoCurrencyTransaction>): Set<TxHistoryItem> {
return transactions.mapTo(hashSetOf()) { it.txHistoryItem }
}
private fun getNetworkAddressOrNull(selectedAddress: String?, availableAddresses: Set<Address>?): NetworkAddress? {
if (selectedAddress == null || availableAddresses == null) {
return null
}
return getNetworkAddress(selectedAddress, availableAddresses)
}
private fun getNetworkAddress(selectedAddress: String, availableAddresses: Set<Address>): NetworkAddress {
val defaultAddress = availableAddresses
.firstOrNull { it.value == selectedAddress }
?.let(::mapToDomainAddress)
requireNotNull(defaultAddress) { "Selected address must not be null" }
return if (availableAddresses.size != 1) {
NetworkAddress.Selectable(defaultAddress, availableAddresses.mapTo(hashSetOf(), ::mapToDomainAddress))
} else {
NetworkAddress.Single(defaultAddress)
}
}
private fun mapToDomainAddress(address: Address): NetworkAddress.Address {
val type = when (address.type) {
Address.Type.Primary -> NetworkAddress.Address.Type.Primary
Address.Type.Secondary -> NetworkAddress.Address.Type.Secondary
}
if (address.value.isBlank()) {
Timber.w("Address value is blank")
}
return NetworkAddress.Address(address.value, type)
}
}

View file

@ -1,37 +0,0 @@
package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
/**
* Helper to apply compatibility changes for [UserTokensResponse] to support old saved tokens
* in new application with new IDs
*/
class UserTokensBackwardCompatibility {
fun applyCompatibilityAndGetUpdated(userTokensResponse: UserTokensResponse): UserTokensResponse {
return userTokensResponse.copy(
tokens = userTokensResponse.tokens.map { token ->
val oldSavedId = NETWORKS_TO_OLD_SAVED_IDS[token.networkId]
if (oldSavedId != null && token.id == oldSavedId) {
Blockchain.fromNetworkId(token.networkId)?.let { blockchain ->
token.copy(
id = blockchain.toCoinId(),
)
} ?: token
} else {
token
}
},
)
}
companion object {
private val NETWORKS_TO_OLD_SAVED_IDS = mapOf(
"optimistic-ethereum" to "ethereum",
"arbitrum-one" to "ethereum",
)
}
}