Updated on 2026-08-14
This commit is contained in:
parent
51c396a5d7
commit
6c82e6e484
17 changed files with 1001 additions and 14 deletions
|
|
@ -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<ManageTokensViewModel>()
|
||||
val viewModel = hiltViewModel<ManageTokensViewModel>()
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<ManageTokensState>,
|
||||
private val clickIntents: ManageTokensClickIntents,
|
||||
) {
|
||||
|
||||
fun getInitialState(tokens: Flow<PagingData<TokenItemState>>): 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<UserWallet>,
|
||||
userWallets: List<UserWallet>,
|
||||
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()))
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<MutableMap<UserWallet, MutableList<CryptoCurrency>>>,
|
||||
private val selectedWalletProvider: Provider<UserWallet?>,
|
||||
private val onNetworkToggleClick: (token: TokenItemState.Loaded, network: NetworkItemState.Toggleable) -> Unit,
|
||||
) : Converter<Token.Network, NetworkItemState> {
|
||||
|
||||
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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<Token.Network>, ChooseNetworkState> {
|
||||
|
||||
override fun convert(value: List<Token.Network>): ChooseNetworkState {
|
||||
return createChooseNetworksState(value)
|
||||
}
|
||||
|
||||
private fun createChooseNetworksState(networks: List<Token.Network>): ChooseNetworkState {
|
||||
val nativeNetworks = mutableListOf<NetworkItemState>()
|
||||
val nonNativeNetworks = mutableListOf<NetworkItemState>()
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Quote, QuotesState> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<CryptoCurrency>>,
|
||||
private val selectedAppCurrency: Provider<AppCurrency>,
|
||||
private val onTokenItemButtonClick: (TokenItemState.Loaded) -> Unit,
|
||||
) : Converter<Token, TokenItemState.Loaded> {
|
||||
|
||||
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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<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) {
|
||||
CryptoCurrencyFactory().createToken(
|
||||
sdkToken = sdkToken,
|
||||
blockchain = blockchain,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
extraDerivationPath = null,
|
||||
)
|
||||
} else {
|
||||
CryptoCurrencyFactory().createCoin(
|
||||
blockchain = blockchain,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
extraDerivationPath = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -40,7 +40,11 @@ internal fun TokensList(tokens: LazyPagingItems<TokenItemState>, 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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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<CryptoCurrency> = mutableListOf()
|
||||
|
||||
private var wallets: List<UserWallet> by Delegates.notNull()
|
||||
|
||||
private var addedCurrenciesByWallet: MutableMap<UserWallet, MutableList<CryptoCurrency>> = mutableMapOf()
|
||||
|
||||
private var selectedWallet: UserWallet? = null
|
||||
|
||||
private var neededDerivations: Map<UserWallet, List<DerivationData>> = emptyMap()
|
||||
|
||||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = 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<PagingData<TokenItemState>> {
|
||||
return getGlobalTokenListUseCase(searchText = searchText).map {
|
||||
it.map { token -> tokenConverter.convert(token) }
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
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<AppCurrency> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<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 }
|
||||
}
|
||||
|
|
@ -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<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