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)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCheckDerivationPathSupportedUseCase(
|
||||
customTokensRepository: CustomTokensRepository,
|
||||
): CheckDerivationPathSupportedUseCase {
|
||||
return CheckDerivationPathSupportedUseCase(customTokensRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
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.wallet.UserWallet
|
||||
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 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) {
|
||||
when (val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)) {
|
||||
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 isDerivationPathSupported(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): Boolean
|
||||
|
||||
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.wrappedList
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.domain.managetokens.CheckDerivationPathSupportedUseCase
|
||||
import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase
|
||||
import com.tangem.domain.managetokens.FindTokenUseCase
|
||||
import com.tangem.domain.managetokens.ValidateTokenFormUseCase
|
||||
|
|
@ -51,6 +52,7 @@ internal class CustomTokenFormModel @Inject constructor(
|
|||
private val createCryptoCurrencyUseCase: CreateCryptoCurrencyUseCase,
|
||||
private val findTokenUseCase: FindTokenUseCase,
|
||||
private val validateTokenFormUseCase: ValidateTokenFormUseCase,
|
||||
private val checkDerivationPathSupportedUseCase: CheckDerivationPathSupportedUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
customTokenFormUseCasesFacadeFactory: CustomTokenFormUseCasesFacade.Factory,
|
||||
) : 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) {
|
||||
TangemLogger.e("Error", throwable)
|
||||
val message = when (throwable) {
|
||||
|
|
@ -355,6 +365,19 @@ internal class CustomTokenFormModel @Inject constructor(
|
|||
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 ->
|
||||
showErrorDialog(throwable)
|
||||
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