Updated on 2026-08-14
This commit is contained in:
parent
adb8c50ae2
commit
dd95bf238a
7 changed files with 189 additions and 0 deletions
|
|
@ -56,6 +56,14 @@ internal object ManageTokensDomainModule {
|
||||||
return ValidateDerivationPathUseCase(customTokensRepository)
|
return ValidateDerivationPathUseCase(customTokensRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideCheckDerivationPathSupportedUseCase(
|
||||||
|
customTokensRepository: CustomTokensRepository,
|
||||||
|
): CheckDerivationPathSupportedUseCase {
|
||||||
|
return CheckDerivationPathSupportedUseCase(customTokensRepository)
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideCheckCurrencyUnsupportedUseCase(repository: ManageTokensRepository): CheckCurrencyUnsupportedUseCase {
|
fun provideCheckCurrencyUnsupportedUseCase(repository: ManageTokensRepository): CheckCurrencyUnsupportedUseCase {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.network.Network
|
import com.tangem.domain.models.network.Network
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.domain.wallets.config.curvesConfig
|
||||||
|
import com.tangem.lib.crypto.derivation.supportsDerivationPath
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
|
@ -217,6 +219,20 @@ internal class DefaultCustomTokensRepository(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun isDerivationPathSupported(
|
||||||
|
userWalletId: UserWalletId,
|
||||||
|
networkId: Network.ID,
|
||||||
|
derivationPath: Network.DerivationPath,
|
||||||
|
): Boolean = withContext(dispatchers.io) {
|
||||||
|
val rawPath = derivationPath.value ?: return@withContext true
|
||||||
|
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
|
||||||
|
val blockchain = networkId.toBlockchain()
|
||||||
|
val curve = userWallet.curvesConfig.primaryCurve(blockchain) ?: return@withContext false
|
||||||
|
val path = runCatching { DerivationPath(rawPath) }.getOrNull() ?: return@withContext false
|
||||||
|
|
||||||
|
curve.supportsDerivationPath(path)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getSupportedNetworks(userWalletId: UserWalletId): List<Network> = withContext(dispatchers.io) {
|
override suspend fun getSupportedNetworks(userWalletId: UserWalletId): List<Network> = withContext(dispatchers.io) {
|
||||||
when (val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)) {
|
when (val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)) {
|
||||||
is UserWallet.Hot -> {
|
is UserWallet.Hot -> {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.tangem.domain.managetokens
|
||||||
|
|
||||||
|
import arrow.core.Either
|
||||||
|
import arrow.core.left
|
||||||
|
import arrow.core.right
|
||||||
|
import com.tangem.domain.managetokens.repository.CustomTokensRepository
|
||||||
|
import com.tangem.domain.models.network.Network
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.utils.coroutines.runSuspendCatching
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the elliptic curve used by [networkId] for the [userWalletId] wallet supports the selected
|
||||||
|
* [derivationPath]. Used before adding a custom token to prevent adding a token with a derivation path that the
|
||||||
|
* network's curve cannot derive (e.g. an Algorand token with an EVM derivation path).
|
||||||
|
*
|
||||||
|
* Returns [Either.Right] with `true` when the derivation is supported, `false` otherwise. [Either.Left] is returned
|
||||||
|
* when the check itself fails unexpectedly.
|
||||||
|
*/
|
||||||
|
class CheckDerivationPathSupportedUseCase(
|
||||||
|
private val customTokensRepository: CustomTokensRepository,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend operator fun invoke(
|
||||||
|
userWalletId: UserWalletId,
|
||||||
|
networkId: Network.ID,
|
||||||
|
derivationPath: Network.DerivationPath,
|
||||||
|
): Either<Throwable, Boolean> = runSuspendCatching {
|
||||||
|
customTokensRepository.isDerivationPathSupported(
|
||||||
|
userWalletId = userWalletId,
|
||||||
|
networkId = networkId,
|
||||||
|
derivationPath = derivationPath,
|
||||||
|
)
|
||||||
|
}.fold(
|
||||||
|
onSuccess = { it.right() },
|
||||||
|
onFailure = { it.left() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -43,5 +43,11 @@ interface CustomTokensRepository {
|
||||||
|
|
||||||
suspend fun getSupportedNetworks(userWalletId: UserWalletId): List<Network>
|
suspend fun getSupportedNetworks(userWalletId: UserWalletId): List<Network>
|
||||||
|
|
||||||
|
suspend fun isDerivationPathSupported(
|
||||||
|
userWalletId: UserWalletId,
|
||||||
|
networkId: Network.ID,
|
||||||
|
derivationPath: Network.DerivationPath,
|
||||||
|
): Boolean
|
||||||
|
|
||||||
fun createDerivationPath(rawPath: String): Network.DerivationPath
|
fun createDerivationPath(rawPath: String): Network.DerivationPath
|
||||||
}
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.resourceReference
|
||||||
import com.tangem.core.ui.extensions.stringReference
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
import com.tangem.core.ui.extensions.wrappedList
|
import com.tangem.core.ui.extensions.wrappedList
|
||||||
import com.tangem.core.ui.message.DialogMessage
|
import com.tangem.core.ui.message.DialogMessage
|
||||||
|
import com.tangem.domain.managetokens.CheckDerivationPathSupportedUseCase
|
||||||
import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase
|
import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase
|
||||||
import com.tangem.domain.managetokens.FindTokenUseCase
|
import com.tangem.domain.managetokens.FindTokenUseCase
|
||||||
import com.tangem.domain.managetokens.ValidateTokenFormUseCase
|
import com.tangem.domain.managetokens.ValidateTokenFormUseCase
|
||||||
|
|
@ -51,6 +52,7 @@ internal class CustomTokenFormModel @Inject constructor(
|
||||||
private val createCryptoCurrencyUseCase: CreateCryptoCurrencyUseCase,
|
private val createCryptoCurrencyUseCase: CreateCryptoCurrencyUseCase,
|
||||||
private val findTokenUseCase: FindTokenUseCase,
|
private val findTokenUseCase: FindTokenUseCase,
|
||||||
private val validateTokenFormUseCase: ValidateTokenFormUseCase,
|
private val validateTokenFormUseCase: ValidateTokenFormUseCase,
|
||||||
|
private val checkDerivationPathSupportedUseCase: CheckDerivationPathSupportedUseCase,
|
||||||
paramsContainer: ParamsContainer,
|
paramsContainer: ParamsContainer,
|
||||||
customTokenFormUseCasesFacadeFactory: CustomTokenFormUseCasesFacade.Factory,
|
customTokenFormUseCasesFacadeFactory: CustomTokenFormUseCasesFacade.Factory,
|
||||||
) : Model() {
|
) : Model() {
|
||||||
|
|
@ -238,6 +240,14 @@ internal class CustomTokenFormModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showDerivationPathNotSupportedDialog() {
|
||||||
|
val dialog = DialogMessage(
|
||||||
|
message = resourceReference(R.string.custom_token_invalid_derivation_path),
|
||||||
|
)
|
||||||
|
|
||||||
|
messageSender.send(dialog)
|
||||||
|
}
|
||||||
|
|
||||||
private fun showErrorDialog(throwable: Throwable) {
|
private fun showErrorDialog(throwable: Throwable) {
|
||||||
TangemLogger.e("Error", throwable)
|
TangemLogger.e("Error", throwable)
|
||||||
val message = when (throwable) {
|
val message = when (throwable) {
|
||||||
|
|
@ -355,6 +365,19 @@ internal class CustomTokenFormModel @Inject constructor(
|
||||||
return@resource
|
return@resource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isDerivationSupported = checkDerivationPathSupportedUseCase(
|
||||||
|
userWalletId = params.mode.userWalletId,
|
||||||
|
networkId = currency.network.id,
|
||||||
|
derivationPath = currency.network.derivationPath,
|
||||||
|
).getOrElse { throwable ->
|
||||||
|
showErrorDialog(throwable)
|
||||||
|
return@resource
|
||||||
|
}
|
||||||
|
if (!isDerivationSupported) {
|
||||||
|
showDerivationPathNotSupportedDialog()
|
||||||
|
return@resource
|
||||||
|
}
|
||||||
|
|
||||||
useCasesFacade.addCryptoCurrenciesUseCase(currency).getOrElse { throwable ->
|
useCasesFacade.addCryptoCurrenciesUseCase(currency).getOrElse { throwable ->
|
||||||
showErrorDialog(throwable)
|
showErrorDialog(throwable)
|
||||||
return@resource
|
return@resource
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.tangem.lib.crypto.derivation
|
||||||
|
|
||||||
|
import com.tangem.common.card.EllipticCurve
|
||||||
|
import com.tangem.crypto.hdWallet.DerivationPath
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether this [EllipticCurve] is able to derive the given [path].
|
||||||
|
*
|
||||||
|
* `ed25519` and `ed25519_slip0010` support hardened derivation only (SLIP-0010), so any path that contains a
|
||||||
|
* non-hardened node cannot produce a key/address for them. This is the root cause of the "custom token added without
|
||||||
|
* an address" bug: e.g. an Algorand (ed25519) token with an EVM derivation path like `m/44'/60'/0'/0/0`.
|
||||||
|
*
|
||||||
|
* `secp256k1`, `secp256r1` and `bip0340` support both hardened and non-hardened derivation, so any path is fine.
|
||||||
|
*
|
||||||
|
* BLS curves do not support derivation at all.
|
||||||
|
*/
|
||||||
|
fun EllipticCurve.supportsDerivationPath(path: DerivationPath): Boolean {
|
||||||
|
return when (this) {
|
||||||
|
EllipticCurve.Ed25519,
|
||||||
|
EllipticCurve.Ed25519Slip0010,
|
||||||
|
-> path.nodes.all { it.isHardened }
|
||||||
|
EllipticCurve.Secp256k1,
|
||||||
|
EllipticCurve.Secp256r1,
|
||||||
|
EllipticCurve.Bip0340,
|
||||||
|
-> true
|
||||||
|
EllipticCurve.Bls12381G2,
|
||||||
|
EllipticCurve.Bls12381G2Aug,
|
||||||
|
EllipticCurve.Bls12381G2Pop,
|
||||||
|
-> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
package com.tangem.lib.crypto.derivation
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth
|
||||||
|
import com.tangem.common.card.EllipticCurve
|
||||||
|
import com.tangem.crypto.hdWallet.DerivationPath
|
||||||
|
import com.tangem.test.core.ProvideTestModels
|
||||||
|
import org.junit.jupiter.api.TestInstance
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
|
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
internal class EllipticCurveDerivationSupportTest {
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ProvideTestModels
|
||||||
|
fun supportsDerivationPath(model: TestModel) {
|
||||||
|
// Arrange
|
||||||
|
val path = DerivationPath(rawPath = model.derivationPath)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val actual = model.curve.supportsDerivationPath(path)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Truth.assertThat(actual).isEqualTo(model.expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun provideTestModels() = provideEd25519Models() +
|
||||||
|
provideSecpModels() +
|
||||||
|
provideBlsModels()
|
||||||
|
|
||||||
|
private fun provideEd25519Models() = listOf(
|
||||||
|
// region ed25519-family require a fully-hardened path
|
||||||
|
// Algorand default path — fully hardened
|
||||||
|
TestModel(curve = EllipticCurve.Ed25519, derivationPath = "m/44'/283'/0'/0'/0'", expected = true),
|
||||||
|
TestModel(curve = EllipticCurve.Ed25519Slip0010, derivationPath = "m/44'/283'/0'/0'/0'", expected = true),
|
||||||
|
// The reported bug: Algorand (ed25519) + ApeChain/EVM path with non-hardened tail
|
||||||
|
TestModel(curve = EllipticCurve.Ed25519, derivationPath = "m/44'/60'/0'/0/0", expected = false),
|
||||||
|
TestModel(curve = EllipticCurve.Ed25519Slip0010, derivationPath = "m/44'/60'/0'/0/0", expected = false),
|
||||||
|
// Solana default path — fully hardened
|
||||||
|
TestModel(curve = EllipticCurve.Ed25519Slip0010, derivationPath = "m/44'/501'/0'/0'", expected = true),
|
||||||
|
// A single non-hardened node is enough to make it unsupported
|
||||||
|
TestModel(curve = EllipticCurve.Ed25519, derivationPath = "m/44'/283'/0'/0'/0", expected = false),
|
||||||
|
// endregion
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun provideSecpModels() = listOf(
|
||||||
|
// region secp256k1 / secp256r1 / bip0340 accept any path
|
||||||
|
TestModel(curve = EllipticCurve.Secp256k1, derivationPath = "m/44'/60'/0'/0/0", expected = true),
|
||||||
|
TestModel(curve = EllipticCurve.Secp256k1, derivationPath = "m/44'/0'/0'/0/0", expected = true),
|
||||||
|
TestModel(curve = EllipticCurve.Secp256k1, derivationPath = "m/44'/283'/0'/0'/0'", expected = true),
|
||||||
|
TestModel(curve = EllipticCurve.Secp256r1, derivationPath = "m/44'/60'/0'/0/0", expected = true),
|
||||||
|
TestModel(curve = EllipticCurve.Bip0340, derivationPath = "m/44'/60'/0'/0/0", expected = true),
|
||||||
|
// endregion
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun provideBlsModels() = listOf(
|
||||||
|
// region BLS curves do not support derivation at all
|
||||||
|
TestModel(curve = EllipticCurve.Bls12381G2, derivationPath = "m/44'/60'/0'/0/0", expected = false),
|
||||||
|
TestModel(curve = EllipticCurve.Bls12381G2Aug, derivationPath = "m/44'/60'/0'/0'/0'", expected = false),
|
||||||
|
TestModel(curve = EllipticCurve.Bls12381G2Pop, derivationPath = "m/44'/60'/0'/0'/0'", expected = false),
|
||||||
|
// endregion
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TestModel(
|
||||||
|
val curve: EllipticCurve,
|
||||||
|
val derivationPath: String,
|
||||||
|
val expected: Boolean,
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue