From 6c82e6e484361fbe4eabd159ffe4e7fdd87449eb Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 28 Nov 2023 17:03:17 +0100 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 12 + .../managetokens/ManageTokensFragment.kt | 10 +- .../presentation/common/state/AlertState.kt | 11 +- .../common/ui/ChooseWalletScreen.kt | 10 +- .../common/ui/components/Alert.kt | 2 +- .../state/factory/ManageTokensStateFactory.kt | 118 ++++++ .../NetworkToNetworkItemStateConverter.kt | 74 ++++ .../NetworksToChooseNetworkStateConverter.kt | 36 ++ .../factory/QuotesToQuotesStateConverter.kt | 34 ++ .../state/factory/TokenConverter.kt | 80 ++++ .../factory/TokenToCryptoCurrencyConverter.kt | 45 ++ .../managetokens/ui/ChooseNetworkScreen.kt | 8 +- .../managetokens/ui/components/TokensList.kt | 6 +- .../viewmodels/ManageTokensClickIntents.kt | 30 ++ .../viewmodels/ManageTokensViewModel.kt | 393 ++++++++++++++++++ .../MissingDerivationsIdentifier.kt | 95 +++++ .../viewmodels/TokenCompatibility.kt | 51 +++ 17 files changed, 1001 insertions(+), 14 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/ManageTokensStateFactory.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworkToNetworkItemStateConverter.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworksToChooseNetworkStateConverter.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/QuotesToQuotesStateConverter.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenConverter.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenToCryptoCurrencyConverter.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensClickIntents.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/MissingDerivationsIdentifier.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/TokenCompatibility.kt 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 d4488fe919..0436f6f03d 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 @@ -16,6 +16,18 @@ import dagger.hilt.android.scopes.ViewModelScoped @Suppress("TooManyFunctions") internal object TokensDomainModule { + @Provides + @ViewModelScoped + fun provideAddCryptoCurrenciesUseCase( + currenciesRepository: CurrenciesRepository, + networksRepository: NetworksRepository, + ): AddCryptoCurrenciesUseCase { + return AddCryptoCurrenciesUseCase( + currenciesRepository = currenciesRepository, + networksRepository = networksRepository, + ) + } + @Provides @ViewModelScoped fun provideFetchTokenListUseCase( diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/ManageTokensFragment.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/ManageTokensFragment.kt index f4f7fa959b..b898add2be 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/ManageTokensFragment.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/ManageTokensFragment.kt @@ -2,11 +2,15 @@ package com.tangem.managetokens import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.hilt.navigation.compose.hiltViewModel import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.features.managetokens.navigation.ManageTokensRouter +import com.tangem.managetokens.presentation.managetokens.ui.ManageTokensScreen +import com.tangem.managetokens.presentation.managetokens.viewmodels.ManageTokensViewModel import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -26,16 +30,16 @@ internal class ManageTokensFragment : ComposeFragment() { @Composable override fun ScreenContent(modifier: Modifier) { - // val viewModel = hiltViewModel() + val viewModel = hiltViewModel() // viewModel.router = [REDACTED_EMAIL] // - // LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) + LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) val systemBarsColor = TangemTheme.colors.background.secondary SystemBarsEffect { setSystemBarsColor(systemBarsColor) } - // ManageTokensScreen(state = viewModel.uiState) + ManageTokensScreen(state = viewModel.uiState) } } \ No newline at end of file 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 767427f16d..88e4982754 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 @@ -25,18 +25,21 @@ internal sealed class AlertState { override val message: TextReference = resourceReference(R.string.manage_tokens_network_selector_non_native_info) } - object TokensUnsupported : AlertState() { - override val message: TextReference = resourceReference(R.string.alert_manage_tokens_unsupported_message) + class TokensUnsupported(networkName: String) : AlertState() { + override val message: TextReference = resourceReference( + id = R.string.alert_manage_tokens_unsupported_message, + formatArgs = wrappedList(networkName), + ) } object TokensUnsupportedCurve : AlertState() { override val message: TextReference = resourceReference(R.string.alert_manage_tokens_unsupported_curve_message) } - class TokensUnsupportedBlockchainByCard(val token: String) : AlertState() { + class TokensUnsupportedBlockchainByCard(networkName: String) : AlertState() { override val message: TextReference = resourceReference( id = R.string.alert_manage_tokens_unsupported_blockchain_by_card_message, - formatArgs = wrappedList(token), + formatArgs = wrappedList(networkName), ) } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt index d0dab333d8..617fed74bd 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt @@ -33,7 +33,11 @@ internal fun ChooseWalletScreen(state: ChooseWalletState.Choose, modifier: Modif LazyColumn( modifier = modifier .background(TangemTheme.colors.background.tertiary) - .padding(TangemTheme.dimens.spacing16), + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), ) { item { Box( @@ -101,13 +105,13 @@ private fun WalletItem(wallet: WalletState, selectedWallet: WalletState?, modifi .build(), loading = { Image( - painter = painterResource(R.drawable.card_placeholder_primary), + painter = painterResource(R.drawable.card_placeholder_black), contentDescription = null, ) }, error = { Image( - painter = painterResource(R.drawable.card_placeholder_primary), + painter = painterResource(R.drawable.card_placeholder_black), contentDescription = null, ) }, 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 27b83aca6b..d1d179e53f 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 @@ -14,7 +14,7 @@ internal fun Alert(state: AlertState, onDismiss: () -> Unit) { is AlertState.DefaultAlert, is AlertState.NonNative, AlertState.TokensUnsupportedCurve, - AlertState.TokensUnsupported, + is AlertState.TokensUnsupported, is AlertState.TokensUnsupportedBlockchainByCard, -> DefaultAlert(state, onDismiss) is AlertState.TokenUnavailable -> TokenUnavailableAlert(state, onDismiss) 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 new file mode 100644 index 0000000000..672e594948 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/ManageTokensStateFactory.kt @@ -0,0 +1,118 @@ +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.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.managetokens.viewmodels.ManageTokensClickIntents +import com.tangem.utils.Provider +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.Flow + +internal class ManageTokensStateFactory( + private val currentStateProvider: Provider, + private val clickIntents: ManageTokensClickIntents, +) { + + fun getInitialState(tokens: Flow>): ManageTokensState { + return ManageTokensState( + searchBarState = SearchBarState( + query = "", + onQueryChange = clickIntents::onSearchQueryChange, + active = false, + onActiveChange = clickIntents::onSearchActiveChange, + ), + tokens = tokens, + addCustomTokenButton = AddCustomTokenButton( + isVisible = false, + onClick = clickIntents::onAddCustomTokensButtonClick, + ), + derivationNotification = null, + isLoading = false, + event = consumedEvent(), + chooseWalletState = ChooseWalletState.NoSelection, + ) + } + + fun updateChooseWalletState( + wallets: List, + userWallets: List, + selectedWallet: UserWallet?, + ): ManageTokensState { + val chooseWalletState = when { + wallets.size == 1 -> { + ChooseWalletState.NoSelection + } + wallets.isEmpty() && userWallets.all { !it.isMultiCurrency } -> { + ChooseWalletState.Warning(ChooseWalletWarning.SINGLE_CURRENCY) + } + else -> { + var selectedWalletState: WalletState? = null + ChooseWalletState.Choose( + wallets = wallets.map { wallet -> + val walletState = WalletState( + walletId = wallet.walletId.stringValue, + artworkUrl = wallet.artworkUrl, + onSelected = clickIntents::onWalletSelected, + walletName = wallet.name, + ) + if (wallet.walletId.stringValue == selectedWallet?.walletId?.stringValue) { + selectedWalletState = walletState + } + walletState + }.toPersistentList(), + selectedWallet = selectedWalletState, + onChooseWalletClick = clickIntents::onChooseWalletClick, + onCloseChoosingWalletClick = clickIntents::onCloseChoosingWalletClick, + ) + } + } + return currentStateProvider().copy(chooseWalletState = chooseWalletState) + } + + fun showAddCustomTokensButton(show: Boolean): ManageTokensState { + return currentStateProvider().copy( + addCustomTokenButton = currentStateProvider().addCustomTokenButton.copy(isVisible = show), + ) + } + + fun updateSelectedWallet(selectedWalletId: String?): ManageTokensState { + val chooseWalletState = currentStateProvider().chooseWalletState + return currentStateProvider().copy( + showChooseWalletScreen = false, + chooseWalletState = if (chooseWalletState is ChooseWalletState.Choose) { + chooseWalletState.copy( + selectedWallet = chooseWalletState.wallets.find { + it.walletId == selectedWalletId + } ?: chooseWalletState.wallets.first(), + ) + } else { + chooseWalletState + }, + ) + } + + fun getStateAndTriggerEvent( + state: ManageTokensState, + event: Event, + setUiState: (ManageTokensState) -> Unit, + ): ManageTokensState { + return state.copy( + event = triggeredEvent( + data = event, + onConsume = { + val currentState = currentStateProvider() + setUiState(currentState.copy(event = consumedEvent())) + }, + ), + ) + } +} \ 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 new file mode 100644 index 0000000000..fab454ac84 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworkToNetworkItemStateConverter.kt @@ -0,0 +1,74 @@ +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.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.managetokens.state.TokenItemState +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter + +internal class NetworkToNetworkItemStateConverter( + private val addedCurrenciesByWalletProvider: Provider>>, + private val selectedWalletProvider: Provider, + private val onNetworkToggleClick: (token: TokenItemState.Loaded, network: NetworkItemState.Toggleable) -> Unit, +) : Converter { + + override fun convert(value: Token.Network): NetworkItemState { + return createManageNetworkContent(value) + } + + 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( + address = network.address, + blockchain = blockchain, + currencies = addedCurrencies, + ) + return NetworkItemState.Toggleable( + name = blockchain.fullName.uppercase(), + iconResId = mutableIntStateOf( + getNetworkIconResId(isAdded, blockchain.id), + ), + isMainNetwork = isMainNetwork(network), + isAdded = mutableStateOf(isAdded), + id = network.networkId, + protocolName = network.standardType, + address = network.address, + decimals = network.decimalCount, + blockchain = blockchain, + onToggleClick = onNetworkToggleClick, + ) + } + + private fun getNetworkIconResId(isAdded: Boolean, blockchainId: String): Int { + return if (isAdded) { + getActiveIconRes(blockchainId) + } else { + getGreyedOutIconRes(blockchainId) + } + } + + 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/NetworksToChooseNetworkStateConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworksToChooseNetworkStateConverter.kt new file mode 100644 index 0000000000..99509e85ea --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworksToChooseNetworkStateConverter.kt @@ -0,0 +1,36 @@ +package com.tangem.managetokens.presentation.managetokens.state.factory + +import com.tangem.domain.tokens.model.Token +import com.tangem.managetokens.presentation.common.state.NetworkItemState +import com.tangem.managetokens.presentation.managetokens.state.ChooseNetworkState +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toPersistentList + +internal class NetworksToChooseNetworkStateConverter( + private val networkToNetworkItemStateConverter: NetworkToNetworkItemStateConverter, + private val onNonNativeNetworkHintClick: () -> Unit, + private val onCloseChooseNetworkScreen: () -> Unit, +) : Converter, ChooseNetworkState> { + + override fun convert(value: List): ChooseNetworkState { + return createChooseNetworksState(value) + } + + private fun createChooseNetworksState(networks: List): ChooseNetworkState { + val nativeNetworks = mutableListOf() + val nonNativeNetworks = mutableListOf() + networks.map { networkToNetworkItemStateConverter.convert(it) }.forEach { + if (it is NetworkItemState.Toggleable && it.isMainNetwork) { + nativeNetworks.add(it) + } else { + nonNativeNetworks.add(it) + } + } + return ChooseNetworkState( + nativeNetworks = nativeNetworks.toPersistentList(), + nonNativeNetworks = nonNativeNetworks.toPersistentList(), + onNonNativeNetworkHintClick = onNonNativeNetworkHintClick, + onCloseChooseNetworkScreen = onCloseChooseNetworkScreen, + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/QuotesToQuotesStateConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/QuotesToQuotesStateConverter.kt new file mode 100644 index 0000000000..b9559d3a9c --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/QuotesToQuotesStateConverter.kt @@ -0,0 +1,34 @@ +package com.tangem.managetokens.presentation.managetokens.state.factory + +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.tokens.model.Quote +import com.tangem.managetokens.presentation.managetokens.state.PriceChangeType +import com.tangem.managetokens.presentation.managetokens.state.QuotesState +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal + +@Suppress("MagicNumber") // TODO: remove when chart data is added (in [REDACTED_TASK_KEY] when endpoint is ready) +internal class QuotesToQuotesStateConverter : Converter { + override fun convert(value: Quote): QuotesState { + val priceChange = value.priceChange + return QuotesState.Content( + priceChange = BigDecimalFormatter.formatPercent( + percent = priceChange.movePointLeft(2), + useAbsoluteValue = true, + maxFractionDigits = 1, + minFractionDigits = 1, + ), + changeType = priceChange.getPriceChangeType(), + chartData = // TODO (in [REDACTED_TASK_KEY] when endpoint is ready) + when (priceChange.getPriceChangeType()) { + PriceChangeType.UP -> persistentListOf(0f, 5f, 10f, 30f) + PriceChangeType.DOWN -> persistentListOf(15f, 12f, 13f, 18f, 10f, 3f) + }, + ) + } + + private fun BigDecimal.getPriceChangeType(): PriceChangeType { + return if (this >= BigDecimal.ZERO) PriceChangeType.UP else PriceChangeType.DOWN + } +} \ 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 new file mode 100644 index 0000000000..ed174da6d3 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenConverter.kt @@ -0,0 +1,80 @@ +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.managetokens.state.QuotesState +import com.tangem.managetokens.presentation.managetokens.state.TokenButtonType +import com.tangem.managetokens.presentation.managetokens.state.TokenIconState +import com.tangem.managetokens.presentation.managetokens.state.TokenItemState +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter + +internal class TokenConverter( + private val quotesStateConverter: QuotesToQuotesStateConverter, + private val networksToChooseNetworkStateConverter: NetworksToChooseNetworkStateConverter, + private val allAddedCurrencies: Provider>, + private val selectedAppCurrency: Provider, + private val onTokenItemButtonClick: (TokenItemState.Loaded) -> Unit, +) : Converter { + + override fun convert(value: Token): TokenItemState.Loaded { + val isAnyAdded = value.networks.any { network -> + isAdded( + address = network.address, + blockchain = requireNotNull(Blockchain.fromNetworkId(network.networkId)), + currencies = allAddedCurrencies(), + ) + } + val buttonType = when { + value.isAvailable && isAnyAdded -> TokenButtonType.EDIT + value.isAvailable && !isAnyAdded -> TokenButtonType.ADD + else -> TokenButtonType.NOT_AVAILABLE + } + + val background = tryGetBackgroundForTokenIcon(value.networks.firstOrNull()?.address ?: "") + val tint = getTintForTokenIcon(background) + + return TokenItemState.Loaded( + id = value.id, + name = value.name, + currencySymbol = value.symbol, + tokenId = value.id, + tokenIcon = TokenIconState( + ImageReference.Url(value.iconUrl), + placeholderBackground = background, + placeholderTint = tint, + ), + quotes = value.quote?.let { quotesStateConverter.convert(it) } ?: QuotesState.Unknown, + rate = value.quote?.fiatRate?.let { rate -> + BigDecimalFormatter.formatFiatAmount( + fiatAmount = rate, + fiatCurrencyCode = selectedAppCurrency().code, + fiatCurrencySymbol = selectedAppCurrency().symbol, + ) + }, + availableAction = mutableStateOf(buttonType), + chooseNetworkState = networksToChooseNetworkStateConverter.convert(value.networks), + 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 new file mode 100644 index 0000000000..c122945dd8 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenToCryptoCurrencyConverter.kt @@ -0,0 +1,45 @@ +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 +import com.tangem.utils.converter.Converter + +internal class TokenToCryptoCurrencyConverter( + private val network: NetworkItemState, + private val derivationStyleProvider: DerivationStyleProvider, +) : 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) { + CryptoCurrencyFactory().createToken( + sdkToken = sdkToken, + blockchain = blockchain, + derivationStyleProvider = derivationStyleProvider, + extraDerivationPath = null, + ) + } else { + CryptoCurrencyFactory().createCoin( + blockchain = blockchain, + derivationStyleProvider = derivationStyleProvider, + extraDerivationPath = null, + ) + } + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt index ebc600073f..444df9d0fe 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt @@ -35,9 +35,13 @@ internal fun ChooseNetworkScreen( ) { val networkState = state.chooseNetworkState LazyColumn( + contentPadding = PaddingValues( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), modifier = modifier - .background(TangemTheme.colors.background.tertiary) - .padding(TangemTheme.dimens.spacing16), + .background(TangemTheme.colors.background.tertiary), ) { item { Text( diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt index fe6c1c2bf7..02887479e7 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt @@ -40,7 +40,11 @@ internal fun TokensList(tokens: LazyPagingItems, addCustomTokenB text = stringResource(id = R.string.manage_tokens_nothing_found), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + modifier = Modifier.padding( + top = TangemTheme.dimens.spacing8, + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + ), ) } } diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensClickIntents.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensClickIntents.kt new file mode 100644 index 0000000000..a86b2cd1bb --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensClickIntents.kt @@ -0,0 +1,30 @@ +package com.tangem.managetokens.presentation.managetokens.viewmodels + +import com.tangem.managetokens.presentation.common.state.NetworkItemState +import com.tangem.managetokens.presentation.managetokens.state.TokenItemState + +internal interface ManageTokensClickIntents { + fun onAddCustomTokensButtonClick() + + fun onSearchQueryChange(query: String) + fun onSearchActiveChange(active: Boolean) + + fun onTokenItemButtonClick(token: TokenItemState.Loaded) + + fun onGenerateDerivationClick() + + fun onBackClick() + + fun onCloseChooseNetworkScreen() + + fun onNetworkToggleClick(token: TokenItemState.Loaded, network: NetworkItemState.Toggleable) + fun onNonNativeNetworkHintClick() + + fun onSelectWalletsClick() + + fun onChooseWalletClick() + + fun onCloseChoosingWalletClick() + + fun onWalletSelected(walletId: String) +} \ No newline at end of file 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 new file mode 100644 index 0000000000..2c89ed4748 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt @@ -0,0 +1,393 @@ +package com.tangem.managetokens.presentation.managetokens.viewmodels + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.ViewModel +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 +import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.tokens.* +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet +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 +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.plus +import kotlinx.coroutines.withContext +import javax.inject.Inject +import kotlin.collections.set +import kotlin.properties.Delegates + +@Suppress("LargeClass", "LongParameterList", "TooManyFunctions") +@HiltViewModel +internal class ManageTokensViewModel @Inject constructor( + private val dispatchers: CoroutineDispatcherProvider, + private val getGlobalTokenListUseCase: GetGlobalTokenListUseCase, + private val getWalletsUseCase: GetWalletsUseCase, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val removeCurrencyUseCase: RemoveCurrencyUseCase, + private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, + private val selectWalletUseCase: SelectWalletUseCase, + private val getCurrenciesUseCase: GetCryptoCurrenciesUseCase, + private val derivePublicKeysUseCase: DerivePublicKeysUseCase, + private val updateWalletUseCase: UpdateWalletUseCase, + private val fetchTokenListUseCase: FetchTokenListUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, +) : ViewModel(), ManageTokensClickIntents, DefaultLifecycleObserver { + + private val debouncer = Debouncer() + + private val stateFactory = ManageTokensStateFactory( + currentStateProvider = Provider { uiState }, + clickIntents = this, + ) + + var uiState: ManageTokensState by mutableStateOf(stateFactory.getInitialState(flowOf(PagingData.from(emptyList())))) + private set + + private var allAddedCurrencies: MutableList = mutableListOf() + + private var wallets: List by Delegates.notNull() + + private var addedCurrenciesByWallet: MutableMap> = mutableMapOf() + + private var selectedWallet: UserWallet? = null + + private var neededDerivations: Map> = emptyMap() + + private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() + + private val quotesStateConverter = QuotesToQuotesStateConverter() + + private val networkToNetworkItemStateConverter = NetworkToNetworkItemStateConverter( + addedCurrenciesByWalletProvider = Provider { addedCurrenciesByWallet }, + selectedWalletProvider = Provider { selectedWallet }, + onNetworkToggleClick = this::onNetworkToggleClick, + ) + + private val networksToChooseNetworkStateConverter = NetworksToChooseNetworkStateConverter( + networkToNetworkItemStateConverter = networkToNetworkItemStateConverter, + onNonNativeNetworkHintClick = this::onNonNativeNetworkHintClick, + onCloseChooseNetworkScreen = this::onCloseChooseNetworkScreen, + ) + + private val tokenConverter = TokenConverter( + quotesStateConverter = quotesStateConverter, + networksToChooseNetworkStateConverter = networksToChooseNetworkStateConverter, + allAddedCurrencies = Provider { allAddedCurrencies }, + selectedAppCurrency = Provider { selectedAppCurrencyFlow.value }, + onTokenItemButtonClick = this::onTokenItemButtonClick, + ) + + init { + viewModelScope.launch(dispatchers.io) { + getWalletsUseCase() + .distinctUntilChanged() + .collectLatest { userWallets -> + wallets = userWallets.filter { it.isMultiCurrency && !it.isLocked } + wallets.map { wallet -> + val currencies = getCurrenciesUseCase(wallet.walletId).fold( + ifLeft = { emptyList() }, + ifRight = { it }, + ) + allAddedCurrencies += currencies + addedCurrenciesByWallet[wallet] = currencies.toMutableList() + } + withContext(dispatchers.main) { + uiState = uiState.copy(tokens = getInitialTokensList()) + } + selectedWallet = getSelectedWalletSyncUseCase().fold( + ifLeft = { null }, + ifRight = { if (!it.isMultiCurrency || it.isLocked) null else it }, + ) + if (selectedWallet == null && wallets.isNotEmpty()) { + selectWalletUseCase(wallets.first().walletId) + selectedWallet = wallets.first() + } + withContext(dispatchers.main) { + neededDerivations = MissingDerivationsIdentifier + .getMissingDerivationsForWallets(wallets, addedCurrenciesByWallet) + updateDerivationNotificationState() + uiState = stateFactory.updateChooseWalletState(wallets, userWallets, selectedWallet) + } + } + } + } + + private fun getInitialTokensList(searchText: String = ""): Flow> { + return getGlobalTokenListUseCase(searchText = searchText).map { + it.map { token -> tokenConverter.convert(token) } + } + } + + 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() { + 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 createSelectedAppCurrencyFlow(): StateFlow { + return getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + } + + override fun onAddCustomTokensButtonClick() { + TODO("Not yet implemented") // TODO: add when custom tokens are implemented + } + + override fun onSearchQueryChange(query: String) { + uiState = uiState.copy(searchBarState = uiState.searchBarState.copy(query = query)) + + debouncer.debounce(waitMs = 500L, coroutineScope = viewModelScope + dispatchers.io) { + val state = stateFactory.showAddCustomTokensButton(query.isNotBlank()) + uiState = state.copy(tokens = getInitialTokensList(query)) + } + } + + override fun onSearchActiveChange(active: Boolean) { + uiState = uiState.copy(searchBarState = uiState.searchBarState.copy(active = active)) + } + + override fun onTokenItemButtonClick(token: TokenItemState.Loaded) { + 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() + } + } + } + TokenButtonType.NOT_AVAILABLE -> { + uiState = stateFactory.getStateAndTriggerEvent( + state = uiState, + event = Event.ShowAlert( + AlertState.TokenUnavailable( + onUpvoteClick = {}, // TODO later, when endpoint is available + ), + ), + setUiState = { uiState = it }, + ) + } + } + } + + 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) + .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() + } + } + } + } + + override fun onBackClick() { + TODO("Not yet implemented") // TODO: implement if needed when custom tokens and bottom sheet is complete + } + + override fun onCloseChooseNetworkScreen() { + uiState = uiState.copy(selectedToken = null) + } + + override fun onNetworkToggleClick(token: TokenItemState.Loaded, network: NetworkItemState.Toggleable) { + val selectedWallet = selectedWallet ?: return + if (!selectedWallet.isMultiCurrency || selectedWallet.isLocked) return + + 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) + }, + ) + } + } + + private fun toggleToken( + token: TokenItemState.Loaded, + network: NetworkItemState.Toggleable, + selectedWallet: UserWallet, + ) { + val cryptoCurrency = requireNotNull( + TokenToCryptoCurrencyConverter( + network = network, + derivationStyleProvider = selectedWallet.scanResponse.derivationStyleProvider, + ).convert(token), + ) { + "It is only null if Blockchain is Unknown, which mustn't happen here" + } + + if (!network.isAdded.value) { + addedCurrenciesByWallet[selectedWallet]?.add(cryptoCurrency) + allAddedCurrencies.add(cryptoCurrency) + viewModelScope.launch(dispatchers.io) { + addCryptoCurrenciesUseCase(selectedWallet.walletId, cryptoCurrency) + } + } else { + addedCurrenciesByWallet[selectedWallet]?.remove(cryptoCurrency) + allAddedCurrencies.remove(cryptoCurrency) + viewModelScope.launch(dispatchers.io) { + 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) + updateDerivationNotificationState() + } + + override fun onNonNativeNetworkHintClick() { + uiState = stateFactory.getStateAndTriggerEvent( + state = uiState, + event = Event.ShowAlert(AlertState.NonNative), + setUiState = { uiState = it }, + ) + } + + override fun onSelectWalletsClick() { + uiState = uiState.copy( + showChooseWalletScreen = true, + ) + } + + override fun onChooseWalletClick() { + uiState = uiState.copy( + showChooseWalletScreen = true, + ) + } + + override fun onCloseChoosingWalletClick() { + uiState = uiState.copy( + showChooseWalletScreen = false, + ) + } + + override fun onWalletSelected(walletId: String) { + viewModelScope.launch(dispatchers.io) { + selectWalletUseCase(UserWalletId(walletId)) + } + selectedWallet = wallets.find { it.walletId.stringValue == walletId } + uiState.selectedToken?.let { onTokenItemButtonClick(it) } + uiState = stateFactory.updateSelectedWallet(selectedWalletId = selectedWallet?.walletId?.stringValue) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..a6c097b87f --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/MissingDerivationsIdentifier.kt @@ -0,0 +1,95 @@ +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 new file mode 100644 index 0000000000..c0e85efd31 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/TokenCompatibility.kt @@ -0,0 +1,51 @@ +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