Updated on 2026-08-14
This commit is contained in:
parent
aff4704274
commit
185e5b5033
24 changed files with 547 additions and 348 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Int>,
|
||||
val isMainNetwork: Boolean,
|
||||
val isAdded: MutableState<Boolean>,
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Netw
|
|||
id = "",
|
||||
address = "",
|
||||
onToggleClick = { _, _ -> },
|
||||
blockchain = Blockchain.Ethereum,
|
||||
decimals = 0,
|
||||
),
|
||||
NetworkItemState.Toggleable(
|
||||
|
|
@ -142,7 +140,6 @@ private class NetworkItemStateProvider : CollectionPreviewParameterProvider<Netw
|
|||
id = "",
|
||||
address = "",
|
||||
onToggleClick = { _, _ -> },
|
||||
blockchain = Blockchain.BSC,
|
||||
decimals = 0,
|
||||
),
|
||||
NetworkItemState.Selectable(
|
||||
|
|
@ -151,7 +148,6 @@ private class NetworkItemStateProvider : CollectionPreviewParameterProvider<Netw
|
|||
iconResId = R.drawable.img_polygon_22,
|
||||
id = "",
|
||||
onNetworkClick = { },
|
||||
blockchain = Blockchain.Ethereum,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.managetokens.presentation.common.utils
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
||||
internal object CurrencyUtils {
|
||||
fun isAdded(address: String?, networkId: String, currencies: Collection<CryptoCurrency>): 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CryptoCurrency>,
|
||||
) {
|
||||
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<NetworkItemState>,
|
||||
allAddedCurrencies: Collection<CryptoCurrency>,
|
||||
): 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<CryptoCurrency>,
|
||||
) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CryptoCurrency>): 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CryptoCurrency>): 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TokenItemState.Loaded, CryptoCurrency?> {
|
||||
|
||||
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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<UserWallet, List<DerivationData>> = emptyMap()
|
||||
private var neededDerivations: Map<UserWalletId, List<CryptoCurrency>> = emptyMap()
|
||||
|
||||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = 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<CryptoCurrency>): 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<AppCurrency> {
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<UserWallet>,
|
||||
addedCurrenciesByWallet: MutableMap<UserWallet, MutableList<CryptoCurrency>>,
|
||||
): Map<UserWallet, List<DerivationData>> {
|
||||
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<DerivationData>,
|
||||
): List<DerivationData> {
|
||||
return buildMap<ByteArrayKey, MutableList<DerivationPath>> {
|
||||
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<ByteArrayKey, List<DerivationPath>>) {
|
||||
fun derivationsNeeded(): Int {
|
||||
return derivations.second.size
|
||||
}
|
||||
}
|
||||
|
||||
internal fun List<DerivationData>.toMapOfDerivations(): Map<ByteArrayKey, List<DerivationPath>> {
|
||||
return associate { it.derivations }
|
||||
}
|
||||
|
|
@ -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<AddTokenError, Unit> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue