From d9f3a8e456d3d21c490a631620751584bc1f9071 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 20 Apr 2026 12:34:01 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../DefaultDynamicAddressesRepository.kt | 61 +++--- domain/dynamic-addresses/build.gradle.kts | 8 + .../DynamicAddressesDerivationChecker.kt | 52 +++++ .../repository/DynamicAddressesRepository.kt | 3 + .../DynamicAddressesDerivationCheckerTest.kt | 203 ++++++++++++++++++ features/manage-tokens/impl/build.gradle.kts | 1 + ...aultCustomTokenDerivationInputComponent.kt | 35 ++- .../DynamicAddressesDerivationValidator.kt | 51 +++++ .../tokendetails/model/TokenDetailsModel.kt | 7 +- 9 files changed, 376 insertions(+), 45 deletions(-) create mode 100644 domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesDerivationChecker.kt create mode 100644 domain/dynamic-addresses/src/test/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesDerivationCheckerTest.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/DynamicAddressesDerivationValidator.kt diff --git a/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultDynamicAddressesRepository.kt b/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultDynamicAddressesRepository.kt index 56ff5bfdb2..1830d9415f 100644 --- a/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultDynamicAddressesRepository.kt +++ b/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultDynamicAddressesRepository.kt @@ -1,11 +1,11 @@ package com.tangem.data.dynamicaddresses -import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.data.common.account.WalletAccountsSaver import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.dynamicaddresses.DynamicAddressesDerivationChecker import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository import com.tangem.domain.models.network.Network @@ -97,34 +97,30 @@ internal class DefaultDynamicAddressesRepository( .flatMap { it.tokens.orEmpty() } .any { token -> val tokenDerivationPath = token.derivationPath ?: return@any false - token.networkId == network.rawId && + token.networkId == network.id.rawId.value && tokenDerivationPath != baseDerivationPath && - hasNonZeroChangeOrIndex(tokenDerivationPath, baseDerivationPath) + DynamicAddressesDerivationChecker.hasSameAccountWithNonZeroChangeOrIndex( + customPath = tokenDerivationPath, + basePath = baseDerivationPath, + ) } } } - /** - * Checks if the token's derivation path has the same first 3 nodes (purpose/coin/account) - * as the base path but different change/index nodes (not both 0). - */ - private fun hasNonZeroChangeOrIndex(tokenPath: String, basePath: String): Boolean { - val tokenNodes = runCatching { DerivationPath(tokenPath).nodes }.getOrNull() ?: return false - val baseNodes = runCatching { DerivationPath(basePath).nodes }.getOrNull() ?: return false - - if (tokenNodes.size < DERIVATION_NODE_COUNT || baseNodes.size < DERIVATION_NODE_COUNT) return false - - // First 3 nodes must match (purpose/coin/account) by value, ignoring hardening - val isSameAccount = (0 until ACCOUNT_NODE_COUNT).all { i -> - tokenNodes[i].getIndex(includeHardened = false) == baseNodes[i].getIndex(includeHardened = false) - } - if (!isSameAccount) return false - - // Check if change or index ≠ 0 - val change = tokenNodes[CHANGE_NODE_INDEX].getIndex(includeHardened = false) - val index = tokenNodes[INDEX_NODE_INDEX].getIndex(includeHardened = false) - - return change != 0L || index != 0L + override fun isDynamicAddressesEnabledForNetwork( + userWalletId: UserWalletId, + networkId: Network.ID, + ): Flow { + return walletAccountsFetcher.get(userWalletId) + .map { response -> + response.accounts + .flatMap { it.tokens.orEmpty() } + .any { token -> + token.matchesNetwork(networkId) && + token.dynamicAddressesEnabled == true + } + } + .flowOn(dispatchers.io) } private suspend fun updateTokenDynamicAddressesFlag( @@ -137,7 +133,7 @@ internal class DefaultDynamicAddressesRepository( accounts = response.accounts.map { account -> account.copy( tokens = account.tokens?.map { token -> - if (token.matchesNetwork(network)) { + if (token.matchesNetwork(network.id)) { token.copy(dynamicAddressesEnabled = enabled) } else { token @@ -157,19 +153,12 @@ internal class DefaultDynamicAddressesRepository( private fun GetWalletAccountsResponse.findToken(network: Network): UserTokensResponse.Token? { return accounts .flatMap { it.tokens.orEmpty() } - .find { it.matchesNetwork(network) } + .find { it.matchesNetwork(network.id) } } - private fun UserTokensResponse.Token.matchesNetwork(network: Network): Boolean { - return networkId == network.rawId && - derivationPath == network.derivationPath.value && + private fun UserTokensResponse.Token.matchesNetwork(networkId: Network.ID): Boolean { + return this.networkId == networkId.rawId.value && + derivationPath == networkId.derivationPath.value && contractAddress == null } - - private companion object { - const val DERIVATION_NODE_COUNT = 5 - const val ACCOUNT_NODE_COUNT = 3 - const val CHANGE_NODE_INDEX = 3 - const val INDEX_NODE_INDEX = 4 - } } \ No newline at end of file diff --git a/domain/dynamic-addresses/build.gradle.kts b/domain/dynamic-addresses/build.gradle.kts index d366a99097..f569b12112 100644 --- a/domain/dynamic-addresses/build.gradle.kts +++ b/domain/dynamic-addresses/build.gradle.kts @@ -8,6 +8,10 @@ android { namespace = "com.tangem.domain.dynamicaddresses" } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { api(projects.domain.core) api(projects.domain.dynamicAddresses.models) @@ -21,4 +25,8 @@ dependencies { exclude(module = "joda-time") } implementation(tangemDeps.card.core) + + testRuntimeOnly(deps.test.junit5.engine) + testImplementation(projects.common.test) + testImplementation(projects.test.core) } \ No newline at end of file diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesDerivationChecker.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesDerivationChecker.kt new file mode 100644 index 0000000000..f2e2bf3486 --- /dev/null +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesDerivationChecker.kt @@ -0,0 +1,52 @@ +package com.tangem.domain.dynamicaddresses + +import com.tangem.crypto.hdWallet.DerivationPath + +/** + * Shared utility for checking if a derivation path conflicts with Dynamic Addresses. + * + * A path conflicts when it belongs to the same BIP44 account as the base path + * (first 3 nodes: purpose / coin_type / account match) but has non-zero + * change (node 3) or address_index (node 4). + */ +object DynamicAddressesDerivationChecker { + + private const val BIP44_NODE_COUNT = 5 + private const val ACCOUNT_NODE_COUNT = 3 + private const val CHANGE_NODE_INDEX = 3 + private const val ADDRESS_INDEX_NODE_INDEX = 4 + + /** + * @return `true` if [path] has zero change (node 3) and zero address_index (node 4). + */ + fun isBaseDerivation(path: String): Boolean { + val nodes = runCatching { DerivationPath(path).nodes }.getOrNull() ?: return false + if (nodes.size < BIP44_NODE_COUNT) return false + + val change = nodes[CHANGE_NODE_INDEX].getIndex(includeHardened = false) + val index = nodes[ADDRESS_INDEX_NODE_INDEX].getIndex(includeHardened = false) + + return change == 0L && index == 0L + } + + /** + * @return `true` if [customPath] shares the same account as [basePath] but has + * non-zero change or address_index nodes. + */ + fun hasSameAccountWithNonZeroChangeOrIndex(customPath: String, basePath: String): Boolean { + val customNodes = runCatching { DerivationPath(customPath).nodes }.getOrNull() ?: return false + val baseNodes = runCatching { DerivationPath(basePath).nodes }.getOrNull() ?: return false + + if (customNodes.size < BIP44_NODE_COUNT || baseNodes.size < BIP44_NODE_COUNT) return false + + val isSameAccount = (0 until ACCOUNT_NODE_COUNT).all { i -> + customNodes[i].getIndex(includeHardened = false) == baseNodes[i].getIndex(includeHardened = false) + } + if (!isSameAccount) return false + + val change = customNodes[CHANGE_NODE_INDEX].getIndex(includeHardened = false) + val index = customNodes[ADDRESS_INDEX_NODE_INDEX].getIndex(includeHardened = false) + + return change != 0L || index != 0L + } +} \ No newline at end of file diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/DynamicAddressesRepository.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/DynamicAddressesRepository.kt index eec289800e..8cdf4b0c12 100644 --- a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/DynamicAddressesRepository.kt +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/DynamicAddressesRepository.kt @@ -22,4 +22,7 @@ interface DynamicAddressesRepository { /** Returns true if there are custom tokens with change/index ≠ 0 that conflict with dynamic addresses */ suspend fun hasConflictingCustomTokens(userWalletId: UserWalletId, network: Network): Boolean + + /** Lightweight check: is the DA flag enabled for the native coin of the given network (no xpub availability check) */ + fun isDynamicAddressesEnabledForNetwork(userWalletId: UserWalletId, networkId: Network.ID): Flow } \ No newline at end of file diff --git a/domain/dynamic-addresses/src/test/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesDerivationCheckerTest.kt b/domain/dynamic-addresses/src/test/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesDerivationCheckerTest.kt new file mode 100644 index 0000000000..e89772ef81 --- /dev/null +++ b/domain/dynamic-addresses/src/test/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesDerivationCheckerTest.kt @@ -0,0 +1,203 @@ +package com.tangem.domain.dynamicaddresses + +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DynamicAddressesDerivationCheckerTest { + + // region Conflicting: same account, non-zero change or index + + @Test + fun `same account, non-zero address index`() { + val result = check(custom = "m/44'/5'/0'/0/1", base = "m/44'/5'/0'/0/0") + assertThat(result).isTrue() + } + + @Test + fun `same account, non-zero change`() { + val result = check(custom = "m/44'/5'/0'/1/0", base = "m/44'/5'/0'/0/0") + assertThat(result).isTrue() + } + + @Test + fun `same account, both change and index non-zero`() { + val result = check(custom = "m/44'/5'/0'/1/5", base = "m/44'/5'/0'/0/0") + assertThat(result).isTrue() + } + + @Test + fun `same account, large address index`() { + val result = check(custom = "m/44'/5'/0'/0/8", base = "m/44'/5'/0'/0/0") + assertThat(result).isTrue() + } + + @Test + fun `bitcoin BIP84, non-zero index`() { + val result = check(custom = "m/84'/0'/0'/0/1", base = "m/84'/0'/0'/0/0") + assertThat(result).isTrue() + } + + @Test + fun `litecoin BIP84, non-zero change`() { + val result = check(custom = "m/84'/2'/0'/1/0", base = "m/84'/2'/0'/0/0") + assertThat(result).isTrue() + } + + @Test + fun `dogecoin, non-zero index`() { + val result = check(custom = "m/44'/3'/0'/0/8", base = "m/44'/3'/0'/0/0") + assertThat(result).isTrue() + } + + @Test + fun `bitcoin cash, non-zero index`() { + val result = check(custom = "m/44'/145'/0'/0/3", base = "m/44'/145'/0'/0/0") + assertThat(result).isTrue() + } + + // endregion + + // region Not conflicting: different account + + @Test + fun `different account index, non-zero address index`() { + val result = check(custom = "m/44'/5'/1'/0/1", base = "m/44'/5'/0'/0/0") + assertThat(result).isFalse() + } + + @Test + fun `different account index, zero change and index`() { + val result = check(custom = "m/44'/5'/1'/0/0", base = "m/44'/5'/0'/0/0") + assertThat(result).isFalse() + } + + @Test + fun `different coin type`() { + val result = check(custom = "m/44'/0'/0'/0/1", base = "m/44'/5'/0'/0/0") + assertThat(result).isFalse() + } + + @Test + fun `different purpose`() { + val result = check(custom = "m/84'/5'/0'/0/1", base = "m/44'/5'/0'/0/0") + assertThat(result).isFalse() + } + + @Test + fun `BIP44 custom against BIP84 base`() { + val result = check(custom = "m/44'/0'/0'/0/1", base = "m/84'/0'/0'/0/0") + assertThat(result).isFalse() + } + + // endregion + + // region Not conflicting: same account, zero change and index + + @Test + fun `identical paths`() { + val result = check(custom = "m/44'/5'/0'/0/0", base = "m/44'/5'/0'/0/0") + assertThat(result).isFalse() + } + + @Test + fun `identical bitcoin BIP84 paths`() { + val result = check(custom = "m/84'/0'/0'/0/0", base = "m/84'/0'/0'/0/0") + assertThat(result).isFalse() + } + + // endregion + + // region Edge cases: invalid or incomplete paths + + @Test + fun `invalid custom path`() { + val result = check(custom = "invalid", base = "m/44'/5'/0'/0/0") + assertThat(result).isFalse() + } + + @Test + fun `invalid base path`() { + val result = check(custom = "m/44'/5'/0'/0/1", base = "not_a_path") + assertThat(result).isFalse() + } + + @Test + fun `empty custom path`() { + val result = check(custom = "", base = "m/44'/5'/0'/0/0") + assertThat(result).isFalse() + } + + @Test + fun `empty base path`() { + val result = check(custom = "m/44'/5'/0'/0/1", base = "") + assertThat(result).isFalse() + } + + @Test + fun `both paths invalid`() { + val result = check(custom = "abc", base = "xyz") + assertThat(result).isFalse() + } + + @Test + fun `custom path with fewer than 5 nodes`() { + val result = check(custom = "m/44'/5'/0'", base = "m/44'/5'/0'/0/0") + assertThat(result).isFalse() + } + + @Test + fun `base path with fewer than 5 nodes`() { + val result = check(custom = "m/44'/5'/0'/0/1", base = "m/44'/5'") + assertThat(result).isFalse() + } + + // endregion + + // region isBaseDerivation + + @Test + fun `isBaseDerivation - standard BIP44 base path`() { + assertThat(DynamicAddressesDerivationChecker.isBaseDerivation("m/44'/5'/0'/0/0")).isTrue() + } + + @Test + fun `isBaseDerivation - BIP84 base path`() { + assertThat(DynamicAddressesDerivationChecker.isBaseDerivation("m/84'/0'/0'/0/0")).isTrue() + } + + @Test + fun `isBaseDerivation - non-zero index`() { + assertThat(DynamicAddressesDerivationChecker.isBaseDerivation("m/44'/5'/0'/0/1")).isFalse() + } + + @Test + fun `isBaseDerivation - non-zero change`() { + assertThat(DynamicAddressesDerivationChecker.isBaseDerivation("m/44'/5'/0'/1/0")).isFalse() + } + + @Test + fun `isBaseDerivation - non-zero account with zero change and index`() { + assertThat(DynamicAddressesDerivationChecker.isBaseDerivation("m/44'/5'/2'/0/0")).isTrue() + } + + @Test + fun `isBaseDerivation - invalid path`() { + assertThat(DynamicAddressesDerivationChecker.isBaseDerivation("invalid")).isFalse() + } + + @Test + fun `isBaseDerivation - too few nodes`() { + assertThat(DynamicAddressesDerivationChecker.isBaseDerivation("m/44'/5'")).isFalse() + } + + // endregion + + private fun check(custom: String, base: String): Boolean { + return DynamicAddressesDerivationChecker.hasSameAccountWithNonZeroChangeOrIndex( + customPath = custom, + basePath = base, + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 867d3f035c..5c8bc1a2ed 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.swap.models) implementation(projects.domain.notifications) + implementation(projects.domain.dynamicAddresses) // region Project - Libs implementation(projects.libs.blockchainSdk) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenDerivationInputComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenDerivationInputComponent.kt index adef3798eb..cb0043af8a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenDerivationInputComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenDerivationInputComponent.kt @@ -7,8 +7,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import arrow.core.getOrElse import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles +import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository import com.tangem.domain.managetokens.ValidateDerivationPathUseCase -import com.tangem.features.managetokens.utils.CardanoDerivationPathValidator import com.tangem.domain.managetokens.model.exceptoin.DerivationPathValidationException import com.tangem.domain.models.network.Network import com.tangem.features.managetokens.component.CustomTokenDerivationInputComponent @@ -16,9 +17,12 @@ import com.tangem.features.managetokens.entity.customtoken.CustomDerivationInput import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.ui.dialog.CustomDerivationInputDialog +import com.tangem.features.managetokens.utils.CardanoDerivationPathValidator +import com.tangem.features.managetokens.utils.DynamicAddressesDerivationValidator import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* @@ -26,9 +30,15 @@ internal class DefaultCustomTokenDerivationInputComponent @AssistedInject constr @Assisted context: AppComponentContext, @Assisted private val params: CustomTokenDerivationInputComponent.Params, private val validateDerivationPathUseCase: ValidateDerivationPathUseCase, + private val dynamicAddressesRepository: DynamicAddressesRepository, + private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles, ) : CustomTokenDerivationInputComponent, AppComponentContext by context { private val cardanoDerivationPathValidator = CardanoDerivationPathValidator() + private val dynamicAddressesDerivationValidator = DynamicAddressesDerivationValidator( + dynamicAddressesRepository = dynamicAddressesRepository, + dynamicAddressesFeatureToggles = dynamicAddressesFeatureToggles, + ) private val state: MutableStateFlow = MutableStateFlow( value = getInitialState(), @@ -52,13 +62,15 @@ internal class DefaultCustomTokenDerivationInputComponent @AssistedInject constr ) } - @OptIn(FlowPreview::class) + @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) private fun observeValueUpdates() { state .map { it.value.text } .distinctUntilChanged() .sample(periodMillis = 1_000) - .onEach(::validateValue) + .flatMapLatest { value -> + flow { emit(validateValue(value)) } + } .launchIn(componentScope) } @@ -70,7 +82,7 @@ internal class DefaultCustomTokenDerivationInputComponent @AssistedInject constr onConfirm = ::confirm, ) - private fun validateValue(value: String) { + private suspend fun validateValue(value: String) { validateDerivationPathUseCase(value).getOrElse { e -> updateWithValidationError(e) return @@ -90,6 +102,21 @@ internal class DefaultCustomTokenDerivationInputComponent @AssistedInject constr return } + val isInvalidForDA = dynamicAddressesDerivationValidator.isInvalidForDynamicAddresses( + userWalletId = params.mode.userWalletId, + networkId = params.selectedNetwork.id, + path = value, + ) + if (isInvalidForDA) { + state.update { state -> + state.copy( + error = resourceReference(R.string.dynamic_addresses_custom_token_error_on_addition), + isConfirmEnabled = false, + ) + } + return + } + state.update { state -> state.copy( error = null, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/DynamicAddressesDerivationValidator.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/DynamicAddressesDerivationValidator.kt new file mode 100644 index 0000000000..a7f0a8a514 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/DynamicAddressesDerivationValidator.kt @@ -0,0 +1,51 @@ +package com.tangem.features.managetokens.utils + +import com.tangem.blockchainsdk.utils.toBlockchain +import com.tangem.domain.dynamicaddresses.DynamicAddressesDerivationChecker +import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles +import com.tangem.domain.dynamicaddresses.DynamicAddressesSupportedBlockchains +import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.firstOrNull + +/** + * Validates that custom derivation paths don't conflict with Dynamic Addresses. + * + * When DA is enabled for an account, custom derivation paths with non-zero + * change (node 3) or address_index (node 4) are forbidden, because DA + * manages those nodes automatically. + */ +internal class DynamicAddressesDerivationValidator( + private val dynamicAddressesRepository: DynamicAddressesRepository, + private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles, +) { + + /** + * @return true if the path is invalid (DA is enabled for the same account and change/index ≠ 0) + */ + suspend fun isInvalidForDynamicAddresses( + userWalletId: UserWalletId, + networkId: Network.ID, + path: String?, + ): Boolean { + if (!dynamicAddressesFeatureToggles.isDynamicAddressesEnabled) return false + if (path == null) return false + if (!isSupportedBlockchain(networkId)) return false + + val basePath = networkId.derivationPath.value ?: return false + val isEnabled = dynamicAddressesRepository + .isDynamicAddressesEnabledForNetwork(userWalletId, networkId) + .firstOrNull() == true + if (!isEnabled) return false + + return DynamicAddressesDerivationChecker.hasSameAccountWithNonZeroChangeOrIndex( + customPath = path, + basePath = basePath, + ) + } + + private fun isSupportedBlockchain(networkId: Network.ID): Boolean { + return DynamicAddressesSupportedBlockchains.isSupported(networkId.toBlockchain()) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 2a3f0c895f..d7f1bb3ec4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -9,6 +9,7 @@ import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.dismiss import com.tangem.blockchain.common.address.AddressType import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.dynamicaddresses.DynamicAddressesDerivationChecker import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles import com.tangem.domain.dynamicaddresses.DynamicAddressesSupportedBlockchains import com.tangem.domain.dynamicaddresses.IsXpubSupportedUseCase @@ -551,11 +552,7 @@ internal class TokenDetailsModel @Inject constructor( val allowedPurpose = DynamicAddressesSupportedBlockchains.getAllowedPurpose(networkId) ?: return false if (purposeNode.getIndex(includeHardened = false) != allowedPurpose) return false - val changeNode = nodes[nodes.size - 2] - val indexNode = nodes.last() - - return changeNode.getIndex(includeHardened = false) == 0L && - indexNode.getIndex(includeHardened = false) == 0L + return DynamicAddressesDerivationChecker.isBaseDerivation(pathValue) } private suspend fun isXPUBSupported(): Boolean {