diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 0436f6f03d..3083d7a5df 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -246,4 +246,12 @@ internal object TokensDomainModule { fun provideGetGlobalTokenListUseCase(tokensListRepository: TokensListRepository): GetGlobalTokenListUseCase { return GetGlobalTokenListUseCase(repository = tokensListRepository) } + + @Provides + @ViewModelScoped + fun provideCheckTokenCompatibilityUseCase( + repository: NetworksCompatibilityRepository, + ): CheckCurrencyCompatibilityUseCase { + return CheckCurrencyCompatibilityUseCase(repository) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index 92aa9d5a33..8f29c32cd9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -50,6 +50,53 @@ fun getActiveIconRes(blockchainId: String): Int { } } +@Suppress("ComplexMethod") +@DrawableRes +fun getActiveIconResByNetworkId(networkId: String): Int { + return when (networkId) { + "arbitrum-one", "arbitrum-one/test" -> R.drawable.img_arbitrum_22 + "avalanche", "avalanche-2", "avalanche/test", "avalanche-2/test" -> R.drawable.img_avalanche_22 + "binance-smart-chain", "binance-smart-chain/test", "binancecoin", "binancecoin/test" -> R.drawable.img_bsc_22 + "bitcoin", "bitcoin/test" -> R.drawable.img_btc_22 + "bitcoin-cash", "bitcoin-cash/test" -> R.drawable.img_btc_cash_22 + "litecoin", "litecoin/test" -> R.drawable.img_litecoin_22 + "ethereum", "ethereum/test" -> R.drawable.img_eth_22 + "ethereum-classic", "ethereum-classic/test" -> R.drawable.img_eth_classic_22 + "rootstock" -> R.drawable.img_rsk_22 + "cardano", "cardano/test" -> R.drawable.img_cardano_22 + "tezos" -> R.drawable.img_tezos_22 + "xrp", "ripple" -> R.drawable.img_xrp_22 + "stellar", "stellar/test" -> R.drawable.img_stellar_22 + "polygon-pos", "polygon-pos/test" -> R.drawable.img_polygon_22 + "solana", "solana/test" -> R.drawable.img_solana_22 + "fantom", "fantom/test" -> R.drawable.img_fantom_22 + "dogecoin" -> R.drawable.img_dogecoin_22 + "tron", "tron/test" -> R.drawable.img_tron_22 + "xdai" -> R.drawable.img_gnosis_22 + "ethereum-pow-iou", "ethereum-pow-iou/test" -> R.drawable.img_eth_pow_22 + "ethereumfair" -> R.drawable.img_eth_fair_22 + "polkadot", "polkadot/test" -> R.drawable.img_polkadot_22 + "kusama" -> R.drawable.img_kusama_22 + "optimistic-ethereum", "optimistic-ethereum/test" -> R.drawable.img_optimism_22 + "dash" -> R.drawable.img_dash_22 + "kaspa" -> R.drawable.img_kaspa_22 + "the-open-network", "the-open-network/test" -> R.drawable.img_ton_22 + "kava", "kava/test" -> R.drawable.img_kava_22 + "ravencoin", "ravencoin/test" -> R.drawable.img_ravencoin_22 + "cosmos", "cosmos/test" -> R.drawable.img_cosmos_22 + "terra", "terra-luna" -> R.drawable.img_terra_22 + "terra-2", "terra-luna-2" -> R.drawable.img_terra2_22 + "cronos" -> R.drawable.img_cronos_22 + "telos", "telos/test" -> R.drawable.img_telos_22 + "aleph-zero", "aleph-zero/test" -> R.drawable.img_azero_22 + "octaspace", "octaspace/test" -> R.drawable.img_octaspace_22 + "chia", "chia/test" -> R.drawable.img_chia_22 + "near-protocol", "near-protocol/test" -> R.drawable.img_near_22 + "decimal", "decimal/test" -> R.drawable.img_decimal_22 + else -> R.drawable.ic_alert_24 + } +} + @Suppress("ComplexMethod") @DrawableRes fun getActiveIconResByCoinId(coinId: String): Int { @@ -139,4 +186,51 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "decimal", "decimal/test" -> R.drawable.ic_decimal_22 else -> R.drawable.ic_alert_24 } +} + +@Suppress("ComplexMethod") +@DrawableRes +fun getGreyedOutIconResByNetworkId(networkId: String): Int { + return when (networkId) { + "arbitrum-one", "arbitrum-one/test" -> R.drawable.ic_arbitrum_22 + "avalanche", "avalanche-2", "avalanche/test", "avalanche-2/test" -> R.drawable.ic_avalanche_22 + "binance-smart-chain", "binance-smart-chain/test", "binancecoin", "binancecoin/test" -> R.drawable.ic_bsc_16 + "bitcoin", "bitcoin/test" -> R.drawable.ic_bitcoin_16 + "bitcoin-cash", "bitcoin-cash/test" -> R.drawable.ic_bitcoin_cash_16 + "litecoin", "litecoin/test" -> R.drawable.ic_litecoin_22 + "ethereum", "ethereum/test" -> R.drawable.ic_eth_16 + "ethereum-classic", "ethereum-classic/test" -> R.drawable.ic_eth_16 + "rootstock" -> R.drawable.ic_rsk_16 + "cardano", "cardano/test" -> R.drawable.ic_cardano_16 + "tezos" -> R.drawable.ic_tezos_16 + "xrp", "ripple" -> R.drawable.ic_xrp_22 + "stellar", "stellar/test" -> R.drawable.ic_stellar_16 + "polygon-pos", "polygon-pos/test" -> R.drawable.ic_polygon_16 + "solana", "solana/test" -> R.drawable.ic_solana_16 + "fantom", "fantom/test" -> R.drawable.ic_fantom_22 + "dogecoin" -> R.drawable.ic_dogecoin_16 + "tron", "tron/test" -> R.drawable.ic_tron_22 + "xdai" -> R.drawable.ic_gnosis_22 + "ethereum-pow-iou", "ethereum-pow-iou/test" -> R.drawable.ic_ethereumpow_22 + "ethereumfair" -> R.drawable.ic_ethereumfair_22 + "polkadot", "polkadot/test" -> R.drawable.ic_polkadot_16 + "kusama" -> R.drawable.ic_kusama_16 + "optimistic-ethereum", "optimistic-ethereum/test" -> R.drawable.ic_optimism_22 + "dash" -> R.drawable.ic_dash_22 + "kaspa" -> R.drawable.ic_kaspa_22 + "the-open-network", "the-open-network/test" -> R.drawable.ic_ton_22 + "kava", "kava/test" -> R.drawable.ic_kava_22 + "ravencoin", "ravencoin/test" -> R.drawable.ic_ravencoin_22 + "cosmos", "cosmos/test" -> R.drawable.ic_cosmos_22 + "terra", "terra-luna" -> R.drawable.ic_terra_22 + "terra-2", "terra-luna-2" -> R.drawable.ic_terra2_22 + "cronos" -> R.drawable.ic_cronos_22 + "telos", "telos/test" -> R.drawable.ic_telos_22 + "aleph-zero", "aleph-zero/test" -> R.drawable.ic_azero_22 + "octaspace", "octaspace/test" -> R.drawable.ic_octaspace_22 + "chia", "chia/test" -> R.drawable.ic_chia_22 + "near-protocol", "near-protocol/test" -> R.drawable.ic_near_22 + "decimal", "decimal/test" -> R.drawable.ic_decimal_22 + else -> R.drawable.ic_alert_24 + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index db088b565c..ce6c4226b1 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -102,4 +102,13 @@ internal object TokensDataModule { quotesRepository = quotesRepository, ) } + + @Provides + @Singleton + fun provideCardNetworksRepository( + userWalletsStore: UserWalletsStore, + dispatchers: CoroutineDispatcherProvider, + ): NetworksCompatibilityRepository { + return DefaultNetworksCompatibilityRepository(userWalletsStore = userWalletsStore, dispatchers = dispatchers) + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt index 7735b24528..b6d7d5395e 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt @@ -32,6 +32,7 @@ internal object CoinsResponseConverter : Converter> { Token.Network( networkId = network.networkId, standardType = getNetworkStandardType(blockchain).name, + name = blockchain.fullName, address = network.contractAddress, iconUrl = getIconUrl(network.networkId, value.imageHost), decimalCount = network.decimalCount?.toInt(), diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt new file mode 100644 index 0000000000..86a31f0fea --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt @@ -0,0 +1,72 @@ +package com.tangem.data.tokens.repository + +import com.tangem.blockchain.common.Blockchain +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.extensions.canHandleBlockchain +import com.tangem.domain.common.extensions.canHandleToken +import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.common.extensions.supportedTokens +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.tokens.repository.NetworksCompatibilityRepository +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +internal class DefaultNetworksCompatibilityRepository( + private val userWalletsStore: UserWalletsStore, + private val dispatchers: CoroutineDispatcherProvider, +) : NetworksCompatibilityRepository { + + /** + * @return returns true if either the network is not Solana (check is not relevant) or if it is Solana and + * UserWallet supports tokens on Solana Network + */ + @Throws(IllegalArgumentException::class) + override suspend fun areSolanaTokensSupportedIfRelevant(networkId: String, userWalletId: UserWalletId): Boolean { + return withContext(dispatchers.io) { + val scanResponse = getWalletOrThrow(userWalletId).scanResponse + val blockchain = getBlockchainOrThrow(networkId) + val blockchainsSupportingTokens = scanResponse.card.supportedTokens(scanResponse.cardTypesResolver) + blockchain != Blockchain.Solana || blockchainsSupportingTokens.contains(Blockchain.Solana) + } + } + + @Throws(IllegalArgumentException::class) + override suspend fun areTokensSupportedByNetwork(networkId: String, userWalletId: UserWalletId): Boolean { + return withContext(dispatchers.io) { + val scanResponse = getWalletOrThrow(userWalletId).scanResponse + val blockchain = getBlockchainOrThrow(networkId) + val blockchainsSupportingTokens = scanResponse.card.supportedTokens(scanResponse.cardTypesResolver) + scanResponse.card.canHandleToken( + supportedTokens = blockchainsSupportingTokens, + blockchain = blockchain, + cardTypesResolver = scanResponse.cardTypesResolver, + ) + } + } + + @Throws(IllegalArgumentException::class) + override suspend fun isNetworkSupported(networkId: String, userWalletId: UserWalletId): Boolean { + return withContext(dispatchers.io) { + val scanResponse = getWalletOrThrow(userWalletId).scanResponse + val blockchain = getBlockchainOrThrow(networkId) + scanResponse.card.canHandleBlockchain( + blockchain = blockchain, + cardTypesResolver = scanResponse.cardTypesResolver, + ) + } + } + + private suspend fun getWalletOrThrow(userWalletId: UserWalletId): UserWallet { + return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { + "Requested UserWallet not found" + } + } + + private fun getBlockchainOrThrow(networkId: String): Blockchain { + return requireNotNull(Blockchain.fromNetworkId(networkId)) { + "Requested network not found" + } + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt index f102b2b7fa..9d9e0f3cbc 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt @@ -2,6 +2,7 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain import com.tangem.domain.common.DerivationStyleProvider +import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.tokens.model.CryptoCurrency import timber.log.Timber @@ -57,4 +58,43 @@ class CryptoCurrencyFactory { isCustom = isCustomCoin(network), ) } + + fun createCoin( + networkId: String, + extraDerivationPath: String?, + derivationStyleProvider: DerivationStyleProvider, + ): CryptoCurrency.Coin? { + val blockchain = Blockchain.fromNetworkId(networkId) ?: Blockchain.Unknown + return createCoin(blockchain, extraDerivationPath, derivationStyleProvider) + } + + fun createToken( + token: Token, + networkId: String, + extraDerivationPath: String?, + derivationStyleProvider: DerivationStyleProvider, + ): CryptoCurrency.Token? { + val sdkToken = SdkToken( + name = token.name, + symbol = token.symbol, + contractAddress = token.contractAddress, + decimals = token.decimals, + id = token.id, + ) + val blockchain = Blockchain.fromNetworkId(networkId) ?: Blockchain.Unknown + return createToken( + sdkToken = sdkToken, + blockchain = blockchain, + extraDerivationPath = extraDerivationPath, + derivationStyleProvider = derivationStyleProvider, + ) + } + + data class Token( + val name: String, + val symbol: String, + val contractAddress: String, + val decimals: Int, + val id: String? = null, + ) } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt index ed26e356b8..9c088e4fce 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt @@ -26,6 +26,7 @@ data class Token( * * @property networkId The unique identifier of the network. * @property standardType The type of blockchain associated with the network. + * @property name The full name of the network. * @property address The contract address of the token on the current network. * It is null for the main currencies of the network. * @property iconUrl URL of the network's icon. @@ -34,6 +35,7 @@ data class Token( data class Network( val networkId: String, val standardType: String, + val name: String, val address: String?, val iconUrl: String, val decimalCount: Int?, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/CheckCurrencyCompatibilityUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/CheckCurrencyCompatibilityUseCase.kt new file mode 100644 index 0000000000..6a784c589a --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/CheckCurrencyCompatibilityUseCase.kt @@ -0,0 +1,53 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.left +import arrow.core.raise.catch +import arrow.core.raise.either +import arrow.core.right +import com.tangem.domain.tokens.repository.NetworksCompatibilityRepository +import com.tangem.domain.wallets.models.UserWalletId + +class CheckCurrencyCompatibilityUseCase(private val repository: NetworksCompatibilityRepository) { + + suspend operator fun invoke( + networkId: String, + isMainNetwork: Boolean, + userWalletId: UserWalletId, + ): Either { + return either { + catch( + block = { + val error = getErrorIfNotSupported(networkId, isMainNetwork, userWalletId) + error?.left() ?: Unit.right() + }, + catch = { raise(CurrencyCompatibilityError.UnsupportedBlockchain) }, + ) + } + } + + @Throws(IllegalArgumentException::class) + private suspend fun getErrorIfNotSupported( + networkId: String, + isMainNetwork: Boolean, + userWalletId: UserWalletId, + ): CurrencyCompatibilityError? { + if (isMainNetwork) { + val supported = repository.isNetworkSupported(networkId, userWalletId) + if (!supported) return CurrencyCompatibilityError.UnsupportedBlockchain + } else { + val solanaTokensSupportedOrNotSolanaNetwork = + repository.areSolanaTokensSupportedIfRelevant(networkId, userWalletId) + if (!solanaTokensSupportedOrNotSolanaNetwork) return CurrencyCompatibilityError.SolanaTokensUnsupported + val tokensSupported = repository.areTokensSupportedByNetwork(networkId, userWalletId) + if (!tokensSupported) return CurrencyCompatibilityError.UnsupportedCurve + } + return null + } +} + +sealed class CurrencyCompatibilityError { + object SolanaTokensUnsupported : CurrencyCompatibilityError() + object UnsupportedCurve : CurrencyCompatibilityError() + object UnsupportedBlockchain : CurrencyCompatibilityError() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMissedAddressesCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMissedAddressesCryptoCurrenciesUseCase.kt index c6a1f90774..f5f5b09a43 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMissedAddressesCryptoCurrenciesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMissedAddressesCryptoCurrenciesUseCase.kt @@ -2,6 +2,7 @@ package com.tangem.domain.tokens import arrow.core.Either import arrow.core.left +import arrow.core.raise.either import arrow.core.right import com.tangem.domain.tokens.error.GetCurrenciesError import com.tangem.domain.tokens.model.CryptoCurrency @@ -9,13 +10,39 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map +/** + * Use case for retrieving a list of cryptocurrencies ([CryptoCurrency]) added to a specific wallet and requiring derivation + * (for which address is not available until the public key is derived on a card). + */ class GetMissedAddressesCryptoCurrenciesUseCase(private val currenciesRepository: CurrenciesRepository) { + /** + * Retrieves a list of cryptocurrencies without an address (requiring derivation) for a given user wallet. + * @param userWalletId: Specifies the wallet by its ID + */ operator fun invoke(userWalletId: UserWalletId): Flow>> { return currenciesRepository.getMissedAddressesCryptoCurrencies(userWalletId) .map, Either>>(List::right) .catch { emit(GetCurrenciesError.DataError(it).left()) } } + + /** + * Retrieves a map containing lists of cryptocurrencies without an address (requiring derivation) grouped by + * user wallet IDs. + * @param userWalletIds: List of IDs specifying the user wallets + */ + operator fun invoke( + userWalletIds: List, + ): Flow>>> { + return combine( + userWalletIds.map { userWalletId -> invoke(userWalletId) }, + ) { missedAddressesCryptoCurrenciesResult -> + userWalletIds.zip(missedAddressesCryptoCurrenciesResult).toMap().let { map -> + either { map.bindAll() } // taking out Left if encountered in inner responses + } + }.catch { cause -> emit(GetCurrenciesError.DataError(cause).left()) } + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt new file mode 100644 index 0000000000..6361ebdd73 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.tokens.repository + +import com.tangem.domain.wallets.models.UserWalletId + +interface NetworksCompatibilityRepository { + + @Throws(IllegalArgumentException::class) + suspend fun areSolanaTokensSupportedIfRelevant(networkId: String, userWalletId: UserWalletId): Boolean + + @Throws(IllegalArgumentException::class) + suspend fun areTokensSupportedByNetwork(networkId: String, userWalletId: UserWalletId): Boolean + + @Throws(IllegalArgumentException::class) + suspend fun isNetworkSupported(networkId: String, userWalletId: UserWalletId): Boolean +} \ No newline at end of file diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 4e28a505f2..692109ece2 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -35,7 +35,6 @@ dependencies { implementation(deps.jodatime) implementation(deps.kotlin.immutable.collections) implementation(deps.tangem.card.core) - implementation(deps.tangem.blockchain) /** DI */ implementation(deps.hilt.android) diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt index 88e4982754..e8e7018826 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt @@ -42,4 +42,11 @@ internal sealed class AlertState { formatArgs = wrappedList(networkName), ) } + + class CannotHideNetworkWithTokens(tokenName: String, networkName: String) : AlertState() { + override val message: TextReference = resourceReference( + id = R.string.token_details_unable_hide_alert_message, + formatArgs = wrappedList(tokenName, networkName), + ) + } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt index a759836afc..7f57f95a84 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt @@ -1,9 +1,7 @@ package com.tangem.managetokens.presentation.common.state import androidx.compose.runtime.MutableState -import com.tangem.blockchain.common.Blockchain -import com.tangem.core.ui.extensions.getActiveIconRes -import com.tangem.core.ui.extensions.getGreyedOutIconRes +import com.tangem.core.ui.extensions.* import com.tangem.managetokens.presentation.managetokens.state.TokenItemState /** @@ -12,7 +10,6 @@ import com.tangem.managetokens.presentation.managetokens.state.TokenItemState * @property name network name * @property protocolName network protocol name * @property id network id - * @property blockchain blockchain * @property iconRes network icon id from resources */ internal sealed interface NetworkItemState { @@ -20,7 +17,6 @@ internal sealed interface NetworkItemState { val name: String val protocolName: String val id: String - val blockchain: Blockchain val iconRes: Int get() = when (this) { @@ -34,7 +30,6 @@ internal sealed interface NetworkItemState { * @property name network name * @property protocolName network protocol name * @property id network id - * @property blockchain blockchain * @property iconResId network icon id from resources * @property isMainNetwork flag that determines if the network is the main network for the token * @property isAdded flag that determines if the user has saved the network @@ -47,7 +42,6 @@ internal sealed interface NetworkItemState { override val name: String, override val protocolName: String, override val id: String, - override val blockchain: Blockchain, val iconResId: MutableState, val isMainNetwork: Boolean, val isAdded: MutableState, @@ -64,7 +58,7 @@ internal sealed interface NetworkItemState { fun changeToggleState() { val reverseState = !isAdded.value isAdded.value = reverseState - iconResId.value = if (reverseState) getActiveIconRes(blockchain.id) else getGreyedOutIconRes(blockchain.id) + iconResId.value = if (reverseState) getActiveIconResByNetworkId(id) else getGreyedOutIconResByNetworkId(id) } } @@ -75,7 +69,6 @@ internal sealed interface NetworkItemState { * @property protocolName network protocol name * @property iconResId network icon id from resources * @property id network id - * @property blockchain blockchain * @property onNetworkClick lambda be invoked when network item is been clicked * */ @@ -84,7 +77,6 @@ internal sealed interface NetworkItemState { override val protocolName: String, val iconResId: Int, override val id: String, - override val blockchain: Blockchain, val onNetworkClick: (NetworkItemState) -> Unit, ) : NetworkItemState } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt index d1d179e53f..71e95a8bc2 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt @@ -16,6 +16,7 @@ internal fun Alert(state: AlertState, onDismiss: () -> Unit) { AlertState.TokensUnsupportedCurve, is AlertState.TokensUnsupported, is AlertState.TokensUnsupportedBlockchainByCard, + is AlertState.CannotHideNetworkWithTokens, -> DefaultAlert(state, onDismiss) is AlertState.TokenUnavailable -> TokenUnavailableAlert(state, onDismiss) } diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt index 18de0a171a..6b90d9f12b 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt @@ -15,7 +15,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.blockchain.common.Blockchain import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.components.TangemSwitch import com.tangem.core.ui.res.TangemTheme @@ -130,7 +129,6 @@ private class NetworkItemStateProvider : CollectionPreviewParameterProvider }, - blockchain = Blockchain.Ethereum, decimals = 0, ), NetworkItemState.Toggleable( @@ -142,7 +140,6 @@ private class NetworkItemStateProvider : CollectionPreviewParameterProvider }, - blockchain = Blockchain.BSC, decimals = 0, ), NetworkItemState.Selectable( @@ -151,7 +148,6 @@ private class NetworkItemStateProvider : CollectionPreviewParameterProvider): Boolean { + return if (address != null) { + currencies.any { + !it.isCustom && it is CryptoCurrency.Token && it.contractAddress == address && + it.network.backendId == networkId + } + } else { + currencies.any { + !it.isCustom && it is CryptoCurrency.Coin && it.network.backendId == networkId + } + } + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/ManageTokensStateFactory.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/ManageTokensStateFactory.kt index 672e594948..bf81e5bc4c 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/ManageTokensStateFactory.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/ManageTokensStateFactory.kt @@ -3,15 +3,12 @@ package com.tangem.managetokens.presentation.managetokens.state.factory import androidx.paging.PagingData import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent +import com.tangem.domain.tokens.CurrencyCompatibilityError +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWallet -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import com.tangem.managetokens.presentation.common.state.ChooseWalletWarning -import com.tangem.managetokens.presentation.common.state.Event -import com.tangem.managetokens.presentation.common.state.WalletState -import com.tangem.managetokens.presentation.managetokens.state.AddCustomTokenButton -import com.tangem.managetokens.presentation.managetokens.state.ManageTokensState -import com.tangem.managetokens.presentation.managetokens.state.SearchBarState -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState +import com.tangem.managetokens.presentation.common.state.* +import com.tangem.managetokens.presentation.common.utils.CurrencyUtils +import com.tangem.managetokens.presentation.managetokens.state.* import com.tangem.managetokens.presentation.managetokens.viewmodels.ManageTokensClickIntents import com.tangem.utils.Provider import kotlinx.collections.immutable.toPersistentList @@ -115,4 +112,73 @@ internal class ManageTokensStateFactory( ), ) } + + fun transformAddTokenErrorToAlert(error: CurrencyCompatibilityError, networkName: String): AlertState { + return when (error) { + CurrencyCompatibilityError.SolanaTokensUnsupported -> AlertState.TokensUnsupported(networkName) + CurrencyCompatibilityError.UnsupportedBlockchain -> AlertState.TokensUnsupportedBlockchainByCard( + networkName, + ) + CurrencyCompatibilityError.UnsupportedCurve -> AlertState.TokensUnsupportedCurve + } + } + + fun toggleNetworkState( + token: TokenItemState.Loaded, + network: NetworkItemState.Toggleable, + allAddedCurrencies: Collection, + ) { + network.changeToggleState() + val anyNetworkAdded = isAnyNetworkAdded( + networks = token.chooseNetworkState.nativeNetworks + token.chooseNetworkState.nonNativeNetworks, + allAddedCurrencies = allAddedCurrencies, + ) + val buttonType = if (anyNetworkAdded) TokenButtonType.EDIT else TokenButtonType.ADD + token.availableAction.value = buttonType + } + + private fun isAnyNetworkAdded( + networks: List, + allAddedCurrencies: Collection, + ): Boolean { + return networks.any { + it is NetworkItemState.Toggleable && CurrencyUtils.isAdded( + address = it.address, + networkId = it.id, + currencies = allAddedCurrencies, + ) + } + } + + fun updateTokenNetworksOnTokenSelection( + token: TokenItemState.Loaded, + addedCurrenciesOnWallet: Collection, + ) { + token.chooseNetworkState.nativeNetworks.forEach { + if (it is NetworkItemState.Toggleable) { + val isAdded = CurrencyUtils.isAdded(it.address, it.id, addedCurrenciesOnWallet) + if (isAdded != it.isAdded.value) (it as? NetworkItemState.Toggleable)?.changeToggleState() + } + } + token.chooseNetworkState.nonNativeNetworks.forEach { + if (it is NetworkItemState.Toggleable) { + val isAdded = CurrencyUtils.isAdded(it.address, it.id, addedCurrenciesOnWallet) + if (isAdded != it.isAdded.value) (it as? NetworkItemState.Toggleable)?.changeToggleState() + } + } + } + + fun updateDerivationNotification(totalNeeded: Int, totalWallets: Int, walletsToDerive: Int): ManageTokensState { + val derivationNotificationState = if (totalNeeded == 0) { + null + } else { + DerivationNotificationState( + totalNeeded = totalNeeded, + totalWallets = totalWallets, + walletsToDerive = walletsToDerive, + onGenerateClick = clickIntents::onGenerateDerivationClick, + ) + } + return currentStateProvider().copy(derivationNotification = derivationNotificationState) + } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworkToNetworkItemStateConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworkToNetworkItemStateConverter.kt index fab454ac84..27810cf7f7 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworkToNetworkItemStateConverter.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworkToNetworkItemStateConverter.kt @@ -2,14 +2,13 @@ package com.tangem.managetokens.presentation.managetokens.state.factory import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf -import com.tangem.blockchain.common.Blockchain -import com.tangem.core.ui.extensions.getActiveIconRes -import com.tangem.core.ui.extensions.getGreyedOutIconRes -import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.core.ui.extensions.getActiveIconResByNetworkId +import com.tangem.core.ui.extensions.getGreyedOutIconResByNetworkId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Token import com.tangem.domain.wallets.models.UserWallet import com.tangem.managetokens.presentation.common.state.NetworkItemState +import com.tangem.managetokens.presentation.common.utils.CurrencyUtils import com.tangem.managetokens.presentation.managetokens.state.TokenItemState import com.tangem.utils.Provider import com.tangem.utils.converter.Converter @@ -26,18 +25,15 @@ internal class NetworkToNetworkItemStateConverter( private fun createManageNetworkContent(network: Token.Network): NetworkItemState { val addedCurrencies = addedCurrenciesByWalletProvider()[selectedWalletProvider()] ?: emptyList() - val blockchain = requireNotNull(Blockchain.fromNetworkId(network.networkId)) { - "Network ID must be valid" - } - val isAdded = isAdded( + val isAdded = CurrencyUtils.isAdded( address = network.address, - blockchain = blockchain, + networkId = network.networkId, currencies = addedCurrencies, ) return NetworkItemState.Toggleable( - name = blockchain.fullName.uppercase(), + name = network.name.uppercase(), iconResId = mutableIntStateOf( - getNetworkIconResId(isAdded, blockchain.id), + getNetworkIconResId(isAdded, network.networkId), // todo ), isMainNetwork = isMainNetwork(network), isAdded = mutableStateOf(isAdded), @@ -45,30 +41,17 @@ internal class NetworkToNetworkItemStateConverter( protocolName = network.standardType, address = network.address, decimals = network.decimalCount, - blockchain = blockchain, onToggleClick = onNetworkToggleClick, ) } - private fun getNetworkIconResId(isAdded: Boolean, blockchainId: String): Int { + private fun getNetworkIconResId(isAdded: Boolean, networkId: String): Int { return if (isAdded) { - getActiveIconRes(blockchainId) + getActiveIconResByNetworkId(networkId) } else { - getGreyedOutIconRes(blockchainId) + getGreyedOutIconResByNetworkId(networkId) } } private fun isMainNetwork(network: Token.Network) = network.address == null - - private fun isAdded(address: String?, blockchain: Blockchain, currencies: Collection): Boolean { - return if (address != null) { - currencies.any { - !it.isCustom && it is CryptoCurrency.Token && it.contractAddress == address - } - } else { - currencies.any { - !it.isCustom && it is CryptoCurrency.Coin && it.name == blockchain.fullName - } - } - } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenConverter.kt index ed174da6d3..156c65d70d 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenConverter.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenConverter.kt @@ -1,15 +1,14 @@ package com.tangem.managetokens.presentation.managetokens.state.factory import androidx.compose.runtime.mutableStateOf -import com.tangem.blockchain.common.Blockchain import com.tangem.core.ui.extensions.ImageReference import com.tangem.core.ui.extensions.getTintForTokenIcon import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Token +import com.tangem.managetokens.presentation.common.utils.CurrencyUtils import com.tangem.managetokens.presentation.managetokens.state.QuotesState import com.tangem.managetokens.presentation.managetokens.state.TokenButtonType import com.tangem.managetokens.presentation.managetokens.state.TokenIconState @@ -27,9 +26,9 @@ internal class TokenConverter( override fun convert(value: Token): TokenItemState.Loaded { val isAnyAdded = value.networks.any { network -> - isAdded( + CurrencyUtils.isAdded( address = network.address, - blockchain = requireNotNull(Blockchain.fromNetworkId(network.networkId)), + networkId = network.networkId, currencies = allAddedCurrencies(), ) } @@ -65,16 +64,4 @@ internal class TokenConverter( onButtonClick = onTokenItemButtonClick, ) } - - private fun isAdded(address: String?, blockchain: Blockchain, currencies: Collection): Boolean { - return if (address != null) { - currencies.any { - !it.isCustom && it is CryptoCurrency.Token && it.contractAddress == address - } - } else { - currencies.any { - !it.isCustom && it is CryptoCurrency.Coin && it.name == blockchain.fullName - } - } - } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenToCryptoCurrencyConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenToCryptoCurrencyConverter.kt index c122945dd8..a7a6832cc6 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenToCryptoCurrencyConverter.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenToCryptoCurrencyConverter.kt @@ -1,9 +1,7 @@ package com.tangem.managetokens.presentation.managetokens.state.factory -import com.tangem.blockchain.common.Blockchain import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.common.DerivationStyleProvider -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.managetokens.presentation.common.state.NetworkItemState import com.tangem.managetokens.presentation.managetokens.state.TokenItemState @@ -15,28 +13,22 @@ internal class TokenToCryptoCurrencyConverter( ) : Converter { override fun convert(value: TokenItemState.Loaded): CryptoCurrency? { - val blockchain = requireNotNull(Blockchain.fromNetworkId(network.id)) - val sdkToken = if (network is NetworkItemState.Toggleable && network.address != null) { - com.tangem.blockchain.common.Token( - symbol = value.currencySymbol, - name = value.name, - contractAddress = network.address, - decimals = network.decimals!!, - id = value.tokenId, - ) - } else { - null - } - return if (sdkToken != null) { + return if (network is NetworkItemState.Toggleable && network.address != null) { CryptoCurrencyFactory().createToken( - sdkToken = sdkToken, - blockchain = blockchain, + CryptoCurrencyFactory.Token( + symbol = value.currencySymbol, + name = value.name, + id = value.tokenId, + contractAddress = network.address, + decimals = requireNotNull(network.decimals), + ), + networkId = network.id, derivationStyleProvider = derivationStyleProvider, extraDerivationPath = null, ) } else { CryptoCurrencyFactory().createCoin( - blockchain = blockchain, + networkId = network.id, derivationStyleProvider = derivationStyleProvider, extraDerivationPath = null, ) diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt index 12be7db588..e9fedc1587 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt @@ -1,7 +1,6 @@ package com.tangem.managetokens.presentation.managetokens.state.previewdata import androidx.compose.runtime.mutableStateOf -import com.tangem.blockchain.common.Blockchain import com.tangem.features.managetokens.impl.R import com.tangem.managetokens.presentation.common.state.NetworkItemState import com.tangem.managetokens.presentation.managetokens.state.ChooseNetworkState @@ -26,7 +25,6 @@ internal val nativeNetworks = listOf( isAdded = mutableStateOf(true), id = "", onToggleClick = { _, _ -> }, - blockchain = Blockchain.Ethereum, address = "", decimals = 0, ), @@ -41,7 +39,6 @@ internal val nonNativeNetworks = listOf( isAdded = mutableStateOf(true), id = "", onToggleClick = { _, _ -> }, - blockchain = Blockchain.Ethereum, address = "", decimals = 0, ), @@ -53,7 +50,6 @@ internal val nonNativeNetworks = listOf( isAdded = mutableStateOf(false), id = "", onToggleClick = { _, _ -> }, - blockchain = Blockchain.BSC, address = "", decimals = 0, ), diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt index 2c89ed4748..581bb496f7 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt @@ -9,7 +9,6 @@ import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.map import arrow.core.getOrElse -import com.tangem.blockchain.common.Blockchain import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.DerivePublicKeysUseCase @@ -21,16 +20,13 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.domain.wallets.usecase.SelectWalletUseCase -import com.tangem.domain.wallets.usecase.UpdateWalletUseCase import com.tangem.managetokens.presentation.common.state.AlertState import com.tangem.managetokens.presentation.common.state.Event import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.managetokens.presentation.managetokens.state.DerivationNotificationState import com.tangem.managetokens.presentation.managetokens.state.ManageTokensState import com.tangem.managetokens.presentation.managetokens.state.TokenButtonType import com.tangem.managetokens.presentation.managetokens.state.TokenItemState import com.tangem.managetokens.presentation.managetokens.state.factory.* -import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.Debouncer @@ -43,7 +39,7 @@ import javax.inject.Inject import kotlin.collections.set import kotlin.properties.Delegates -@Suppress("LargeClass", "LongParameterList", "TooManyFunctions") +@Suppress("LongParameterList", "LargeClass") @HiltViewModel internal class ManageTokensViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, @@ -55,9 +51,11 @@ internal class ManageTokensViewModel @Inject constructor( private val selectWalletUseCase: SelectWalletUseCase, private val getCurrenciesUseCase: GetCryptoCurrenciesUseCase, private val derivePublicKeysUseCase: DerivePublicKeysUseCase, - private val updateWalletUseCase: UpdateWalletUseCase, + private val getMissedAddressesCryptoCurrenciesUseCase: GetMissedAddressesCryptoCurrenciesUseCase, private val fetchTokenListUseCase: FetchTokenListUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val checkCurrencyCompatibilityUseCase: CheckCurrencyCompatibilityUseCase, + private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase, ) : ViewModel(), ManageTokensClickIntents, DefaultLifecycleObserver { private val debouncer = Debouncer() @@ -78,7 +76,7 @@ internal class ManageTokensViewModel @Inject constructor( private var selectedWallet: UserWallet? = null - private var neededDerivations: Map> = emptyMap() + private var neededDerivations: Map> = emptyMap() private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() @@ -129,10 +127,8 @@ internal class ManageTokensViewModel @Inject constructor( selectWalletUseCase(wallets.first().walletId) selectedWallet = wallets.first() } + updateDerivationNotificationState() withContext(dispatchers.main) { - neededDerivations = MissingDerivationsIdentifier - .getMissingDerivationsForWallets(wallets, addedCurrenciesByWallet) - updateDerivationNotificationState() uiState = stateFactory.updateChooseWalletState(wallets, userWallets, selectedWallet) } } @@ -145,30 +141,28 @@ internal class ManageTokensViewModel @Inject constructor( } } - private fun isAdded(address: String?, blockchain: Blockchain, currencies: Collection): Boolean { - return if (address != null) { - currencies.any { - !it.isCustom && it is CryptoCurrency.Token && it.contractAddress == address - } - } else { - currencies.any { - !it.isCustom && it is CryptoCurrency.Coin && it.name == blockchain.fullName - } + private fun updateDerivationNotificationState() { + viewModelScope.launch(dispatchers.io) { + getMissedAddressesCryptoCurrenciesUseCase(wallets.map { it.walletId }) + .distinctUntilChanged() + .collectLatest { + it.onRight { mapOfMissingDerivations -> + neededDerivations = mapOfMissingDerivations + withContext(dispatchers.main) { updateDerivation() } + } + } } } - private fun updateDerivationNotificationState() { - val derivationNotificationState = if (neededDerivations.isEmpty()) { - null - } else { - DerivationNotificationState( - totalNeeded = neededDerivations.values.map { it.map { it.derivationsNeeded() } }.sumOf { it.sum() }, - totalWallets = wallets.size, - walletsToDerive = neededDerivations.keys.size, - onGenerateClick = this::onGenerateDerivationClick, - ) - } - uiState = uiState.copy(derivationNotification = derivationNotificationState) + private fun updateDerivation() { + val totalNeeded = neededDerivations.values.sumOf { derivations -> derivations.size } + val walletsToDerive = neededDerivations.values + .filter { derivations -> derivations.isNotEmpty() }.size + uiState = stateFactory.updateDerivationNotification( + totalNeeded = totalNeeded, + totalWallets = wallets.size, + walletsToDerive = walletsToDerive, + ) } private fun createSelectedAppCurrencyFlow(): StateFlow { @@ -204,21 +198,8 @@ internal class ManageTokensViewModel @Inject constructor( when (token.availableAction.value) { TokenButtonType.ADD, TokenButtonType.EDIT -> { uiState = uiState.copy(selectedToken = token) - val addedCurrenciesOnWallet = addedCurrenciesByWallet[selectedWallet] ?: listOf() - - token.chooseNetworkState.nativeNetworks.forEach { - if (it is NetworkItemState.Toggleable) { - val isAdded = isAdded(it.address, it.blockchain, addedCurrenciesOnWallet) - if (isAdded != it.isAdded.value) (it as? NetworkItemState.Toggleable)?.changeToggleState() - } - } - token.chooseNetworkState.nonNativeNetworks.forEach { - if (it is NetworkItemState.Toggleable) { - val isAdded = isAdded(it.address, it.blockchain, addedCurrenciesOnWallet) - if (isAdded != it.isAdded.value) (it as? NetworkItemState.Toggleable)?.changeToggleState() - } - } + stateFactory.updateTokenNetworksOnTokenSelection(token, addedCurrenciesOnWallet) } TokenButtonType.NOT_AVAILABLE -> { uiState = stateFactory.getStateAndTriggerEvent( @@ -237,32 +218,13 @@ internal class ManageTokensViewModel @Inject constructor( override fun onGenerateDerivationClick() { if (neededDerivations.isNotEmpty()) { viewModelScope.launch(dispatchers.io) { - val wallet = neededDerivations.keys.firstOrNull() - val derivations = neededDerivations[wallet]?.toMapOfDerivations() - if (wallet == null || derivations.isNullOrEmpty()) return@launch - - derivePublicKeysUseCase(cardId = null, derivations = derivations) + val walletId = neededDerivations.keys.firstOrNull() + val currenciesToDerive = neededDerivations[walletId] + if (walletId == null || currenciesToDerive.isNullOrEmpty()) return@launch + derivePublicKeysUseCase(walletId, currenciesToDerive) .onRight { - val newDerivedKeys = it.entries - val oldDerivedKeys = wallet.scanResponse.derivedKeys - - val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet() - - val updatedDerivedKeys = walletKeys.associateWith { walletKey -> - val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap()) - val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap()) - ExtendedPublicKeysMap(oldDerivations + newDerivations) - } - val updatedScanResponse = wallet.scanResponse.copy(derivedKeys = updatedDerivedKeys) - - updateWalletUseCase( - userWalletId = wallet.walletId, - update = { userWallet -> userWallet.copy(scanResponse = updatedScanResponse) }, - ).onRight { userWallet -> - fetchTokenListUseCase(userWalletId = userWallet.walletId) - } - updateDerivationNotificationState() + fetchTokenListUseCase(userWalletId = walletId) } } } @@ -283,31 +245,41 @@ internal class ManageTokensViewModel @Inject constructor( if (network.isAdded.value) { toggleToken(token, network, selectedWallet) } else { - TokenCompatibility(selectedWallet) - .check(network.blockchain, network.address == null) - .fold( - ifLeft = { error -> - val alert = when (error) { - TokenCompatibility.AddTokenError.SolanaTokensUnsupported -> - AlertState.TokensUnsupported(network.blockchain.fullName) - TokenCompatibility.AddTokenError.UnsupportedBlockchain -> - AlertState.TokensUnsupportedBlockchainByCard(network.blockchain.fullName) - TokenCompatibility.AddTokenError.UnsupportedCurve -> - AlertState.TokensUnsupportedCurve - } - uiState = stateFactory.getStateAndTriggerEvent( - state = uiState, - event = Event.ShowAlert(alert), - setUiState = { uiState = it }, - ) - }, - ifRight = { - toggleToken(token, network, selectedWallet) - }, - ) + viewModelScope.launch(dispatchers.io) { + checkCompatibilityAndToggleToken(token, network, selectedWallet) + } } } + private suspend fun checkCompatibilityAndToggleToken( + token: TokenItemState.Loaded, + network: NetworkItemState.Toggleable, + selectedWallet: UserWallet, + ) { + checkCurrencyCompatibilityUseCase( + networkId = network.id, + isMainNetwork = network.address == null, + userWalletId = selectedWallet.walletId, + ) + .fold( + ifLeft = { error -> + uiState = stateFactory.getStateAndTriggerEvent( + state = uiState, + event = Event.ShowAlert( + stateFactory.transformAddTokenErrorToAlert( + error, + network.name, + ), + ), + setUiState = { uiState = it }, + ) + }, + ifRight = { + withContext(dispatchers.main) { toggleToken(token, network, selectedWallet) } + }, + ) + } + private fun toggleToken( token: TokenItemState.Loaded, network: NetworkItemState.Toggleable, @@ -321,41 +293,56 @@ internal class ManageTokensViewModel @Inject constructor( ) { "It is only null if Blockchain is Unknown, which mustn't happen here" } - if (!network.isAdded.value) { + updateUi(token, network) addedCurrenciesByWallet[selectedWallet]?.add(cryptoCurrency) allAddedCurrencies.add(cryptoCurrency) viewModelScope.launch(dispatchers.io) { - addCryptoCurrenciesUseCase(selectedWallet.walletId, cryptoCurrency) + addCryptoCurrenciesUseCase( + userWalletId = selectedWallet.walletId, + currency = cryptoCurrency, + ) } } else { - addedCurrenciesByWallet[selectedWallet]?.remove(cryptoCurrency) - allAddedCurrencies.remove(cryptoCurrency) viewModelScope.launch(dispatchers.io) { - removeCurrencyUseCase(selectedWallet.walletId, cryptoCurrency) + if (canBeRemovedAndShowAlertIfNot(selectedWallet.walletId, cryptoCurrency)) { + withContext(dispatchers.main) { updateUi(token, network) } + addedCurrenciesByWallet[selectedWallet]?.remove(cryptoCurrency) + allAddedCurrencies.remove(cryptoCurrency) + removeCurrencyUseCase(selectedWallet.walletId, cryptoCurrency) + } } } - network.changeToggleState() - val isAnyAdded = token.chooseNetworkState.nativeNetworks.any { - it is NetworkItemState.Toggleable && isAdded( - address = it.address, - blockchain = it.blockchain, - currencies = allAddedCurrencies, - ) - } || token.chooseNetworkState.nonNativeNetworks.any { - it is NetworkItemState.Toggleable && isAdded( - address = it.address, - blockchain = it.blockchain, - currencies = allAddedCurrencies, - ) - } - val buttonType = if (isAnyAdded) TokenButtonType.EDIT else TokenButtonType.ADD - token.availableAction.value = buttonType - neededDerivations = MissingDerivationsIdentifier - .getMissingDerivationsForWallets(wallets, addedCurrenciesByWallet) + } + + private fun updateUi(token: TokenItemState.Loaded, network: NetworkItemState.Toggleable) { + stateFactory.toggleNetworkState(token, network, allAddedCurrencies) updateDerivationNotificationState() } + private suspend fun canBeRemovedAndShowAlertIfNot( + walletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): Boolean { + return if (cryptoCurrency is CryptoCurrency.Coin && + !isCryptoCurrencyCoinCouldHide(walletId, cryptoCurrency) + ) { + uiState = stateFactory.getStateAndTriggerEvent( + state = uiState, + event = Event.ShowAlert( + AlertState.CannotHideNetworkWithTokens( + tokenName = cryptoCurrency.name, + networkName = cryptoCurrency.network.name, + ), + ), + setUiState = { uiState = it }, + ) + false + } else { + true + } + } + override fun onNonNativeNetworkHintClick() { uiState = stateFactory.getStateAndTriggerEvent( state = uiState, diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/MissingDerivationsIdentifier.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/MissingDerivationsIdentifier.kt deleted file mode 100644 index a6c097b87f..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/MissingDerivationsIdentifier.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.viewmodels - -import com.tangem.blockchain.blockchains.cardano.CardanoUtils -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.card.EllipticCurve -import com.tangem.common.extensions.ByteArrayKey -import com.tangem.common.extensions.toMapKey -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.common.configs.CardConfig -import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.operations.derivation.ExtendedPublicKeysMap - -object MissingDerivationsIdentifier { - - fun getMissingDerivationsForWallets( - wallets: List, - addedCurrenciesByWallet: MutableMap>, - ): Map> { - return wallets.associateWith { wallet -> - val currencies = addedCurrenciesByWallet[wallet] ?: emptyList() - val config = CardConfig.createConfig(wallet.scanResponse.card) - val missingDerivationsByCurrency = currencies.mapNotNull { currency -> - config.primaryCurve(blockchain = Blockchain.fromId(currency.network.id.value))?.let { curve -> - getMissingDerivations(curve, wallet.scanResponse, currency) - } - } - collectMissingDerivationsByPublicKey(missingDerivationsByCurrency) - }.filter { it.value.isNotEmpty() } - } - - private fun getMissingDerivations( - curve: EllipticCurve, - scanResponse: ScanResponse, - currency: CryptoCurrency, - ): DerivationData? { - val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null - - val blockchain = Blockchain.fromId(currency.network.id.value) - val supportedCurves = blockchain.getSupportedCurves() - val path = blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) - .takeIf { supportedCurves.contains(curve) } - - val customPath = currency.network.derivationPath.value?.let { - DerivationPath(it) - }.takeIf { supportedCurves.contains(curve) } - - val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList() - if (bothCandidates.isEmpty()) return null - - if (currency is CryptoCurrency.Coin && blockchain == Blockchain.Cardano) { - currency.network.derivationPath.value?.let { - bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it))) - } - } - - val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey() - val alreadyDerivedKeys: ExtendedPublicKeysMap = - scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap()) - val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList() - - val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) } - if (toDerive.isEmpty()) return null - - return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) - } - - private fun collectMissingDerivationsByPublicKey( - missingDerivationsByCurrency: List, - ): List { - return buildMap> { - missingDerivationsByCurrency.forEach { derivationData -> - val currentEntry = this[derivationData.derivations.first] - if (currentEntry != null) { - currentEntry.addAll(derivationData.derivations.second) - currentEntry.distinct() - } else { - this[derivationData.derivations.first] = derivationData.derivations.second.toMutableList() - } - } - }.map { DerivationData(it.key to it.value) } - } -} - -class DerivationData(val derivations: Pair>) { - fun derivationsNeeded(): Int { - return derivations.second.size - } -} - -internal fun List.toMapOfDerivations(): Map> { - return associate { it.derivations } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/TokenCompatibility.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/TokenCompatibility.kt deleted file mode 100644 index c0e85efd31..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/TokenCompatibility.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.viewmodels - -import arrow.core.Either -import arrow.core.left -import arrow.core.right -import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.common.extensions.canHandleBlockchain -import com.tangem.domain.common.extensions.canHandleToken -import com.tangem.domain.common.extensions.supportedTokens -import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.wallets.models.UserWallet - -class TokenCompatibility(private val selectedWallet: UserWallet) { - - fun check(blockchain: Blockchain, isMainNetwork: Boolean): Either { - val error = if (isMainNetwork) isUnsupportedBlockchain(blockchain) else isUnsupportedToken(blockchain) - return error?.left() ?: Unit.right() - } - - private fun isUnsupportedToken(blockchain: Blockchain): AddTokenError? { - val cardTypesResolver = selectedWallet.scanResponse.cardTypesResolver - val blockchainsSupportingTokens = selectedWallet.scanResponse.card.supportedTokens(cardTypesResolver) - - if (blockchain == Blockchain.Solana && !blockchainsSupportingTokens.contains(Blockchain.Solana)) { - return AddTokenError.SolanaTokensUnsupported - } - val canHandleToken = selectedWallet.scanResponse.card.canHandleToken( - supportedTokens = blockchainsSupportingTokens, - blockchain = blockchain, - cardTypesResolver = cardTypesResolver, - ) - if (!canHandleToken) { - return AddTokenError.UnsupportedCurve - } - return null - } - - private fun isUnsupportedBlockchain(blockchain: Blockchain): AddTokenError? { - val isSupported = selectedWallet.scanResponse.card.canHandleBlockchain( - blockchain = blockchain, - cardTypesResolver = selectedWallet.scanResponse.cardTypesResolver, - ) - return if (!isSupported) AddTokenError.UnsupportedBlockchain else null - } - - sealed class AddTokenError { - object SolanaTokensUnsupported : AddTokenError() - object UnsupportedCurve : AddTokenError() - object UnsupportedBlockchain : AddTokenError() - } -} \ No newline at end of file