Updated on 2026-08-14

This commit is contained in:
Tangem 2023-12-19 13:45:31 +03:00
parent aff4704274
commit 185e5b5033
24 changed files with 547 additions and 348 deletions

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -102,4 +102,13 @@ internal object TokensDataModule {
quotesRepository = quotesRepository,
)
}
@Provides
@Singleton
fun provideCardNetworksRepository(
userWalletsStore: UserWalletsStore,
dispatchers: CoroutineDispatcherProvider,
): NetworksCompatibilityRepository {
return DefaultNetworksCompatibilityRepository(userWalletsStore = userWalletsStore, dispatchers = dispatchers)
}
}

View file

@ -32,6 +32,7 @@ internal object CoinsResponseConverter : Converter<CoinsData, List<Token>> {
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(),

View file

@ -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"
}
}
}

View file

@ -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,
)
}

View file

@ -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?,

View file

@ -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<CurrencyCompatibilityError, Unit> {
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()
}

View file

@ -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<Either<GetCurrenciesError, List<CryptoCurrency>>> {
return currenciesRepository.getMissedAddressesCryptoCurrencies(userWalletId)
.map<List<CryptoCurrency>, Either<GetCurrenciesError, List<CryptoCurrency>>>(List<CryptoCurrency>::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<UserWalletId>,
): Flow<Either<GetCurrenciesError, Map<UserWalletId, List<CryptoCurrency>>>> {
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()) }
}
}

View file

@ -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
}

View file

@ -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)

View file

@ -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),
)
}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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,
),
),
)

View file

@ -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
}
}
}
}

View file

@ -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)
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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,
)

View file

@ -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,
),

View file

@ -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,

View file

@ -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 }
}

View file

@ -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()
}
}