Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-19 11:04:47 +04:00
parent 2d3e5fdb7a
commit dd0add894f
20 changed files with 118 additions and 57 deletions

View file

@ -2,6 +2,7 @@ package com.tangem.tap.di.domain
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.nft.*
import com.tangem.domain.nft.repository.NFTRepository
@ -146,8 +147,15 @@ internal object NFTDomainModule {
fun provideClearNFTCacheUseCase(
nftRepository: NFTRepository,
currenciesRepository: CurrenciesRepository,
accountsFeatureToggles: AccountsFeatureToggles,
singleAccountListSupplier: SingleAccountListSupplier,
): ObserveAndClearNFTCacheIfNeedUseCase {
return ObserveAndClearNFTCacheIfNeedUseCase(nftRepository, currenciesRepository)
return ObserveAndClearNFTCacheIfNeedUseCase(
nftRepository = nftRepository,
currenciesRepository = currenciesRepository,
accountsFeatureToggles = accountsFeatureToggles,
singleAccountListSupplier = singleAccountListSupplier,
)
}
@Provides

View file

@ -268,7 +268,7 @@ internal class DefaultCurrenciesRepository(
}
}
override fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
private fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
return channelFlow {
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)

View file

@ -87,6 +87,19 @@ data class AccountList private constructor(
)
}
/**
* Flattens the list of accounts to extract all crypto currencies contained within them.
*
* @return a list of all crypto currencies from all accounts
*/
fun flattenCurrencies(): List<CryptoCurrency> {
return accounts.flatMap { account ->
when (account) {
is Account.CryptoPortfolio -> account.cryptoCurrencies
}
}
}
/**
* Represents possible errors that can occur when creating an `AccountList`
*/

View file

@ -3,6 +3,8 @@ package com.tangem.domain.account.supplier
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.producer.SingleAccountListProducer
import com.tangem.domain.core.flow.FlowCachingSupplier
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
/**
* Supplier that provides a single [AccountList] for a specific user wallet.
@ -12,4 +14,11 @@ import com.tangem.domain.core.flow.FlowCachingSupplier
abstract class SingleAccountListSupplier(
override val factory: SingleAccountListProducer.Factory,
override val keyCreator: (SingleAccountListProducer.Params) -> String,
) : FlowCachingSupplier<SingleAccountListProducer, SingleAccountListProducer.Params, AccountList>()
) : FlowCachingSupplier<SingleAccountListProducer, SingleAccountListProducer.Params, AccountList>() {
operator fun invoke(userWalletId: UserWalletId): Flow<AccountList> {
return super.invoke(
params = SingleAccountListProducer.Params(userWalletId = userWalletId),
)
}
}

View file

@ -5,7 +5,7 @@ import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.managetokens.repository.CustomTokensRepository
import com.tangem.domain.models.wallet.UserWalletId
@Deprecated("Use SaveCryptoCurrenciesUseCase")
@Deprecated("Use ManageCryptoCurrenciesUseCase")
class RemoveCustomManagedCryptoCurrencyUseCase(private val repository: CustomTokensRepository) {
suspend operator fun invoke(

View file

@ -19,7 +19,7 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Deprecated("Use SaveCryptoCurrenciesUseCase")
@Deprecated("Use ManageCryptoCurrenciesUseCase")
@Suppress("LongParameterList")
class SaveManagedTokensUseCase(
private val customTokensRepository: CustomTokensRepository,

View file

@ -43,7 +43,7 @@ interface CustomTokensRepository {
formValues: AddCustomTokenForm.Validated.All,
): CryptoCurrency.Token
@Deprecated("Use SaveCryptoCurrenciesUseCase")
@Deprecated("Use ManageCryptoCurrenciesUseCase")
suspend fun removeCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom)
suspend fun convertToCryptoCurrency(

View file

@ -25,7 +25,7 @@ import kotlinx.coroutines.withContext
*
[REDACTED_AUTHOR]
*/
@Deprecated("Use SaveCryptoCurrenciesUseCase")
@Deprecated("Use ManageCryptoCurrenciesUseCase")
@Suppress("LongParameterList")
class SaveMarketTokensUseCase(
private val derivationsRepository: DerivationsRepository,

View file

@ -20,6 +20,7 @@ class GetNFTCollectionsUseCase(
private val accountsFeatureToggles: AccountsFeatureToggles,
) {
@Deprecated("Use invokeForAccounts instead")
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(userWalletId: UserWalletId): Flow<List<NFTCollections>> =
if (accountsFeatureToggles.isFeatureEnabled) {

View file

@ -1,18 +1,23 @@
package com.tangem.domain.nft
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.*
class ObserveAndClearNFTCacheIfNeedUseCase(
private val nftRepository: NFTRepository,
private val currenciesRepository: CurrenciesRepository,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val singleAccountListSupplier: SingleAccountListSupplier,
) {
operator fun invoke(userWalletId: UserWalletId): Flow<Set<Network>> = currenciesRepository
.getWalletCurrenciesUpdates(userWalletId)
operator fun invoke(userWalletId: UserWalletId): Flow<Set<Network>> = getCryptoCurrencies(userWalletId)
.map { it.map(CryptoCurrency::network) }
.mapDiff { old, new ->
// calculate networks sets difference to determine which networks were removed
@ -25,6 +30,14 @@ class ObserveAndClearNFTCacheIfNeedUseCase(
}
}
private fun getCryptoCurrencies(userWalletId: UserWalletId): Flow<Collection<CryptoCurrency>> {
return if (accountsFeatureToggles.isFeatureEnabled) {
singleAccountListSupplier(userWalletId).map(AccountList::flattenCurrencies)
} else {
currenciesRepository.getWalletCurrenciesUpdates(userWalletId)
}
}
private fun <T, R> Flow<T>.mapDiff(diff: (old: T, new: T) -> R): Flow<R> = flow {
var previous: T? = null
collect { current ->

View file

@ -24,7 +24,7 @@ import kotlinx.coroutines.coroutineScope
* This use case interacts with the underlying repositories to both add currencies and refresh
* network statuses, particularly after the addition of new tokens.
*/
@Deprecated("Use SaveCryptoCurrenciesUseCase")
@Deprecated("Use ManageCryptoCurrenciesUseCase")
@Suppress("LongParameterList")
class AddCryptoCurrenciesUseCase(
private val currenciesRepository: CurrenciesRepository,

View file

@ -15,6 +15,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withContext
@Deprecated("Use ApplyAccountListSortingUseCase")
class ApplyTokenListSortingUseCase(
private val currenciesRepository: CurrenciesRepository,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,

View file

@ -2,10 +2,11 @@ package com.tangem.domain.tokens
import arrow.core.Either
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.models.wallet.UserWalletId
@Deprecated("Use MultiWalletCryptoCurrenciesSupplier")
class GetCryptoCurrenciesUseCase(
private val currenciesRepository: CurrenciesRepository,
) {

View file

@ -9,14 +9,14 @@ import com.tangem.domain.tokens.model.remove.RemoveCurrencyError
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
@Deprecated("Use SaveCryptoCurrenciesUseCase")
@Deprecated("Use ManageCryptoCurrenciesUseCase")
class RemoveCurrencyUseCase(
private val currenciesRepository: CurrenciesRepository,
private val walletManagersFacade: WalletManagersFacade,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
) {
@Deprecated("Use SaveCryptoCurrenciesUseCase")
@Deprecated("Use ManageCryptoCurrenciesUseCase")
suspend operator fun invoke(
userWalletId: UserWalletId,
currency: CryptoCurrency,

View file

@ -27,6 +27,7 @@ interface CurrenciesRepository {
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
@Deprecated("Use ManageCryptoCurrenciesUseCase")
suspend fun saveTokens(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
@ -42,7 +43,7 @@ interface CurrenciesRepository {
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
@Deprecated("Use SaveCryptoCurrenciesUseCase")
@Deprecated("Use ManageCryptoCurrenciesUseCase")
suspend fun addCurrenciesCache(userWalletId: UserWalletId, currencies: List<CryptoCurrency>): List<CryptoCurrency>
/**
@ -53,7 +54,7 @@ interface CurrenciesRepository {
* @throws DataError.UserWalletError.WrongUserWallet If multi-currency user wallet
* ID provided.
*/
@Deprecated("Use SaveCryptoCurrenciesUseCase")
@Deprecated("Use ManageCryptoCurrenciesUseCase")
suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency)
/**
@ -64,7 +65,7 @@ interface CurrenciesRepository {
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
@Deprecated("Use SaveCryptoCurrenciesUseCase")
@Deprecated("Use ManageCryptoCurrenciesUseCase")
suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>)
/**
@ -76,6 +77,7 @@ interface CurrenciesRepository {
* @param userWalletId The unique identifier of the user wallet.
* @return A list of [CryptoCurrency].
*/
@Deprecated("Use MultiWalletCryptoCurrenciesSupplier")
fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>>
/**
@ -121,18 +123,6 @@ interface CurrenciesRepository {
id: CryptoCurrency.ID,
): CryptoCurrency
/**
* Retrieves updates of the list of cryptocurrencies within a multi-currency wallet.
*
* Loads remote cryptocurrencies if they have expired.
*
* @param userWalletId The unique identifier of the user wallet.
* @return A [Flow] emitting the set of cryptocurrencies associated with the user wallet.
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>>
/**
* Retrieves the list of cryptocurrencies within a multi-currency wallet.
*
@ -144,6 +134,7 @@ interface CurrenciesRepository {
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
@Deprecated("Use MultiWalletCryptoCurrenciesSupplier")
suspend fun getMultiCurrencyWalletCurrenciesSync(
userWalletId: UserWalletId,
refresh: Boolean = false,
@ -170,6 +161,7 @@ interface CurrenciesRepository {
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
@Deprecated("Use SingleAccountListSupplier instead")
fun isTokensGrouped(userWalletId: UserWalletId): Flow<Boolean>
/**
@ -180,6 +172,7 @@ interface CurrenciesRepository {
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
@Deprecated("Use SingleAccountListSupplier instead")
fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow<Boolean>
/**
@ -213,6 +206,7 @@ interface CurrenciesRepository {
): CryptoCurrency.Token
/** Get crypto currencies by [currencyRawId] from all user wallets */
@Deprecated("Use MultiAccountListSupplier instead")
fun getAllWalletsCryptoCurrencies(currencyRawId: CryptoCurrency.RawID): Flow<Map<UserWallet, List<CryptoCurrency>>>
fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean
@ -225,6 +219,7 @@ interface CurrenciesRepository {
* @param userWalletId The unique identifier of the user wallet to sync tokens for.
* @throws Exception if the sync request to the backend fails
*/
@Deprecated("Use AccountsCRUDRepository instead")
@Throws
suspend fun syncTokens(userWalletId: UserWalletId)

View file

@ -89,10 +89,6 @@ internal class MockCurrenciesRepository(
return token.getOrElse { e -> throw e }
}
override fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
return tokens.map { it.getOrElse { e -> throw e } }
}
override suspend fun getNetworkCoin(
userWalletId: UserWalletId,
networkId: Network.ID,

View file

@ -9,6 +9,7 @@ import com.tangem.common.routing.deeplink.DeeplinkConst.TRANSACTION_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.TYPE_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
@ -17,9 +18,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.notifications.models.NotificationType
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
@ -48,6 +47,8 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val walletBalanceFetcher: WalletBalanceFetcher,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
) : TokenDetailsDeepLinkHandler {
init {
@ -74,7 +75,7 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
return@launch
}
val cryptoCurrency = getCryptoCurrency(userWallet = userWallet, networkId = networkId, tokenId = tokenId)
val cryptoCurrency = findCryptoCurrency(userWallet = userWallet, networkId = networkId, tokenId = tokenId)
if (cryptoCurrency == null) {
Timber.e(
@ -132,25 +133,34 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
}
}
private suspend fun getCryptoCurrency(userWallet: UserWallet, networkId: String?, tokenId: String?) =
private suspend fun findCryptoCurrency(userWallet: UserWallet, networkId: String?, tokenId: String?) =
if (userWallet.isMultiCurrency) {
val derivationPath = queryParams[DERIVATION_PATH_KEY]
getCryptoCurrenciesUseCase(userWalletId = userWallet.walletId)
.getOrNull()
?.firstOrNull {
val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true)
val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true
getCryptoCurrencies(userWalletId = userWallet.walletId)?.firstOrNull {
val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true)
val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true
val isDefaultDerivation = it.network.derivationPath is Network.DerivationPath.Card
val isCustomDerivation = derivationPath?.equals(it.network.derivationPath.value) == true
val isCorrectDerivation = isDefaultDerivation || isCustomDerivation
isNetwork && isCurrency && isCorrectDerivation
}
val isDefaultDerivation = it.network.derivationPath is Network.DerivationPath.Card
val isCustomDerivation = derivationPath?.equals(it.network.derivationPath.value) == true
val isCorrectDerivation = isDefaultDerivation || isCustomDerivation
isNetwork && isCurrency && isCorrectDerivation
}
} else {
getCryptoCurrencyUseCase(userWalletId = userWallet.walletId).getOrNull()
}
private suspend fun getCryptoCurrencies(userWalletId: UserWalletId): List<CryptoCurrency>? {
return if (accountsFeatureToggles.isFeatureEnabled) {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId),
)
?.toList()
} else {
getCryptoCurrenciesUseCase(userWalletId = userWalletId).getOrNull()
}
}
@AssistedFactory
interface Factory : TokenDetailsDeepLinkHandler.Factory {
override fun create(

View file

@ -15,6 +15,7 @@ import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrency
@ -31,7 +32,6 @@ import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase
import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase
import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.impl.R
@ -125,7 +125,12 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
addVisaPresalePromoNotification(clickIntents, shouldShowVisaPromo)
addSepaPromoNotification(userWallet, clickIntents, shouldShowSepaBanner)
addSepaPromoNotification(
userWallet = userWallet,
flattenCurrencies = flattenCurrencies,
clickIntents = clickIntents,
shouldShowSepaPromo = shouldShowSepaBanner,
)
addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents)
@ -293,12 +298,20 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private suspend fun MutableList<WalletNotification>.addSepaPromoNotification(
userWallet: UserWallet,
flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>,
clickIntents: WalletClickIntents,
shouldShowSepaPromo: Boolean,
) {
val currencies = getCryptoCurrenciesUseCase(userWalletId = userWallet.walletId).getOrElse {
Timber.e("Error on getting crypto currency list")
return
val currencies = if (accountDependencies.accountsFeatureToggles.isFeatureEnabled) {
flattenCurrencies.map { statuses -> statuses.map { it.currency } }.getOrNull() ?: run {
Timber.e("Error on getting crypto currency list")
return
}
} else {
getCryptoCurrenciesUseCase(userWalletId = userWallet.walletId).getOrElse {
Timber.e("Error on getting crypto currency list")
return
}
}
val bitcoinCurrency = currencies.find { isBitcoin(it.network.rawId) } ?: return

View file

@ -1,8 +1,8 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.nft.GetNFTCollectionsUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
@ -12,6 +12,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
@Deprecated("Use WalletNFTListSubscriberV2 instead")
internal class WalletNFTListSubscriber(
private val userWallet: UserWallet,
private val stateHolder: WalletStateController,

View file

@ -35,17 +35,17 @@ internal class WalletNFTListSubscriberV2 @AssistedInject constructor(
// if NFT is enabled for this wallet and there are currencies,
// then start observing changes from store and apply transformer if need
if (nftEnabled && currencies.isNotEmpty()) {
getNFTCollectionsUseCase(userWallet.walletId)
getNFTCollectionsUseCase.invokeForAccounts(userWallet.walletId)
.shareIn(
scope = coroutineScope,
started = SharingStarted.WhileSubscribed(),
replay = 1,
)
.onEach {
.onEach { walletNFTCollections ->
stateController.update(
SetNFTCollectionsTransformer(
userWalletId = userWallet.walletId,
nftCollections = it,
nftCollections = walletNFTCollections.flattenCollections,
onItemClick = { clickIntents.onNFTClick(userWallet) },
),
)