Updated on 2026-08-14
This commit is contained in:
parent
53a84a6aa8
commit
d9f3a8e456
9 changed files with 376 additions and 45 deletions
|
|
@ -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<Boolean> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,10 @@ android {
|
|||
namespace = "com.tangem.domain.dynamicaddresses"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Boolean>
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<CustomDerivationInputUM> = 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,
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue