Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-26 14:32:14 +04:00
parent dcaae36f60
commit c6578aa652
27 changed files with 111 additions and 1782 deletions

View file

@ -3,20 +3,11 @@ package com.tangem.tap.di.domain
import com.tangem.domain.managetokens.*
import com.tangem.domain.managetokens.repository.CustomTokensRepository
import com.tangem.domain.managetokens.repository.ManageTokensRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@ -57,40 +48,6 @@ internal object ManageTokensDomainModule {
return CheckIsCurrencyNotAddedUseCase(customTokensRepository)
}
@Provides
@Singleton
fun provideRemoveCustomManagedCryptoCurrencyUseCase(
customTokensRepository: CustomTokensRepository,
): RemoveCustomManagedCryptoCurrencyUseCase {
return RemoveCustomManagedCryptoCurrencyUseCase(customTokensRepository)
}
@Provides
@Singleton
fun provideSaveManagedTokensUseCase(
customTokensRepository: CustomTokensRepository,
walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository,
derivationsRepository: DerivationsRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider,
): SaveManagedTokensUseCase {
return SaveManagedTokensUseCase(
customTokensRepository = customTokensRepository,
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
derivationsRepository = derivationsRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
stakingIdFactory = stakingIdFactory,
parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default),
)
}
@Provides
@Singleton
fun provideGetSupportedNetworksUseCase(

View file

@ -4,22 +4,12 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.markets.*
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@ -66,32 +56,6 @@ object MarketsDomainModule {
return GetCurrencyQuotesUseCase(singleQuoteStatusSupplier = singleQuoteStatusSupplier)
}
@Provides
@Singleton
fun provideSaveMarketTokensUseCase(
derivationsRepository: DerivationsRepository,
marketsTokenRepository: MarketsTokenRepository,
walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider,
): SaveMarketTokensUseCase {
return SaveMarketTokensUseCase(
derivationsRepository = derivationsRepository,
marketsTokenRepository = marketsTokenRepository,
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
stakingIdFactory = stakingIdFactory,
parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default),
)
}
@Provides
@Singleton
fun provideGetTokenMarketCryptoCurrency(

View file

@ -40,28 +40,6 @@ import javax.inject.Singleton
@Suppress("TooManyFunctions", "LargeClass")
internal object TokensDomainModule {
@Provides
@Singleton
fun provideAddCryptoCurrenciesUseCase(
currenciesRepository: CurrenciesRepository,
walletManagersFacade: WalletManagersFacade,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleStakingBalanceFetcher: SingleStakingBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
stakingIdFactory: StakingIdFactory,
): AddCryptoCurrenciesUseCase {
return AddCryptoCurrenciesUseCase(
currenciesRepository = currenciesRepository,
walletManagersFacade = walletManagersFacade,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleStakingBalanceFetcher = singleStakingBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
stakingIdFactory = stakingIdFactory,
)
}
@Provides
@Singleton
fun provideFetchPendingTransactionsUseCase(
@ -88,20 +66,6 @@ internal object TokensDomainModule {
)
}
@Provides
@Singleton
fun provideRemoveCurrencyUseCase(
currenciesRepository: CurrenciesRepository,
walletManagersFacade: WalletManagersFacade,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
): RemoveCurrencyUseCase {
return RemoveCurrencyUseCase(
currenciesRepository = currenciesRepository,
walletManagersFacade = walletManagersFacade,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
)
}
@Provides
@Singleton
fun provideGetCurrencyUseCase(
@ -177,20 +141,6 @@ internal object TokensDomainModule {
return GetCryptoCurrencyUseCase(currenciesRepository, multiWalletCryptoCurrenciesSupplier)
}
@Provides
@Singleton
fun provideApplyTokenListSortingUseCase(
currenciesRepository: CurrenciesRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
dispatchers: CoroutineDispatcherProvider,
): ApplyTokenListSortingUseCase {
return ApplyTokenListSortingUseCase(
currenciesRepository = currenciesRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun provideGetCryptoCurrencyActionsUseCase(

View file

@ -6,8 +6,6 @@ import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.currency.UserTokensResponseFactory
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.data.common.network.NetworkFactory
import com.tangem.data.managetokens.utils.TokenAddressesConverter
import com.tangem.datasource.api.common.response.getOrThrow
@ -26,7 +24,6 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
@ -35,10 +32,8 @@ internal class DefaultCustomTokensRepository(
private val tangemTechApi: TangemTechApi,
private val userWalletsListRepository: UserWalletsListRepository,
private val userTokensResponseStore: UserTokensResponseStore,
private val walletManagersFacade: WalletManagersFacade,
private val excludedBlockchains: ExcludedBlockchains,
private val dispatchers: CoroutineDispatcherProvider,
private val userTokensSaver: UserTokensSaver,
private val networkFactory: NetworkFactory,
) : CustomTokensRepository {
@ -48,7 +43,6 @@ internal class DefaultCustomTokensRepository(
)
private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains)
private val userTokensResponseFactory = UserTokensResponseFactory()
private val tokenAddressConverter = TokenAddressesConverter()
override suspend fun validateContractAddress(contractAddress: String, networkId: Network.ID): Boolean =
@ -213,41 +207,6 @@ internal class DefaultCustomTokensRepository(
)
}
@Deprecated("Use ManageCryptoCurrenciesUseCase")
override suspend fun removeCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) =
withContext(dispatchers.io) {
val cryptoCurrency = when (currency) {
is ManagedCryptoCurrency.Custom.Coin -> createCoin(
userWalletId = userWalletId,
networkId = currency.network.id,
derivationPath = currency.network.derivationPath,
)
is ManagedCryptoCurrency.Custom.Token -> cryptoCurrencyFactory.createToken(
network = currency.network,
rawId = currency.currencyId.rawCurrencyId,
name = currency.name,
symbol = currency.symbol,
decimals = currency.decimals,
contractAddress = currency.contractAddress,
)
}
val storedCurrencies = userTokensResponseStore.getSyncOrNull(userWalletId)
requireNotNull(storedCurrencies) {
"User tokens not found for user wallet [$userWalletId] while removing currency"
}
val token = userTokensResponseFactory.createResponseToken(currency = cryptoCurrency, accountId = null)
userTokensSaver.storeAndPush(
userWalletId = userWalletId,
response = storedCurrencies.copy(tokens = storedCurrencies.tokens.filterNot { it == token }),
)
when (cryptoCurrency) {
is CryptoCurrency.Coin -> walletManagersFacade.remove(userWalletId, setOf(cryptoCurrency.network))
is CryptoCurrency.Token -> walletManagersFacade.removeTokens(userWalletId, setOf(cryptoCurrency))
}
}
override suspend fun convertToCryptoCurrency(
userWalletId: UserWalletId,
currency: ManagedCryptoCurrency.Custom,

View file

@ -15,7 +15,6 @@ import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.managetokens.repository.CustomTokensRepository
import com.tangem.domain.managetokens.repository.ManageTokensRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -65,20 +64,16 @@ internal object ManageTokensDataModule {
tangemTechApi: TangemTechApi,
userWalletsListRepository: UserWalletsListRepository,
userTokensResponseStore: UserTokensResponseStore,
walletManagersFacade: WalletManagersFacade,
dispatchers: CoroutineDispatcherProvider,
excludedBlockchains: ExcludedBlockchains,
userTokensSaver: UserTokensSaver,
networkFactory: NetworkFactory,
): CustomTokensRepository {
return DefaultCustomTokensRepository(
tangemTechApi = tangemTechApi,
userWalletsListRepository = userWalletsListRepository,
userTokensResponseStore = userTokensResponseStore,
walletManagersFacade = walletManagersFacade,
excludedBlockchains = excludedBlockchains,
dispatchers = dispatchers,
userTokensSaver = userTokensSaver,
networkFactory = networkFactory,
)
}

View file

@ -34,7 +34,6 @@ import com.tangem.domain.tokens.model.FeePaidCurrency
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runSuspendCatching
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import timber.log.Timber
@ -66,134 +65,6 @@ internal class DefaultCurrenciesRepository(
userTokensSaver = userTokensSaver,
)
override suspend fun saveTokens(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
isGroupedByNetwork: Boolean,
isSortedByBalance: Boolean,
) = withContext(dispatchers.io) {
ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true)
val response = userTokensResponseFactory.createUserTokensResponse(
currencies = currencies,
isGroupedByNetwork = isGroupedByNetwork,
isSortedByBalance = isSortedByBalance,
)
userTokensSaver.storeAndPush(userWalletId, response)
}
override suspend fun addCurrenciesCache(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
): List<CryptoCurrency> = withContext(dispatchers.io) {
val savedCurrencies = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform add currencies action" },
)
val currenciesToAdd = filterAlreadyAddedCurrencies(
savedCurrencies = savedCurrencies.tokens,
currenciesToAdd = populateCurrenciesWithMissedCoins(currencies = currencies),
)
val updatedResponse = savedCurrencies.copy(
tokens = savedCurrencies.tokens + currenciesToAdd.map(userTokensResponseFactory::createResponseToken),
)
userTokensSaver.store(
userWalletId = userWalletId,
response = updatedResponse,
)
fetchExpressAssetsByNetworkIds(
userWallet = userWalletsListRepository.getSyncStrict(id = userWalletId),
userTokens = updatedResponse,
)
currenciesToAdd
}
private fun filterAlreadyAddedCurrencies(
savedCurrencies: List<UserTokensResponse.Token>,
currenciesToAdd: List<CryptoCurrency>,
): List<CryptoCurrency> {
return currenciesToAdd.filter { currency ->
val networkId = currency.network.toBlockchain().toNetworkId()
val contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress
savedCurrencies.none { token ->
token.contractAddress == contractAddress &&
token.networkId == networkId &&
token.derivationPath == currency.network.derivationPath.value
}
}
}
private fun populateCurrenciesWithMissedCoins(currencies: List<CryptoCurrency>): List<CryptoCurrency> {
val currenciesSequence = currencies.asSequence()
val networksWithTokens = currenciesSequence
.filterIsInstance<CryptoCurrency.Token>()
.map { it.network }
.distinct()
val networksWithCoins = currenciesSequence
.filterIsInstance<CryptoCurrency.Coin>()
.map { it.network }
.distinct()
val networksNeedingCoins = (networksWithTokens - networksWithCoins.toSet()).toMutableList()
if (networksNeedingCoins.isEmpty()) return currencies
return buildList {
currencies.forEach { currency ->
if (currency is CryptoCurrency.Token && currency.network in networksNeedingCoins) {
val coin = cryptoCurrencyFactory.createCoin(currency.network)
add(coin)
networksNeedingCoins.remove(currency.network)
}
add(currency)
}
}
}
override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) =
withContext(dispatchers.io) {
val savedCurrencies = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform remove currency action" },
)
val token = userTokensResponseFactory.createResponseToken(currency)
val updatedResponse =
savedCurrencies.copy(tokens = savedCurrencies.tokens.filterNot { it == token })
userTokensSaver.storeAndPush(
userWalletId = userWalletId,
response = updatedResponse,
)
}
override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
return withContext(dispatchers.io) {
val savedCurrencies = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform remove currencies action" },
)
val tokens = currencies.map(userTokensResponseFactory::createResponseToken)
val updatedResponse = savedCurrencies.copy(
tokens = savedCurrencies.tokens.filterNot(tokens::contains),
)
userTokensSaver.storeAndPush(
userWalletId = userWalletId,
response = updatedResponse,
)
}
}
override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
return channelFlow {
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
@ -577,19 +448,6 @@ internal class DefaultCurrenciesRepository(
return blockchain?.isNetworkFeeZero() == true
}
override suspend fun syncTokens(userWalletId: UserWalletId) {
runSuspendCatching {
val savedCurrencies = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform add currencies action" },
)
userTokensSaver.storeAndPush(
userWalletId = userWalletId,
response = savedCurrencies,
)
}
}
override fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver? {
return (userWalletsListRepository.getSyncStrict(userWalletId) as? UserWallet.Cold)?.cardTypesResolver
}
@ -712,12 +570,6 @@ internal class DefaultCurrenciesRepository(
accountId = null,
)
private fun ensureIsCorrectUserWallet(userWalletId: UserWalletId, isMultiCurrencyWalletExpected: Boolean) {
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected)
}
private fun ensureIsCorrectUserWallet(userWallet: UserWallet, isMultiCurrencyWalletExpected: Boolean) {
val userWalletId = userWallet.walletId

View file

@ -1,19 +0,0 @@
package com.tangem.domain.managetokens
import arrow.core.Either
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.managetokens.repository.CustomTokensRepository
import com.tangem.domain.models.wallet.UserWalletId
@Deprecated("Use ManageCryptoCurrenciesUseCase")
class RemoveCustomManagedCryptoCurrencyUseCase(private val repository: CustomTokensRepository) {
suspend operator fun invoke(
userWalletId: UserWalletId,
customCurrency: ManagedCryptoCurrency.Custom,
): Either<Throwable, Unit> {
return Either.catch {
repository.removeCurrency(userWalletId, customCurrency)
}
}
}

View file

@ -1,174 +0,0 @@
package com.tangem.domain.managetokens
import arrow.core.Either
import arrow.core.flatten
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.managetokens.repository.CustomTokensRepository
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.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Deprecated("Use ManageCryptoCurrenciesUseCase")
@Suppress("LongParameterList")
class SaveManagedTokensUseCase(
private val customTokensRepository: CustomTokensRepository,
private val walletManagersFacade: WalletManagersFacade,
private val currenciesRepository: CurrenciesRepository,
private val derivationsRepository: DerivationsRepository,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
private val stakingIdFactory: StakingIdFactory,
private val parallelUpdatingScope: CoroutineScope,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
currenciesToAdd: Map<ManagedCryptoCurrency.Token, Set<Network>>,
currenciesToRemove: Map<ManagedCryptoCurrency.Token, Set<Network>>,
): Either<Throwable, Unit> = Either.catch {
if (currenciesToRemove.isNotEmpty()) {
val removingCurrencies = currenciesToRemove.mapToCryptoCurrencies(userWalletId)
currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = removingCurrencies)
removeCurrenciesFromWalletManager(userWalletId = userWalletId, currencies = removingCurrencies)
}
if (currenciesToAdd.isNotEmpty()) {
derivationsRepository.derivePublicKeysByNetworks(
userWalletId = userWalletId,
networks = currenciesToAdd.values.flatten(),
)
val addingCurrencies = currenciesToAdd.mapToCryptoCurrencies(userWalletId)
val savedCurrencies = currenciesRepository.addCurrenciesCache(
userWalletId = userWalletId,
currencies = addingCurrencies,
)
parallelUpdatingScope.launch {
withContext(NonCancellable) {
syncTokens(userWalletId = userWalletId, addedCurrencies = savedCurrencies)
launch {
refreshUpdatedNetworks(
userWalletId = userWalletId,
addedCurrencies = savedCurrencies,
)
}
launch {
refreshUpdatedStakingBalances(
userWalletId = userWalletId,
addedCurrencies = savedCurrencies,
)
}
launch { refreshUpdatedQuotes(addedCurrencies = savedCurrencies) }
}
}
}
}
private suspend fun removeCurrenciesFromWalletManager(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
) {
walletManagersFacade.remove(
userWalletId = userWalletId,
networks = currencies
.filterIsInstance<CryptoCurrency.Coin>()
.mapTo(hashSetOf(), CryptoCurrency::network),
)
walletManagersFacade.removeTokens(
userWalletId = userWalletId,
tokens = currencies.filterIsInstance<CryptoCurrency.Token>().toSet(),
)
}
private suspend fun syncTokens(userWalletId: UserWalletId, addedCurrencies: List<CryptoCurrency>) {
createWalletManagers(userWalletId = userWalletId, currencies = addedCurrencies)
currenciesRepository.syncTokens(userWalletId)
}
/**
* Creates wallet managers for the given [currencies] if they do not already exist.
* The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature.
*
* @param userWalletId The ID of the user's wallet.
* @param currencies The list of cryptocurrencies for which to create wallet managers.
*/
private suspend fun createWalletManagers(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
val networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network)
for (network in networks) {
walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network)
}
}
private suspend fun refreshUpdatedNetworks(userWalletId: UserWalletId, addedCurrencies: List<CryptoCurrency>) {
multiNetworkStatusFetcher(
MultiNetworkStatusFetcher.Params(
userWalletId = userWalletId,
networks = addedCurrencies.map(CryptoCurrency::network).toSet(),
),
)
}
private suspend fun refreshUpdatedStakingBalances(
userWalletId: UserWalletId,
addedCurrencies: List<CryptoCurrency>,
) {
val stakingIds = addedCurrencies.mapNotNullTo(hashSetOf()) {
stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull()
}
multiStakingBalanceFetcher(
params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds),
)
}
private suspend fun refreshUpdatedQuotes(addedCurrencies: List<CryptoCurrency>) {
multiQuoteStatusFetcher(
params = MultiQuoteStatusFetcher.Params(
currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId },
appCurrencyId = null,
),
)
}
private suspend fun Map<ManagedCryptoCurrency.Token, Set<Network>>.mapToCryptoCurrencies(
userWalletId: UserWalletId,
): List<CryptoCurrency> {
return flatMap { (token, networks) ->
token.availableNetworks
.filter { sourceNetwork -> networks.contains(sourceNetwork.network) }
.map { sourceNetwork ->
when (sourceNetwork) {
is ManagedCryptoCurrency.SourceNetwork.Default -> customTokensRepository.createToken(
managedCryptoCurrency = token,
sourceNetwork = sourceNetwork,
rawId = CryptoCurrency.RawID(token.id.value),
)
is ManagedCryptoCurrency.SourceNetwork.Main -> customTokensRepository.createCoin(
userWalletId = userWalletId,
networkId = sourceNetwork.id,
derivationPath = sourceNetwork.network.derivationPath,
)
}
}
}
}
}

View file

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

View file

@ -1,145 +0,0 @@
package com.tangem.domain.markets
import arrow.core.Either
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.models.account.DerivationIndex
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.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Use case for saving tokens from Markets
*
* @property derivationsRepository derivations repository
* @property marketsTokenRepository markets token repository
* @property currenciesRepository currencies repository
*
[REDACTED_AUTHOR]
*/
@Deprecated("Use ManageCryptoCurrenciesUseCase")
@Suppress("LongParameterList")
class SaveMarketTokensUseCase(
private val derivationsRepository: DerivationsRepository,
private val marketsTokenRepository: MarketsTokenRepository,
private val walletManagersFacade: WalletManagersFacade,
private val currenciesRepository: CurrenciesRepository,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
private val stakingIdFactory: StakingIdFactory,
private val parallelUpdatingScope: CoroutineScope,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
tokenMarketParams: TokenMarketParams,
addedNetworks: Set<TokenMarketInfo.Network>,
removedNetworks: Set<TokenMarketInfo.Network>,
): Either<Throwable, Unit> = Either.catch {
if (removedNetworks.isNotEmpty()) {
val removedCurrencies = removedNetworks.mapNotNull { network ->
marketsTokenRepository.createCryptoCurrency(
userWalletId = userWalletId,
token = tokenMarketParams,
network = network,
)
}
currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = removedCurrencies)
}
if (addedNetworks.isNotEmpty()) {
derivationsRepository.derivePublicKeysByNetworkIds(
userWalletId = userWalletId,
networkIds = addedNetworks.map { Network.RawID(it.networkId) },
accountIndex = DerivationIndex.Main,
)
val addedCurrencies = addedNetworks.mapNotNull { network ->
marketsTokenRepository.createCryptoCurrency(
userWalletId = userWalletId,
token = tokenMarketParams,
network = network,
accountIndex = DerivationIndex.Main,
)
}
val savedCurrencies = currenciesRepository.addCurrenciesCache(
userWalletId = userWalletId,
currencies = addedCurrencies,
)
parallelUpdatingScope.launch {
withContext(NonCancellable) {
syncTokens(userWalletId, savedCurrencies)
launch { refreshUpdatedNetworks(userWalletId, savedCurrencies) }
launch { refreshUpdatedStakingBalances(userWalletId, savedCurrencies) }
launch { refreshUpdatedQuotes(savedCurrencies) }
}
}
}
}
private suspend fun syncTokens(userWalletId: UserWalletId, addedCurrencies: List<CryptoCurrency>) {
createWalletManagers(userWalletId = userWalletId, currencies = addedCurrencies)
currenciesRepository.syncTokens(userWalletId)
}
/**
* Creates wallet managers for the given [currencies] if they do not already exist.
* The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature.
*
* @param userWalletId The ID of the user's wallet.
* @param currencies The list of cryptocurrencies for which to create wallet managers.
*/
private suspend fun createWalletManagers(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
val networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network)
for (network in networks) {
walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network)
}
}
private suspend fun refreshUpdatedNetworks(userWalletId: UserWalletId, addedCurrencies: List<CryptoCurrency>) {
multiNetworkStatusFetcher(
MultiNetworkStatusFetcher.Params(
userWalletId = userWalletId,
networks = addedCurrencies.map(CryptoCurrency::network).toSet(),
),
)
}
private suspend fun refreshUpdatedStakingBalances(
userWalletId: UserWalletId,
existingCurrencies: List<CryptoCurrency>,
) {
val stakingIds = existingCurrencies.mapNotNullTo(hashSetOf()) {
stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull()
}
multiStakingBalanceFetcher(
params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds),
)
}
private suspend fun refreshUpdatedQuotes(addedCurrencies: List<CryptoCurrency>) {
multiQuoteStatusFetcher(
params = MultiQuoteStatusFetcher.Params(
currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId },
appCurrencyId = null,
),
)
}
}

View file

@ -1,251 +0,0 @@
package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.raise.Raise
import arrow.core.raise.catch
import arrow.core.raise.either
import arrow.core.right
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.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.single.SingleStakingBalanceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
/**
* A use case for adding multiple cryptocurrencies to a user's wallet.
*
* 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 ManageCryptoCurrenciesUseCase")
@Suppress("LongParameterList")
class AddCryptoCurrenciesUseCase(
private val currenciesRepository: CurrenciesRepository,
private val walletManagersFacade: WalletManagersFacade,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
private val singleStakingBalanceFetcher: SingleStakingBalanceFetcher,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val stakingIdFactory: StakingIdFactory,
) {
/**
* Adds a [cryptoCurrency] token with specific [network] and derivation to the wallet identified by [userWalletId].
*
* After successfully adding a currency, it also refreshes the networks for tokens
* that are being added and have corresponding coins in the existing currencies list.
*
* @param userWalletId The ID of the user's wallet.
* @param cryptoCurrency Token to add.
* @param network Network where we add
* @return Either an [Throwable] or [Unit] indicating the success of the operation.
*/
suspend operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency.Token,
network: Network,
): Either<Throwable, Unit> = either {
val tokenToAdd = currenciesRepository.createTokenCurrency(cryptoCurrency = cryptoCurrency, network = network)
invoke(userWalletId = userWalletId, currency = tokenToAdd)
}
/**
* Adds a [currency] to the wallet identified by [userWalletId].
*
* After successfully adding a currency, it also refreshes the networks for tokens
* that are being added and have corresponding coins in the existing currencies list.
*
* @param userWalletId The ID of the user's wallet.
* @param currency Cryptocurrency to add.
* @return Either an [Throwable] or [Unit] indicating the success of the operation.
*/
suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either<Throwable, Unit> =
either {
val existingCurrencies = catch(
block = { currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) },
catch = ::raise,
)
val currencyToAdd = currency.takeUnless(existingCurrencies::contains) ?: return@either
val addedCurrencies = addCurrencies(userWalletId, currencyToAdd)
coroutineScope {
syncTokens(userWalletId, addedCurrencies)
awaitAll(
async { refreshUpdatedNetworks(userWalletId, currencyToAdd, existingCurrencies) },
async { refreshUpdatedStakingBalances(userWalletId, currencyToAdd) },
async { refreshUpdatedQuotes(currencyToAdd) },
)
}
}
suspend operator fun invoke(
userWalletId: UserWalletId,
contractAddress: String,
networkId: String,
): Either<Throwable, CryptoCurrency> = either {
val existingCurrencies = catch(
block = {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
.toList()
},
catch = ::raise,
)
val foundToken = existingCurrencies
.filterIsInstance<CryptoCurrency.Token>()
.firstOrNull { token ->
token.network.backendId == networkId &&
!token.isCustom &&
token.contractAddress.equals(contractAddress, true)
}
if (foundToken != null) {
return@either foundToken
}
val tokenToAdd = createTokenCurrency(userWalletId, contractAddress, networkId)
val addedCurrencies = addCurrencies(userWalletId, tokenToAdd)
coroutineScope {
syncTokens(userWalletId = userWalletId, addedCurrencies = addedCurrencies)
awaitAll(
async { refreshUpdatedNetworks(userWalletId, tokenToAdd, existingCurrencies) },
async { refreshUpdatedStakingBalances(userWalletId, tokenToAdd) },
async { refreshUpdatedQuotes(tokenToAdd) },
)
}
tokenToAdd
}
private suspend fun syncTokens(userWalletId: UserWalletId, addedCurrencies: List<CryptoCurrency>) {
createWalletManagers(userWalletId = userWalletId, currencies = addedCurrencies)
currenciesRepository.syncTokens(userWalletId)
}
/**
* Creates wallet managers for the given [currencies] if they do not already exist.
* The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature.
*
* @param userWalletId The ID of the user's wallet.
* @param currencies The list of cryptocurrencies for which to create wallet managers.
*/
private suspend fun createWalletManagers(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
val networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network)
for (network in networks) {
walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network)
}
}
/**
* Refreshes the network statuses for tokens that have corresponding coins in the
* [existingCurrencies] list.
*/
private suspend fun refreshUpdatedNetworks(
userWalletId: UserWalletId,
currencyToAdd: CryptoCurrency,
existingCurrencies: List<CryptoCurrency>,
) {
val networksToUpdate = currencyToAdd.takeIf { currency ->
currency is CryptoCurrency.Token && hasCoinForToken(existingCurrencies, currency)
}
?.network
val networkToUpdate = currencyToAdd.takeIf {
!existingCurrencies.map(CryptoCurrency::network).contains(it.network)
}
?.network
multiNetworkStatusFetcher(
MultiNetworkStatusFetcher.Params(
userWalletId = userWalletId,
networks = setOfNotNull(networksToUpdate, networkToUpdate),
),
)
}
private suspend fun refreshUpdatedStakingBalances(
userWalletId: UserWalletId,
addedCurrency: CryptoCurrency,
): Either<Throwable, Unit> = either {
val stakingId = stakingIdFactory.create(
userWalletId = userWalletId,
currencyId = addedCurrency.id,
network = addedCurrency.network,
)
.getOrElse { error ->
when (error) {
is StakingIdFactory.Error.UnableToGetAddress -> raise(IllegalStateException("$error"))
StakingIdFactory.Error.UnsupportedCurrency -> Unit.right()
}
return@either
}
singleStakingBalanceFetcher(
params = SingleStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId),
)
.bind()
}
private suspend fun refreshUpdatedQuotes(currencyToAdd: CryptoCurrency) {
multiQuoteStatusFetcher(
params = MultiQuoteStatusFetcher.Params(
currenciesIds = setOfNotNull(currencyToAdd.id.rawCurrencyId),
appCurrencyId = null,
),
)
}
private suspend fun Raise<Throwable>.createTokenCurrency(
userWalletId: UserWalletId,
contractAddress: String,
networkId: String,
): CryptoCurrency.Token {
return catch(
block = {
currenciesRepository.createTokenCurrency(
userWalletId = userWalletId,
contractAddress = contractAddress,
networkId = networkId,
)
},
catch = {
raise(it)
},
)
}
private suspend fun Raise<Throwable>.addCurrencies(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): List<CryptoCurrency> {
return catch(
block = { currenciesRepository.addCurrenciesCache(userWalletId, listOf(currency)) },
catch = ::raise,
)
}
/**
* Determines if the [existingCurrencies] list contains a coin that corresponds
* to the given [token].
*/
private fun hasCoinForToken(existingCurrencies: List<CryptoCurrency>, token: CryptoCurrency.Token): Boolean {
return existingCurrencies.any { currency ->
currency is CryptoCurrency.Coin && currency.network == token.network
}
}
}

View file

@ -1,115 +0,0 @@
package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.raise.Raise
import arrow.core.raise.catch
import arrow.core.raise.either
import arrow.core.raise.ensureNotNull
import arrow.core.toNonEmptyListOrNull
import arrow.core.toNonEmptySetOrNull
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.error.TokenListSortingError
import com.tangem.domain.tokens.repository.CurrenciesRepository
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,
private val dispatchers: CoroutineDispatcherProvider,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
sortedTokensIds: List<CryptoCurrency.ID>,
isGroupedByNetwork: Boolean,
isSortedByBalance: Boolean,
): Either<TokenListSortingError, Unit> {
return either {
val storedCurrencies = getCurrencies(userWalletId)
val isSortingTypeChanged = checkIsCurrenciesSortedByBalance(userWalletId) != isSortedByBalance
val isGroupingTypeChanged = checkIsCurrenciesGroupedByNetwork(userWalletId) != isGroupedByNetwork
val sortedCurrencies = sortTokens(sortedTokensIds, storedCurrencies)
if (storedCurrencies != sortedCurrencies || isSortingTypeChanged || isGroupingTypeChanged) {
applySorting(
userWalletId = userWalletId,
currencies = sortedCurrencies,
isGrouped = isGroupedByNetwork,
isSortedByBalance = isSortedByBalance,
)
}
}
}
private suspend fun Raise<TokenListSortingError>.checkIsCurrenciesSortedByBalance(userWalletId: UserWalletId) =
catch(
block = { currenciesRepository.isTokensSortedByBalance(userWalletId).firstOrNull() == true },
catch = { raise(TokenListSortingError.DataError(it)) },
)
private suspend fun Raise<TokenListSortingError>.checkIsCurrenciesGroupedByNetwork(userWalletId: UserWalletId) =
catch(
block = { currenciesRepository.isTokensGrouped(userWalletId).firstOrNull() == true },
catch = { raise(TokenListSortingError.DataError(it)) },
)
private suspend fun Raise<TokenListSortingError>.sortTokens(
sortedCurrenciesIds: List<CryptoCurrency.ID>,
unsortedCurrencies: List<CryptoCurrency>,
): List<CryptoCurrency> = withContext(dispatchers.default) {
val nonEmptySortedTokensIds = ensureNotNull(sortedCurrenciesIds.toNonEmptySetOrNull()) {
TokenListSortingError.TokenListIsEmpty
}
val sortedTokens = sortedMapOf<Int, CryptoCurrency>()
unsortedCurrencies.distinct().forEach { currency ->
val index = nonEmptySortedTokensIds.indexOfFirst { currencyId ->
currencyId == currency.id
}
if (index >= 0) {
sortedTokens[index] = currency
} else {
raise(TokenListSortingError.UnableToSortTokenList)
}
}
ensureNotNull(sortedTokens.values.toNonEmptyListOrNull()) {
TokenListSortingError.TokenListIsEmpty
}
}
private suspend fun Raise<TokenListSortingError>.getCurrencies(userWalletId: UserWalletId): List<CryptoCurrency> {
val tokens = catch(
block = {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
},
catch = { raise(TokenListSortingError.DataError(it)) },
)
return ensureNotNull(tokens.toNonEmptyListOrNull()) {
TokenListSortingError.TokenListIsEmpty
}
}
private suspend fun Raise<TokenListSortingError>.applySorting(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
isGrouped: Boolean,
isSortedByBalance: Boolean,
) = withContext(dispatchers.io) {
catch(
block = { currenciesRepository.saveTokens(userWalletId, currencies, isGrouped, isSortedByBalance) },
catch = { raise(TokenListSortingError.DataError(it)) },
)
}
}

View file

@ -1,60 +0,0 @@
package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.model.remove.RemoveCurrencyError
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
@Deprecated("Use ManageCryptoCurrenciesUseCase")
class RemoveCurrencyUseCase(
private val currenciesRepository: CurrenciesRepository,
private val walletManagersFacade: WalletManagersFacade,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
) {
@Deprecated("Use ManageCryptoCurrenciesUseCase")
suspend operator fun invoke(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): Either<RemoveCurrencyError, Unit> {
return either {
if (hasLinkedTokens(userWalletId, currency)) {
raise(RemoveCurrencyError.HasLinkedTokens)
}
catch(
block = {
currenciesRepository.removeCurrency(userWalletId, currency)
when (currency) {
is CryptoCurrency.Coin -> {
walletManagersFacade.remove(userWalletId, setOf(currency.network))
}
is CryptoCurrency.Token -> {
walletManagersFacade.removeTokens(userWalletId, setOf(currency))
}
}
},
catch = { raise(RemoveCurrencyError.DataError(it)) },
)
}
}
suspend fun hasLinkedTokens(userWalletId: UserWalletId, currency: CryptoCurrency): Boolean {
return when (currency) {
is CryptoCurrency.Coin -> {
val walletCurrencies = multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
walletCurrencies.any { it is CryptoCurrency.Token && it.network == currency.network }
}
is CryptoCurrency.Token -> false
}
}
}

View file

@ -16,58 +16,6 @@ import kotlinx.coroutines.flow.Flow
@Suppress("TooManyFunctions")
interface CurrenciesRepository {
/**
* Saves the given list of cryptocurrencies, along with the preferences for grouping and sorting, for a specific
* multi-currency user wallet.
*
* @param userWalletId The unique identifier of the user wallet.
* @param currencies The list of cryptocurrencies to be saved.
* @param isGroupedByNetwork A boolean flag indicating whether the tokens should be grouped by network.
* @param isSortedByBalance A boolean flag indicating whether the tokens should be sorted by balance.
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
@Deprecated("Use ManageCryptoCurrenciesUseCase")
suspend fun saveTokens(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
isGroupedByNetwork: Boolean,
isSortedByBalance: Boolean,
)
/**
* Add currencies to a specific user wallet.
*
* @param userWalletId The unique identifier of the user wallet.
* @param currencies The currencies which must be added.
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
@Deprecated("Use ManageCryptoCurrenciesUseCase")
suspend fun addCurrenciesCache(userWalletId: UserWalletId, currencies: List<CryptoCurrency>): List<CryptoCurrency>
/**
* Removes currency from a specific user wallet.
*
* @param userWalletId The unique identifier of the user wallet.
* @param currency The currency which must be removed.
* @throws DataError.UserWalletError.WrongUserWallet If multi-currency user wallet
* ID provided.
*/
@Deprecated("Use ManageCryptoCurrenciesUseCase")
suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency)
/**
* Removes currencies from a specific user wallet.
*
* @param userWalletId The unique identifier of the user wallet.
* @param currencies The currencies which must be removed.
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
@Deprecated("Use ManageCryptoCurrenciesUseCase")
suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>)
/**
* Retrieves the list of cryptocurrencies within a user wallet.
*
@ -211,18 +159,6 @@ interface CurrenciesRepository {
fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean
/**
* Synchronizes local tokens with remote data for a specific user wallet.
* This method ensures that the local token list matches the remote state by fetching
* the token data from the local cache and push it to backend.
*
* @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)
@Throws
fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver?
}

View file

@ -1,207 +0,0 @@
package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.core.error.DataError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.error.TokenListSortingError
import com.tangem.domain.tokens.mock.MockTokens
import com.tangem.domain.tokens.repository.MockCurrenciesRepository
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Test
import kotlin.random.Random
internal class ApplyTokenListSortingUseCaseTest {
private val userWalletId = UserWalletId(value = null)
@Test
fun `when tokens are empty then error should be received`() = runTest {
// Given
val expectedResult = TokenListSortingError.TokenListIsEmpty.left()
val useCase = getUseCase()
// When
val result = useCase(
userWalletId = userWalletId,
sortedTokensIds = emptyList(),
isGroupedByNetwork = false,
isSortedByBalance = false,
)
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when tokens saving failed then error should be received`() = runTest {
// Given
val expectedResult = TokenListSortingError.DataError(DataError.NetworkError.NoInternetConnection).left()
val repository = getTokensRepository(
sortTokensResult = DataError.NetworkError.NoInternetConnection.left(),
)
val useCase = getUseCase(repository)
// When
val result = useCase(
userWalletId = userWalletId,
sortedTokensIds = MockTokens.tokens.map { it.id }.sortedByDescending { it.value },
isGroupedByNetwork = false,
isSortedByBalance = false,
)
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when apply sorting for sorted and grouped list then correct args should be used`() = runTest {
// Given
val expectedTokens = getSortedTokens()
val expectedIsGrouped = true
val expectedIsSorted = true
val repository = getTokensRepository()
val useCase = getUseCase(repository)
// When
useCase(
userWalletId = userWalletId,
sortedTokensIds = expectedTokens.map { it.id },
isGroupedByNetwork = expectedIsGrouped,
isSortedByBalance = expectedIsSorted,
)
// Then
assertEquals(expectedTokens, repository.tokensIdsAfterSortingApply)
assertEquals(expectedIsGrouped, repository.isTokensGroupedAfterSortingApply)
assertEquals(expectedIsSorted, repository.isTokensSortedByBalanceAfterSortingApply)
}
@Test
fun `when apply sorting for unsorted and grouped list then correct args should be used`() = runTest {
// Given
val expectedTokens = getSortedTokens()
val expectedIsGrouped = true
val expectedIsSorted = false
val repository = getTokensRepository()
val useCase = getUseCase(repository)
// When
useCase(
userWalletId = userWalletId,
sortedTokensIds = expectedTokens.map { it.id },
isGroupedByNetwork = expectedIsGrouped,
isSortedByBalance = expectedIsSorted,
)
// Then
assertEquals(expectedTokens, repository.tokensIdsAfterSortingApply)
assertEquals(expectedIsGrouped, repository.isTokensGroupedAfterSortingApply)
assertEquals(expectedIsSorted, repository.isTokensSortedByBalanceAfterSortingApply)
}
@Test
fun `when apply sorting for sorted and ungrouped list then correct args should be used`() = runTest {
// Given
val expectedTokens = getSortedTokens()
val expectedIsGrouped = false
val expectedIsSorted = true
val repository = getTokensRepository()
val useCase = getUseCase(repository)
// When
useCase(
userWalletId = userWalletId,
sortedTokensIds = expectedTokens.map { it.id },
isGroupedByNetwork = expectedIsGrouped,
isSortedByBalance = expectedIsSorted,
)
// Then
assertEquals(expectedTokens, repository.tokensIdsAfterSortingApply)
assertEquals(expectedIsGrouped, repository.isTokensGroupedAfterSortingApply)
assertEquals(expectedIsSorted, repository.isTokensSortedByBalanceAfterSortingApply)
}
@Test
fun `when apply sorting for unsorted and ungrouped list then correct args should be used`() = runTest {
// Given
val expectedTokens = getSortedTokens()
val expectedIsGrouped = false
val expectedIsSorted = false
val repository = getTokensRepository()
val useCase = getUseCase(repository)
// When
useCase(
userWalletId = userWalletId,
sortedTokensIds = expectedTokens.map { it.id },
isGroupedByNetwork = expectedIsGrouped,
isSortedByBalance = expectedIsSorted,
)
// Then
assertEquals(expectedTokens, repository.tokensIdsAfterSortingApply)
assertEquals(expectedIsGrouped, repository.isTokensGroupedAfterSortingApply)
assertEquals(expectedIsSorted, repository.isTokensSortedByBalanceAfterSortingApply)
}
@Test
fun `when sorted tokens IDs do not contain all tokens IDs then error should be received`() = runTest {
// Given
val expectedResult = TokenListSortingError.UnableToSortTokenList.left()
val repository = getTokensRepository()
val useCase = getUseCase(repository)
// When
val result = useCase(
userWalletId = userWalletId,
sortedTokensIds = getSortedTokens().drop(n = 3).map { it.id },
isGroupedByNetwork = false,
isSortedByBalance = false,
)
// Then
assertEquals(expectedResult, result)
}
private fun getSortedTokens() = MockTokens.tokens
.sortedBy { Random.nextInt(0, MockTokens.tokens.size) }
private fun getUseCase(tokensRepository: MockCurrenciesRepository = getTokensRepository()) =
ApplyTokenListSortingUseCase(
currenciesRepository = tokensRepository,
dispatchers = TestingCoroutineDispatcherProvider(),
multiWalletCryptoCurrenciesSupplier = mockk(),
)
private fun getTokensRepository(
sortTokensResult: Either<DataError, Unit> = Unit.right(),
removeCurrencyResult: Either<DataError, Unit> = Unit.right(),
tokens: Flow<Either<DataError, List<CryptoCurrency>>> = flowOf(MockTokens.tokens.right()),
): MockCurrenciesRepository {
return MockCurrenciesRepository(
sortTokensResult = sortTokensResult,
removeCurrencyResult = removeCurrencyResult,
token = MockTokens.token1.right(),
tokens = tokens,
isGrouped = emptyFlow(),
isSortedByBalance = emptyFlow(),
)
}
}

View file

@ -1,152 +0,0 @@
package com.tangem.domain.tokens.repository
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.domain.card.CardTypesResolver
import com.tangem.domain.core.error.DataError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.model.FeePaidCurrency
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
internal class MockCurrenciesRepository(
private val sortTokensResult: Either<DataError, Unit>,
private val removeCurrencyResult: Either<DataError, Unit>,
private val token: Either<DataError, CryptoCurrency>,
private val tokens: Flow<Either<DataError, List<CryptoCurrency>>>,
private val isGrouped: Flow<Either<DataError, Boolean>>,
private val isSortedByBalance: Flow<Either<DataError, Boolean>>,
) : CurrenciesRepository {
var tokensIdsAfterSortingApply: List<CryptoCurrency>? = null
private set
var isTokensGroupedAfterSortingApply: Boolean? = null
private set
var isTokensSortedByBalanceAfterSortingApply: Boolean? = null
private set
override suspend fun saveTokens(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
isGroupedByNetwork: Boolean,
isSortedByBalance: Boolean,
) {
sortTokensResult.onLeft { throw it }
tokensIdsAfterSortingApply = currencies
isTokensGroupedAfterSortingApply = isGroupedByNetwork
isTokensSortedByBalanceAfterSortingApply = isSortedByBalance
}
override suspend fun addCurrenciesCache(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
): List<CryptoCurrency> = emptyList()
override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) {
removeCurrencyResult.onLeft { throw it }
}
override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) = Unit
override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
return emptyFlow()
}
override suspend fun getMultiCurrencyWalletCurrenciesSync(
userWalletId: UserWalletId,
refresh: Boolean,
): List<CryptoCurrency> {
return tokens.first().getOrElse { e -> throw e }
}
override suspend fun getSingleCurrencyWalletPrimaryCurrency(
userWalletId: UserWalletId,
refresh: Boolean,
): CryptoCurrency {
return token.getOrElse { e -> throw e }
}
override suspend fun getSingleCurrencyWalletWithCardCurrencies(
userWalletId: UserWalletId,
refresh: Boolean,
): List<CryptoCurrency> {
return tokens.first().getOrElse { e -> throw e }
}
override suspend fun getSingleCurrencyWalletWithCardCurrency(
userWalletId: UserWalletId,
id: CryptoCurrency.ID,
): CryptoCurrency {
return token.getOrElse { e -> throw e }
}
override suspend fun getNetworkCoin(
userWalletId: UserWalletId,
networkId: Network.ID,
derivationPath: Network.DerivationPath,
): CryptoCurrency.Coin {
TODO("Not yet implemented")
}
override fun isTokensGrouped(userWalletId: UserWalletId): Flow<Boolean> {
return isGrouped.map { it.getOrElse { e -> throw e } }
}
override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow<Boolean> {
return isSortedByBalance.map { it.getOrElse { e -> throw e } }
}
override suspend fun isSendBlockedByPendingTransactions(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): Boolean {
return false
}
override suspend fun getFeePaidCurrency(userWalletId: UserWalletId, network: Network): FeePaidCurrency {
return FeePaidCurrency.Coin
}
override fun createCoinCurrency(network: Network): CryptoCurrency.Coin {
error("not implemented")
}
override fun createTokenCurrency(cryptoCurrency: CryptoCurrency.Token, network: Network): CryptoCurrency.Token {
return cryptoCurrency
}
override suspend fun createTokenCurrency(
userWalletId: UserWalletId,
contractAddress: String,
networkId: String,
): CryptoCurrency.Token {
error("not implemented")
}
override fun getAllWalletsCryptoCurrencies(
currencyRawId: CryptoCurrency.RawID,
): Flow<Map<UserWallet, List<CryptoCurrency>>> {
return emptyFlow()
}
override fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean {
return false
}
override suspend fun syncTokens(userWalletId: UserWalletId) {
return Unit
}
override fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver {
error("No-op")
}
}

View file

@ -19,7 +19,6 @@ import com.tangem.domain.models.account.DerivationIndex
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.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
import com.tangem.lib.crypto.derivation.AccountNodeRecognizer
import dagger.assisted.Assisted
@ -30,7 +29,6 @@ import timber.log.Timber
@Suppress("LongParameterList")
internal class CustomTokenFormUseCasesFacade @AssistedInject constructor(
@Assisted private val userWalletId: UserWalletId,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
private val checkIsCurrencyNotAddedUseCase: CheckIsCurrencyNotAddedUseCase,
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
@ -40,14 +38,10 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor(
) {
suspend fun addCryptoCurrenciesUseCase(currency: CryptoCurrency): Either<Throwable, Unit> {
return if (accountsFeatureToggles.isFeatureEnabled) {
either {
val accountId = getAccountId(currency)
return either {
val accountId = getAccountId(currency)
manageCryptoCurrenciesUseCase(accountId = accountId, add = currency).bind()
}
} else {
addCryptoCurrenciesUseCase.invoke(userWalletId = userWalletId, currency = currency)
manageCryptoCurrenciesUseCase(accountId = accountId, add = currency).bind()
}
}

View file

@ -7,7 +7,10 @@ import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.producer.SingleAccountProducer
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.account.supplier.SingleAccountSupplier
import com.tangem.domain.managetokens.*
import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase
import com.tangem.domain.managetokens.CheckHasLinkedTokensUseCase
import com.tangem.domain.managetokens.GetDistinctManagedCurrenciesUseCase
import com.tangem.domain.managetokens.GetManagedTokensUseCase
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
import com.tangem.domain.managetokens.model.ManageTokensListConfig
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
@ -27,10 +30,8 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor(
val getManagedTokensUseCase: GetManagedTokensUseCase,
val getDistinctManagedTokensUseCase: GetDistinctManagedCurrenciesUseCase,
private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase,
private val removeCustomCurrencyUseCase: RemoveCustomManagedCryptoCurrencyUseCase,
private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase,
private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase,
private val saveManagedTokensUseCase: SaveManagedTokensUseCase,
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
private val customTokensRepository: CustomTokensRepository,
private val accountsFeatureToggles: AccountsFeatureToggles,
@ -69,10 +70,7 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor(
manageCryptoCurrenciesUseCase(accountId = mode.accountId, remove = currency)
}
is ManageTokensMode.Wallet -> removeCustomCurrencyUseCase.invoke(
userWalletId = mode.userWalletId,
customCurrency = customCurrency,
)
is ManageTokensMode.Wallet -> error("Unsupported")
ManageTokensMode.None -> nonePortfolioError.left()
}
}
@ -147,13 +145,7 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor(
remove = currenciesToRemove.mapToCryptoCurrencies(userWalletId = mode.accountId.userWalletId),
)
}
is ManageTokensMode.Wallet -> {
saveManagedTokensUseCase.invoke(
userWalletId = mode.userWalletId,
currenciesToAdd = currenciesToAdd,
currenciesToRemove = currenciesToRemove,
)
}
is ManageTokensMode.Wallet -> error("Unsupported")
ManageTokensMode.None -> nonePortfolioError.left()
}

View file

@ -1,12 +1,11 @@
package com.tangem.features.onramp.hottokens.portfolio.model
import arrow.core.getOrElse
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.onramp.hottokens.portfolio.OnrampAddToPortfolioComponent
import com.tangem.features.onramp.hottokens.portfolio.entity.OnrampAddToPortfolioUM
@ -21,11 +20,10 @@ import javax.inject.Inject
/**
* Model for adding token to portfolio
*
* @param paramsContainer params container
* @property dispatchers dispatchers
* @property derivePublicKeysUseCase use case for deriving public key
* @property addCryptoCurrenciesUseCase use case for adding crypto currency
* @property getUserWalletUseCase use case for getting user wallet by id
* @param paramsContainer params container
* @property dispatchers dispatchers
* @property manageCryptoCurrenciesUseCase use case for managing crypto currencies
* @property getUserWalletUseCase use case for getting user wallet by id
*
[REDACTED_AUTHOR]
*/
@ -33,8 +31,7 @@ import javax.inject.Inject
internal class OnrampAddToPortfolioModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
) : Model() {
@ -72,21 +69,14 @@ internal class OnrampAddToPortfolioModel @Inject constructor(
private fun onAddClick() {
modelScope.launch {
changeAddButtonProgressStatus(isProgress = true)
derivePublicKeysUseCase(
userWalletId = params.userWalletId,
currencies = listOf(params.cryptoCurrency),
).getOrElse { throwable ->
Timber.e("Failed to derive public keys: $throwable")
changeAddButtonProgressStatus(isProgress = false)
}
addCryptoCurrenciesUseCase(
userWalletId = params.userWalletId,
currency = params.cryptoCurrency,
)
val accountId = AccountId.forMainCryptoPortfolio(params.userWalletId)
manageCryptoCurrenciesUseCase(accountId = accountId, add = params.cryptoCurrency)
.onRight { params.onSuccessAdding(params.cryptoCurrency.id) }
.onLeft { changeAddButtonProgressStatus(isProgress = false) }
.onLeft { throwable ->
Timber.e("Failed to add crypto currency: $throwable")
changeAddButtonProgressStatus(isProgress = false)
}
}
}

View file

@ -1,6 +1,6 @@
package com.tangem.feature.referral.domain
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
@ -11,7 +11,7 @@ interface ReferralInteractor {
suspend fun getReferralStatus(userWalletId: UserWalletId): ReferralData
suspend fun startReferral(portfolioId: PortfolioId): ReferralData
suspend fun startReferral(accountId: AccountId): ReferralData
suspend fun getCryptoCurrency(
userWalletId: UserWalletId,

View file

@ -1,30 +1,22 @@
package com.tangem.feature.referral.domain
import arrow.core.getOrElse
import com.tangem.common.core.TangemSdkError
import com.tangem.domain.account.producer.SingleAccountProducer
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.account.supplier.SingleAccountSupplier
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.referral.domain.errors.ReferralError
import com.tangem.feature.referral.domain.models.ReferralData
import com.tangem.feature.referral.domain.models.TokenData
import timber.log.Timber
@Suppress("LongParameterList")
internal class ReferralInteractorImpl(
private val repository: ReferralRepository,
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
private val singleAccountSupplier: SingleAccountSupplier,
private val walletManagersFacade: WalletManagersFacade,
@ -40,64 +32,41 @@ internal class ReferralInteractorImpl(
return referralData
}
override suspend fun startReferral(portfolioId: PortfolioId): ReferralData {
override suspend fun startReferral(accountId: AccountId): ReferralData {
if (tokensForReferral.isEmpty()) error("Tokens for ref is empty")
val tokenData = tokensForReferral.first()
val userWalletId = portfolioId.userWalletId
val userWallet = getUserWalletUseCase(userWalletId).getOrElse {
error("Failed to get user wallet $userWalletId: $it")
}
val userWalletId = accountId.userWalletId
val accountIndex = when (portfolioId) {
is PortfolioId.Account -> {
val account = singleAccountSupplier.getSyncOrNull(
params = SingleAccountProducer.Params(accountId = portfolioId.accountId),
)
?: error("Account not found: ${portfolioId.accountId}")
val account = singleAccountSupplier.getSyncOrNull(
params = SingleAccountProducer.Params(accountId = accountId),
)
?: error("Account not found: $accountId")
when (account) {
is Account.CryptoPortfolio -> account.derivationIndex
is Account.Payment -> TODO("[REDACTED_JIRA]")
}
}
is PortfolioId.Wallet -> null
val accountIndex = when (account) {
is Account.CryptoPortfolio -> account.derivationIndex
is Account.Payment -> TODO("[REDACTED_JIRA]")
}
val cryptoCurrency = getCryptoCurrency(
userWalletId = portfolioId.userWalletId,
userWalletId = accountId.userWalletId,
tokenData = tokenData,
accountIndex = accountIndex,
)
?: error("Failed to create crypto currency")
when (portfolioId) {
is PortfolioId.Account -> {
manageCryptoCurrenciesUseCase(
accountId = portfolioId.accountId,
add = cryptoCurrency,
skipDerivationErrors = false,
).mapLeft {
it.mapToDomainError()
}.onLeft { error ->
if (error is ReferralError.UserCancelledException) {
throw error
}
manageCryptoCurrenciesUseCase(
accountId = accountId,
add = cryptoCurrency,
skipDerivationErrors = false,
)
.mapLeft { it.mapToDomainError() }
.onLeft { error ->
Timber.e(error)
if (error is ReferralError.UserCancelledException) {
throw error
}
}
is PortfolioId.Wallet -> {
derivePublicKeysUseCase(userWallet.walletId, listOf(cryptoCurrency)).getOrElse { throwable ->
Timber.e("Failed to derive public keys: $throwable")
throw throwable.mapToDomainError()
}
addCryptoCurrenciesUseCase(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
)
}
}
.onLeft(Timber::e)
val publicAddress = walletManagersFacade.getDefaultAddress(
userWalletId = userWalletId,

View file

@ -4,10 +4,7 @@ import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.account.supplier.SingleAccountSupplier
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.referral.domain.ReferralInteractor
import com.tangem.feature.referral.domain.ReferralInteractorImpl
import com.tangem.feature.referral.domain.ReferralRepository
@ -23,18 +20,12 @@ class ReferralDomainModule {
@ModelScoped
fun provideReferralInteractor(
referralRepository: ReferralRepository,
derivePublicKeysUseCase: DerivePublicKeysUseCase,
getUserWalletUseCase: GetUserWalletUseCase,
addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
singleAccountSupplier: SingleAccountSupplier,
walletManagersFacade: WalletManagersFacade,
): ReferralInteractor {
return ReferralInteractorImpl(
repository = referralRepository,
derivePublicKeysUseCase = derivePublicKeysUseCase,
getUserWalletUseCase = getUserWalletUseCase,
addCryptoCurrenciesUseCase = addCryptoCurrenciesUseCase,
manageCryptoCurrenciesUseCase = manageCryptoCurrenciesUseCase,
singleAccountSupplier = singleAccountSupplier,
walletManagersFacade = walletManagersFacade,

View file

@ -15,7 +15,6 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.model.AccountCryptoCurrency
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
@ -24,8 +23,6 @@ import com.tangem.domain.account.status.usecase.GetAccountCurrencyByAddressUseCa
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
@ -44,10 +41,12 @@ import com.tangem.features.account.PortfolioFetcher
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.account.PortfolioSelectorController
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@OptIn(ExperimentalCoroutinesApi::class)
@Suppress("LongParameterList")
@Stable
@ModelScoped
@ -60,7 +59,6 @@ internal class ReferralModel @Inject constructor(
private val urlOpener: UrlOpener,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val portfolioFetcherFactory: PortfolioFetcher.Factory,
val portfolioSelectorController: PortfolioSelectorController,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
@ -92,26 +90,24 @@ internal class ReferralModel @Inject constructor(
init {
analyticsEventHandler.send(ReferralEvents.ReferralScreenOpened())
if (accountsFeatureToggles.isFeatureEnabled) {
combine(
flow = referralData.filterNotNull().onEach(::selectAccount),
flow2 = portfolioSelectorController.isAccountMode,
transform = { referralData, isAccountMode -> referralData to isAccountMode },
).transformLatest { (referralData, isAccountMode) ->
when (isAccountMode) {
false -> showContent(referralData)
true -> combineAccountUI(referralData)
combine(
flow = referralData.filterNotNull().onEach(::selectAccount),
flow2 = portfolioSelectorController.isAccountMode,
transform = { referralData, isAccountMode -> referralData to isAccountMode },
)
.transformLatest { (referralData, isAccountMode) ->
if (!isAccountMode) {
showContent(referralData)
} else {
combineAccountUI(referralData)
.map { referralData to it }
.collect(::emit)
}
}
.onEach { (referralData, accountAward) -> showContent(referralData, accountAward) }
.launchIn(modelScope)
} else {
referralData.filterNotNull()
.onEach(::showContent)
.launchIn(modelScope)
}
.onEach { (referralData, accountAward) -> showContent(referralData, accountAward) }
.launchIn(modelScope)
loadReferralData()
}
@ -123,12 +119,7 @@ internal class ReferralModel @Inject constructor(
flow3 = getSelectedAppCurrencyUseCase.invokeOrDefault(),
flow4 = portfolioFetcher.data,
) { pair, isBalanceHidden, appCurrency, portfolios ->
val selectedAccount = pair?.second ?: return@combine null
val cryptoPortfolio = when (selectedAccount) {
is AccountStatus.CryptoPortfolio -> selectedAccount
is AccountStatus.Payment -> TODO("[REDACTED_JIRA]")
}
val cryptoPortfolio = pair?.second ?: return@combine null
val awardCryptoCurrency = referralInteractor.getCryptoCurrency(
userWalletId = params.userWalletId,
@ -185,11 +176,8 @@ internal class ReferralModel @Inject constructor(
val lastInfoState = uiState.referralInfoState
uiState = uiState.copy(referralInfoState = ReferralInfoState.Loading)
modelScope.launch {
val portfolioId = when (accountsFeatureToggles.isFeatureEnabled) {
true -> PortfolioId(requireNotNull(portfolioSelectorController.selectedAccountSync))
false -> PortfolioId(params.userWalletId)
}
runCatching { referralInteractor.startReferral(portfolioId) }
val accountId = requireNotNull(portfolioSelectorController.selectedAccountSync)
runCatching { referralInteractor.startReferral(accountId) }
.onSuccess { referral ->
analyticsEventHandler.send(ReferralEvents.ParticipateSuccessful())
referralData.value = referral

View file

@ -23,7 +23,6 @@ import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
@ -36,7 +35,6 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.isHotWallet
import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase
import com.tangem.domain.settings.NeverShowTapHelpUseCase
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase
@ -75,10 +73,8 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
import com.tangem.utils.extensions.stripZeroPlainString
import com.tangem.utils.transformer.update
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@ -100,7 +96,6 @@ internal class SendConfirmModel @Inject constructor(
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
private val feeSelectorCheckReloadListener: FeeSelectorCheckReloadListener,
@ -114,7 +109,6 @@ internal class SendConfirmModel @Inject constructor(
private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger,
private val sendAmountReduceTrigger: SendAmountReduceTrigger,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
private val currenciesRepository: CurrenciesRepository,
private val createAndSendGaslessTransactionUseCase: CreateAndSendGaslessTransactionUseCase,
@ -466,24 +460,14 @@ internal class SendConfirmModel @Inject constructor(
.firstOrNull { it.address == confirmData.enteredDestination }
?: return
val userWalletId = receivingUserWallet.userWalletId ?: return
val network = receivingUserWallet.network ?: return
modelScope.launch(dispatchers.default) {
if (accountsFeatureToggles.isFeatureEnabled) {
val accountId = receivingUserWallet.accountId ?: return@launch
val accountId = receivingUserWallet.accountId ?: return@launch
val tokenToAdd = currenciesRepository.createTokenCurrency(cryptoCurrency, network)
manageCryptoCurrenciesUseCase(accountId = accountId, add = tokenToAdd)
} else {
withContext(NonCancellable) {
addCryptoCurrenciesUseCase(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
network = network,
)
}
}.onLeft(Timber::e)
val tokenToAdd = currenciesRepository.createTokenCurrency(cryptoCurrency, network)
manageCryptoCurrenciesUseCase(accountId = accountId, add = tokenToAdd)
.onLeft(Timber::e)
}
}

View file

@ -115,7 +115,7 @@ internal class TokenDetailsModel @Inject constructor(
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
private val getExploreUrlUseCase: GetExploreUrlUseCase,
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
private val removeCurrencyUseCase: RemoveCurrencyUseCase,
private val isCryptoCurrencyCoinCouldHideUseCase: IsCryptoCurrencyCoinCouldHideUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
@ -758,8 +758,12 @@ internal class TokenDetailsModel @Inject constructor(
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonRemoveToken(cryptoCurrency.symbol))
modelScope.launch {
val hasLinkedTokens = removeCurrencyUseCase.hasLinkedTokens(userWalletId, cryptoCurrency)
internalUiState.value = if (hasLinkedTokens) {
val canHide = cryptoCurrency is CryptoCurrency.Coin && isCryptoCurrencyCoinCouldHideUseCase(
userWalletId = userWalletId,
cryptoCurrencyCoin = cryptoCurrency,
)
internalUiState.value = if (!canHide) {
stateFactory.getStateWithLinkedTokensDialog(cryptoCurrency)
} else {
stateFactory.getStateWithConfirmHideTokenDialog(cryptoCurrency)
@ -769,18 +773,14 @@ internal class TokenDetailsModel @Inject constructor(
override fun onHideConfirmed() {
modelScope.launch {
if (accountsFeatureToggles.isFeatureEnabled) {
val accountId = account?.accountId
val accountId = account?.accountId
if (accountId == null) {
Timber.e("Account ID is null, cannot hide currency ${cryptoCurrency.id}")
return@launch
}
manageCryptoCurrenciesUseCase(accountId = accountId, remove = cryptoCurrency)
} else {
removeCurrencyUseCase(userWalletId, cryptoCurrency)
if (accountId == null) {
Timber.e("Account ID is null, cannot hide currency ${cryptoCurrency.id}")
return@launch
}
manageCryptoCurrenciesUseCase(accountId = accountId, remove = cryptoCurrency)
.onLeft { Timber.e(it) }
.onRight { router.popBackStack() }
}

View file

@ -5,7 +5,6 @@ import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.datasource.local.swap.ExpressAnalyticsStatus
import com.tangem.datasource.local.swap.SwapTransactionStatusStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
@ -14,7 +13,6 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent
import com.tangem.feature.swap.domain.SwapTransactionRepository
import com.tangem.feature.swap.domain.api.SwapRepository
@ -38,8 +36,6 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
private val swapTransactionRepository: SwapTransactionRepository,
private val swapRepository: SwapRepository,
private val quotesRepository: QuotesRepository,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
private val swapTransactionStatusStore: SwapTransactionStatusStore,
@ -117,30 +113,22 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
ifRight = { statusModel ->
sendStatusUpdateAnalytics(statusModel, provider)
val accountId = if (accountsFeatureToggles.isFeatureEnabled) {
getAccountCurrencyStatusUseCase.invokeSync(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
)
.map { it.account.accountId }
.getOrNull()
} else {
null
}
val accountId = getAccountCurrencyStatusUseCase.invokeSync(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
)
.map { it.account.accountId }
.getOrNull()
val refundTokenCurrency = if (accountsFeatureToggles.isFeatureEnabled) {
if (accountId != null) {
addRefundCurrencyIfNeededNew(
accountId = accountId,
status = statusModel,
type = provider.type,
)
} else {
Timber.e("Account ID is null, cannot add refund currency ${cryptoCurrency.id}")
null
}
val refundTokenCurrency = if (accountId != null) {
addRefundCurrencyIfNeeded(
accountId = accountId,
status = statusModel,
type = provider.type,
)
} else {
addRefundCurrencyIfNeededLegacy(status = statusModel, type = provider.type)
Timber.e("Account ID is null, cannot add refund currency ${cryptoCurrency.id}")
null
}
swapTransactionRepository.storeTransactionState(
@ -170,28 +158,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
}
}
/**
* For now do it only for dex-bridge provider
*/
private suspend fun addRefundCurrencyIfNeededLegacy(
status: ExchangeStatusModel?,
type: ExchangeProviderType,
): CryptoCurrency? {
status ?: return null
if (type != ExchangeProviderType.DEX_BRIDGE) return null
val refundNetwork = status.refundNetwork
val refundContractAddress = status.refundContractAddress
if (refundNetwork != null && refundContractAddress != null) {
return addCryptoCurrenciesUseCase(
userWalletId = userWallet.walletId,
contractAddress = refundContractAddress,
networkId = refundNetwork,
).getOrNull()
}
return null
}
private suspend fun addRefundCurrencyIfNeededNew(
private suspend fun addRefundCurrencyIfNeeded(
accountId: AccountId,
status: ExchangeStatusModel?,
type: ExchangeProviderType,

View file

@ -4,7 +4,6 @@ import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.datasource.local.swap.ExpressAnalyticsStatus
import com.tangem.datasource.local.swap.SwapTransactionStatusStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
@ -13,7 +12,6 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent
import com.tangem.feature.swap.domain.SwapTransactionRepository
import com.tangem.feature.swap.domain.api.SwapRepository
@ -39,8 +37,6 @@ internal class TokenDetailsExchangeStatusFactory @AssistedInject constructor(
private val swapTransactionRepository: SwapTransactionRepository,
private val swapRepository: SwapRepository,
private val quotesRepository: QuotesRepository,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
private val swapTransactionStatusStore: SwapTransactionStatusStore,
@ -118,30 +114,22 @@ internal class TokenDetailsExchangeStatusFactory @AssistedInject constructor(
ifRight = { statusModel ->
sendStatusUpdateAnalytics(statusModel, provider)
val accountId = if (accountsFeatureToggles.isFeatureEnabled) {
getAccountCurrencyStatusUseCase.invokeSync(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
)
.map { it.account.accountId }
.getOrNull()
} else {
null
}
val accountId = getAccountCurrencyStatusUseCase.invokeSync(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
)
.map { it.account.accountId }
.getOrNull()
val refundTokenCurrency = if (accountsFeatureToggles.isFeatureEnabled) {
if (accountId != null) {
addRefundCurrencyIfNeededNew(
accountId = accountId,
status = statusModel,
type = provider.type,
)
} else {
Timber.e("Account ID is null, cannot add refund currency ${cryptoCurrency.id}")
null
}
val refundTokenCurrency = if (accountId != null) {
addRefundCurrencyIfNeeded(
accountId = accountId,
status = statusModel,
type = provider.type,
)
} else {
addRefundCurrencyIfNeededLegacy(status = statusModel, type = provider.type)
Timber.e("Account ID is null, cannot add refund currency ${cryptoCurrency.id}")
null
}
swapTransactionRepository.storeTransactionState(
@ -171,28 +159,7 @@ internal class TokenDetailsExchangeStatusFactory @AssistedInject constructor(
}
}
/**
* For now do it only for dex-bridge provider
*/
private suspend fun addRefundCurrencyIfNeededLegacy(
status: ExchangeStatusModel?,
type: ExchangeProviderType,
): CryptoCurrency? {
status ?: return null
if (type != ExchangeProviderType.DEX_BRIDGE) return null
val refundNetwork = status.refundNetwork
val refundContractAddress = status.refundContractAddress
if (refundNetwork != null && refundContractAddress != null) {
return addCryptoCurrenciesUseCase(
userWalletId = userWallet.walletId,
contractAddress = refundContractAddress,
networkId = refundNetwork,
).getOrNull()
}
return null
}
private suspend fun addRefundCurrencyIfNeededNew(
private suspend fun addRefundCurrencyIfNeeded(
accountId: AccountId,
status: ExchangeStatusModel?,
type: ExchangeProviderType,