Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-20 12:34:01 +03:00
parent 53a84a6aa8
commit d9f3a8e456
9 changed files with 376 additions and 45 deletions

View file

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

View file

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

View file

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