From 24c6425299fdb319084384785abccef4c76c5b0f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Mar 2024 20:30:58 +0100 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 12 +- .../viewmodels/AddCustomTokenViewModel.kt | 3 +- .../DefaultNetworksCompatibilityRepository.kt | 11 ++ .../RequiresHardenedDerivationOnlyUseCase.kt | 26 ++++ .../NetworksCompatibilityRepository.kt | 2 + .../factory/AddCustomTokenStateFactory.kt | 125 +++++++++++++----- .../viewmodels/AddCustomTokenViewModel.kt | 45 ++++--- 7 files changed, 170 insertions(+), 54 deletions(-) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RequiresHardenedDerivationOnlyUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index c67960a860..7c830de869 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -278,9 +278,17 @@ internal object TokensDomainModule { @Provides @ViewModelScoped fun provideCheckTokenCompatibilityUseCase( - repository: NetworksCompatibilityRepository, + networksCompatibilityRepository: NetworksCompatibilityRepository, ): CheckCurrencyCompatibilityUseCase { - return CheckCurrencyCompatibilityUseCase(repository) + return CheckCurrencyCompatibilityUseCase(networksCompatibilityRepository) + } + + @Provides + @ViewModelScoped + fun provideNeedHardenedDerivationUseCase( + networksCompatibilityRepository: NetworksCompatibilityRepository, + ): RequiresHardenedDerivationOnlyUseCase { + return RequiresHardenedDerivationOnlyUseCase(networksCompatibilityRepository) } @Provides diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index b15a5fdee5..4efb2848fc 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -15,7 +15,6 @@ import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.crypto.hdWallet.HDWalletError import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.extensions.* import com.tangem.domain.common.util.cardTypesResolver @@ -674,7 +673,7 @@ internal class AddCustomTokenViewModel @Inject constructor( private fun createDerivationPathOrNull(rawPath: String): DerivationPath? { return try { DerivationPath(rawPath) - } catch (error: HDWalletError) { + } catch (error: Throwable) { null } } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt index f17fe671b9..006ca60bb9 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt @@ -1,8 +1,10 @@ package com.tangem.data.tokens.repository import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve import com.tangem.data.tokens.utils.getNetwork import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.extensions.* import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider @@ -71,6 +73,15 @@ internal class DefaultNetworksCompatibilityRepository( } } + override suspend fun requiresHardenedDerivationOnly(networkId: String, userWalletId: UserWalletId): Boolean { + val scanResponse = getWalletOrThrow(userWalletId).scanResponse + val config = CardConfig.createConfig(scanResponse.card) + val blockchain = Blockchain.fromNetworkId(networkId) ?: return false + + return config.primaryCurve(blockchain) == EllipticCurve.Ed25519Slip0010 && + scanResponse.cardTypesResolver.isWallet2() + } + override fun areTokensSupportedByNetwork(networkId: String): Boolean { return Blockchain.fromNetworkId(networkId)?.canHandleTokens() ?: false } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RequiresHardenedDerivationOnlyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RequiresHardenedDerivationOnlyUseCase.kt new file mode 100644 index 0000000000..db8dad152a --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RequiresHardenedDerivationOnlyUseCase.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.tokens.repository.NetworksCompatibilityRepository +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Ed25519Slip0010 requires only hardened derivations on Wallet 2 + */ +class RequiresHardenedDerivationOnlyUseCase( + private val repository: NetworksCompatibilityRepository, +) { + + suspend operator fun invoke(networkId: String, userWalletId: UserWalletId): Either { + return either { + catch( + block = { + repository.requiresHardenedDerivationOnly(networkId, userWalletId) + }, + catch = { throwable -> raise(throwable) }, + ) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt index 1d0f6946b1..a5e2083e05 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt @@ -17,5 +17,7 @@ interface NetworksCompatibilityRepository { @Throws(IllegalArgumentException::class) suspend fun getSupportedNetworks(userWalletId: UserWalletId): List + suspend fun requiresHardenedDerivationOnly(networkId: String, userWalletId: UserWalletId): Boolean + fun areTokensSupportedByNetwork(networkId: String): Boolean } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt index 7fe3b1dc8c..6ad1ac8e68 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt @@ -2,6 +2,7 @@ package com.tangem.managetokens.presentation.addcustomtoken.state.factory import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent +import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.tokens.error.AddCustomTokenError import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet @@ -15,6 +16,7 @@ import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentSet +@Suppress("LargeClass") internal class AddCustomTokenStateFactory( private val currentStateProvider: Provider, private val clickIntents: AddCustomTokenClickIntents, @@ -72,8 +74,11 @@ internal class AddCustomTokenStateFactory( ) } - private fun getListOfDerivations(supportedNetworks: List): List { - return supportedNetworks.mapNotNull { network -> + private fun getListOfDerivations( + networksListToGenerateDerivations: List, + filterOnlyHardenedDerivations: Boolean = false, + ): List { + return networksListToGenerateDerivations.mapNotNull { network -> network.derivationPath.value?.let { rawPath -> Derivation( networkName = network.name, @@ -82,6 +87,12 @@ internal class AddCustomTokenStateFactory( networkId = network.backendId, onDerivationSelected = clickIntents::onDerivationSelected, ) + }.takeIf { derivation -> + if (filterOnlyHardenedDerivations) { + derivation?.let { allNodesHardened(createDerivationPathOrNull(it.path)) } ?: false + } else { + true + } } } } @@ -218,41 +229,52 @@ internal class AddCustomTokenStateFactory( ) } - fun updateStateOnNetworkSelected(networkItemState: NetworkItemState, supportsTokens: Boolean): AddCustomTokenState { + fun updateStateOnNetworkSelected( + networkItemState: NetworkItemState, + supportsTokens: Boolean, + networks: List, + requiresHardenedDerivationOnly: Boolean, + ): AddCustomTokenState { val uiState = currentStateProvider() val tokenData = if (supportsTokens) { - uiState.tokenData - ?: CustomTokenData( - contractAddressTextField = TextFieldState.Editable( - value = "", - isEnabled = true, - onValueChange = clickIntents::onContractAddressChange, - onFocusExit = clickIntents::onContractAddressFocusExit, - ), - nameTextField = TextFieldState.Editable( - value = "", - isEnabled = false, - onValueChange = clickIntents::onTokenNameChange, - onFocusExit = clickIntents::onTokenNameFocusExit, - ), - symbolTextField = TextFieldState.Editable( - value = "", - isEnabled = false, - onValueChange = clickIntents::onSymbolChange, - onFocusExit = clickIntents::onSymbolFocusExit, - ), - decimalsTextField = TextFieldState.Editable( - value = "", - isEnabled = false, - onValueChange = clickIntents::onDecimalsChange, - onFocusExit = clickIntents::onDecimalsFocusExit, - ), - ) + uiState.tokenData ?: CustomTokenData( + contractAddressTextField = TextFieldState.Editable( + value = "", + isEnabled = true, + onValueChange = clickIntents::onContractAddressChange, + onFocusExit = clickIntents::onContractAddressFocusExit, + ), + nameTextField = TextFieldState.Editable( + value = "", + isEnabled = false, + onValueChange = clickIntents::onTokenNameChange, + onFocusExit = clickIntents::onTokenNameFocusExit, + ), + symbolTextField = TextFieldState.Editable( + value = "", + isEnabled = false, + onValueChange = clickIntents::onSymbolChange, + onFocusExit = clickIntents::onSymbolFocusExit, + ), + decimalsTextField = TextFieldState.Editable( + value = "", + isEnabled = false, + onValueChange = clickIntents::onDecimalsChange, + onFocusExit = clickIntents::onDecimalsFocusExit, + ), + ) } else { null } + + val derivations = getListOfDerivations(networks, requiresHardenedDerivationOnly) + val chooseDerivationState = createChooseDerivationState(derivations) + return uiState.copy( - chooseNetworkState = uiState.chooseNetworkState.copy(selectedNetwork = networkItemState), + chooseNetworkState = uiState.chooseNetworkState.copy( + selectedNetwork = networkItemState, + ), + chooseDerivationState = chooseDerivationState, tokenData = tokenData, addTokenButton = uiState.addTokenButton.copy(isEnabled = true), ) @@ -291,6 +313,47 @@ internal class AddCustomTokenStateFactory( ) } + fun updateOnCustomDerivationEntered(input: String, requiresHardenedDerivationOnly: Boolean): AddCustomTokenState { + val uiState = currentStateProvider() + val path = createDerivationPathOrNull(input) + val isWrongDerivationForWallet2 = isWrongDerivationForWallet2( + requiresHardenedDerivationOnly = requiresHardenedDerivationOnly, + derivationPath = path, + ) + val enterDerivationState = uiState.chooseDerivationState?.enterCustomDerivationState?.copy( + confirmButtonEnabled = path != null && !isWrongDerivationForWallet2, + derivationIncorrect = input.isNotBlank() && path == null || isWrongDerivationForWallet2, + ) + return uiState.copy( + chooseDerivationState = uiState.chooseDerivationState?.copy( + enterCustomDerivationState = enterDerivationState, + ), + ) + } + + private fun isWrongDerivationForWallet2( + requiresHardenedDerivationOnly: Boolean, + derivationPath: DerivationPath?, + ): Boolean { + return if (requiresHardenedDerivationOnly) { + !allNodesHardened(derivationPath) + } else { + false + } + } + + private fun allNodesHardened(derivationPath: DerivationPath?): Boolean { + return derivationPath?.nodes?.all { it.isHardened } ?: false + } + + private fun createDerivationPathOrNull(rawPath: String): DerivationPath? { + return try { + DerivationPath(rawPath) + } catch (error: Throwable) { + null + } + } + fun updateStateOnLoadingTokenInfo(contractAddress: String): AddCustomTokenState { return currentStateProvider().copy( tokenData = CustomTokenData( diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt index 1b107076ea..44e55b947d 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt @@ -9,8 +9,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.crypto.hdWallet.HDWalletError import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.* import com.tangem.domain.tokens.model.CryptoCurrency @@ -56,6 +54,7 @@ internal class AddCustomTokenViewModel @Inject constructor( private val validateContractAddressUseCase: ValidateContractAddressUseCase, private val getNetworksSupportedByWallet: GetNetworksSupportedByWallet, private val areTokensSupportedByNetworkUseCase: AreTokensSupportedByNetworkUseCase, + private val requiresHardenedDerivationOnlyUseCase: RequiresHardenedDerivationOnlyUseCase, private val analyticsEventHandler: AnalyticsEventHandler, ) : ViewModel(), AddCustomTokenClickIntents, DefaultLifecycleObserver { @@ -135,13 +134,25 @@ internal class AddCustomTokenViewModel @Inject constructor( private fun selectNetwork(networkItemState: NetworkItemState) { viewModelScope.launch(dispatchers.io) { - val selectedWalletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId + // TODO [REDACTED_TASK_KEY] + val selectedWalletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId ?: return@launch val supportsTokens = areTokensSupportedByNetworkUseCase( networkId = networkItemState.id, userWalletId = selectedWalletId, ).getOrNull() ?: false + + val networksForDerivations = getSupportedNetworks(selectedWalletId) + withContext(dispatchers.main) { - uiState = stateFactory.updateStateOnNetworkSelected(networkItemState, supportsTokens) + uiState = stateFactory.updateStateOnNetworkSelected( + networkItemState = networkItemState, + supportsTokens = supportsTokens, + networks = networksForDerivations, + requiresHardenedDerivationOnly = requiresHardenedDerivationOnly( + networkId = networkItemState.id, + userWalletId = selectedWalletId, + ), + ) } } } @@ -347,6 +358,8 @@ internal class AddCustomTokenViewModel @Inject constructor( } override fun onCustomDerivationChange(input: String) { + val selectedWallet = getSelectedWalletSyncUseCase().getOrNull() ?: return + analyticsEventHandler.send(ManageTokens.CustomTokenDerivationSelected(ManageTokens.Derivation.CUSTOM.value)) uiState = uiState.copy( chooseDerivationState = uiState.chooseDerivationState?.copy( @@ -356,25 +369,19 @@ internal class AddCustomTokenViewModel @Inject constructor( ), ) debouncer.debounce(waitMs = DEFAULT_WAIT_TIME_MS, coroutineScope = viewModelScope + dispatchers.io) { - val path = createDerivationPathOrNull(input) - val enterDerivationState = uiState.chooseDerivationState?.enterCustomDerivationState?.copy( - confirmButtonEnabled = path != null, - derivationIncorrect = input.isNotBlank() && path == null, - ) - uiState = uiState.copy( - chooseDerivationState = uiState.chooseDerivationState?.copy( - enterCustomDerivationState = enterDerivationState, - ), + val networkId = uiState.chooseNetworkState.selectedNetwork?.id ?: return@debounce + uiState = stateFactory.updateOnCustomDerivationEntered( + input = input, + requiresHardenedDerivationOnly = requiresHardenedDerivationOnly(networkId, selectedWallet.walletId), ) } } - private fun createDerivationPathOrNull(rawPath: String): DerivationPath? { - return try { - DerivationPath(rawPath) - } catch (error: HDWalletError) { - null - } + private suspend fun requiresHardenedDerivationOnly(networkId: String, userWalletId: UserWalletId): Boolean { + return requiresHardenedDerivationOnlyUseCase.invoke( + networkId = networkId, + userWalletId = userWalletId, + ).getOrElse { false } } override fun onCustomDerivationSelected() {