Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-20 17:25:27 +04:00
parent e3b857b79a
commit 02e44033c9
18 changed files with 266 additions and 316 deletions

View file

@ -32,8 +32,7 @@ dependencies {
// endregion
// region Test libraries
testImplementation(deps.test.junit5)
testImplementation(projects.test.core)
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.truth)
// endregion
}

View file

@ -25,17 +25,15 @@ class AccountNodeRecognizer(private val blockchain: Blockchain) {
val nodesCount = derivationPath.nodes.size
val index = when {
blockchain == Blockchain.Tezos -> {
UTXO_BLOCKCHAIN_NODE_INDEX.takeIf { nodesCount == 4 }
}
blockchain == Blockchain.Quai || blockchain.isUTXO -> {
UTXO_BLOCKCHAIN_NODE_INDEX.takeIf { nodesCount == 5 }
}
!blockchain.isUTXO -> {
(nodesCount - 1).takeIf { nodesCount == 3 || nodesCount == 5 }
}
else -> null
// region Ethereum-like blockchains
blockchain == Blockchain.Tezos -> ACCOUNT_NODE_INDEX
blockchain == Blockchain.Quai -> ACCOUNT_NODE_INDEX
blockchain.isEvm() -> ADDRESS_INDEX_NODE_INDEX
// endregion
blockchain.isUTXO -> ACCOUNT_NODE_INDEX
else -> ACCOUNT_NODE_INDEX
}
.takeIf { nodesCount >= it }
if (index == null) {
Timber.e("Cannot determine account node index for ${blockchain.fullName}: ${derivationPath.rawPath}")
@ -251,6 +249,7 @@ class AccountNodeRecognizer(private val blockchain: Blockchain) {
}
private companion object {
const val UTXO_BLOCKCHAIN_NODE_INDEX = 2
const val ACCOUNT_NODE_INDEX = 2
const val ADDRESS_INDEX_NODE_INDEX = 4
}
}

View file

@ -3,175 +3,214 @@ package com.tangem.lib.crypto.derivation
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.models.network.Network
import com.tangem.test.core.ProvideTestModels
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class AccountNodeRecognizerTest {
private val utxoBlockchain = Blockchain.Bitcoin
private val ethLikeBlockchain = Blockchain.Ethereum
@Nested
inner class RecognizeAsDerivationPath {
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Recognize {
@Test
fun `returns account node value for a derivation path with 5 nodes and UTXO blockchain`() {
@ParameterizedTest
@ProvideTestModels
fun recognize(model: TestModel) {
// Arrange
val recognizer = AccountNodeRecognizer(utxoBlockchain)
val derivationPath = DerivationPath(rawPath = "m/44'/0'/1'/0/0")
val recognizer = AccountNodeRecognizer(blockchain = model.blockchain)
val derivationPath = DerivationPath(rawPath = model.derivationPath)
// Act
val actual = recognizer.recognize(derivationPath)
// Assert
val expected = 1
Truth.assertThat(actual).isEqualTo(expected)
Truth.assertThat(actual).isEqualTo(model.expected)
}
@Test
fun `returns account node value for a derivation path with 4 nodes and UTXO blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(utxoBlockchain)
val derivationPath = DerivationPath(rawPath = "m/44'/0'/1'/0")
// Act
val actual = recognizer.recognize(derivationPath)
// Assert
Truth.assertThat(actual).isNull()
private fun provideTestModels(): List<TestModel> {
return provideEthLikeTestModels() +
provideUTXOTestModels() +
provideOtherBlockchainTests()
}
@Test
fun `returns account node value for a derivation path with 5 nodes and non-UTXO blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(ethLikeBlockchain)
val derivationPath = DerivationPath(rawPath = "m/44'/0'/1'/2/3")
private fun provideEthLikeTestModels() = listOf(
// region Tezos blockchain
TestModel(
blockchain = Blockchain.Tezos,
derivationPath = "m/44'/1729'/1'/0/0",
expected = 1,
),
TestModel(
blockchain = Blockchain.Tezos,
derivationPath = "m/44'/1729'/1'/0",
expected = 1,
),
TestModel(
blockchain = Blockchain.Tezos,
derivationPath = "m/44'/1729'/1'",
expected = 1,
),
TestModel(
blockchain = Blockchain.Tezos,
derivationPath = "m/44'/1729'",
expected = null,
),
// endregion
// Act
val actual = recognizer.recognize(derivationPath)
// region Quai blockchain
TestModel(
blockchain = Blockchain.Quai,
derivationPath = "m/44'/994'/1'/0",
expected = 1,
),
TestModel(
blockchain = Blockchain.Quai,
derivationPath = "m/44'/994'/1'",
expected = 1,
),
TestModel(
blockchain = Blockchain.Quai,
derivationPath = "m/44'/994'",
expected = null,
),
// endregion
// Assert
val expected = 3
Truth.assertThat(actual).isEqualTo(expected)
}
// region Ethereum-like blockchain
TestModel(
blockchain = Blockchain.Ethereum,
derivationPath = "m/44'/60'/0'/0/1",
expected = 1,
),
TestModel(
blockchain = Blockchain.Ethereum,
derivationPath = "m/44'/60'/0'/0",
expected = null,
),
TestModel(
blockchain = Blockchain.Ethereum,
derivationPath = "m/44'/60'/0'",
expected = null,
),
TestModel(
blockchain = Blockchain.Ethereum,
derivationPath = "m/44'/60'",
expected = null,
),
// endregion
)
@Test
fun `returns account node value for a derivation path with 4 nodes and non-UTXO blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(ethLikeBlockchain)
val derivationPath = DerivationPath(rawPath = "m/44'/0'/1'/2")
private fun provideUTXOTestModels() = listOf(
// region Bitcoin blockchain
TestModel(
blockchain = Blockchain.Bitcoin,
derivationPath = "m/44'/0'/1'/0/0",
expected = 1,
),
TestModel(
blockchain = Blockchain.Bitcoin,
derivationPath = "m/44'/0'/1'/0",
expected = 1,
),
TestModel(
blockchain = Blockchain.Bitcoin,
derivationPath = "m/44'/0'/1'",
expected = 1,
),
TestModel(
blockchain = Blockchain.Bitcoin,
derivationPath = "m/44'/0'",
expected = null,
),
TestModel(
blockchain = Blockchain.Bitcoin,
derivationPath = "m/44'",
expected = null,
),
// endregion
)
// Act
val actual = recognizer.recognize(derivationPath)
private fun provideOtherBlockchainTests(): List<TestModel> = listOf(
// region Solana blockchain
TestModel(
blockchain = Blockchain.Solana,
derivationPath = "m/44'/501'/1'",
expected = 1,
),
TestModel(
blockchain = Blockchain.Solana,
derivationPath = "m/44'/501'",
expected = null,
),
// endregion
// Assert
Truth.assertThat(actual).isNull()
}
// region Cardano blockchain
TestModel(
blockchain = Blockchain.Cardano,
derivationPath = "m/1852'/1815'/1'/0/0",
expected = 1,
),
TestModel(
blockchain = Blockchain.Cardano,
derivationPath = "m/1852'/1815'/1'/0",
expected = 1,
),
TestModel(
blockchain = Blockchain.Cardano,
derivationPath = "m/1852'/1815'/1'",
expected = 1,
),
TestModel(
blockchain = Blockchain.Cardano,
derivationPath = "m/1852'/1815'",
expected = null,
),
// endregion
@Test
fun `returns account node value for a derivation path with 3 nodes and non-UTXO blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(ethLikeBlockchain)
val derivationPath = DerivationPath(rawPath = "m/44'/0'/1'")
// Act
val actual = recognizer.recognize(derivationPath)
// Assert
val expected = 1
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `returns account node value for a derivation path with 4 nodes and Tezos blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(Blockchain.Tezos)
val derivationPath = DerivationPath(rawPath = "m/44'/0'/0/5'")
// Act
val actual = recognizer.recognize(derivationPath)
// Assert
val expected = 0
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `returns account node value for a derivation path with 5 nodes and Tezos blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(Blockchain.Tezos)
val derivationPath = DerivationPath(rawPath = "m/44'/0'/0'/5/1")
// Act
val actual = recognizer.recognize(derivationPath)
// Assert
Truth.assertThat(actual).isNull()
}
@Test
fun `returns account node value for a derivation path with 4 nodes and Quai blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(Blockchain.Quai)
val derivationPath = DerivationPath(rawPath = "m/44'/0'/0/5'")
// Act
val actual = recognizer.recognize(derivationPath)
// Assert
Truth.assertThat(actual).isNull()
}
@Test
fun `returns account node value for a derivation path with 5 nodes and Quai blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(Blockchain.Quai)
val derivationPath = DerivationPath(rawPath = "m/44'/0'/0'/5/1")
// Act
val actual = recognizer.recognize(derivationPath)
// Assert
val expected = 0
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `returns null if derivation path is shorter than expected`() {
// Arrange
val recognizer = AccountNodeRecognizer(ethLikeBlockchain)
val derivationPath = DerivationPath(rawPath = "m/44'/0'")
// Act
val actual = recognizer.recognize(derivationPath)
// Assert
Truth.assertThat(actual).isNull()
}
// region Tron blockchain
TestModel(
blockchain = Blockchain.Tron,
derivationPath = "m/44'/195'/1'/0/0",
expected = 1,
),
TestModel(
blockchain = Blockchain.Tron,
derivationPath = "m/44'/195'/1'/0",
expected = 1,
),
TestModel(
blockchain = Blockchain.Tron,
derivationPath = "m/44'/195'/1'",
expected = 1,
),
TestModel(
blockchain = Blockchain.Tron,
derivationPath = "m/44'/195'",
expected = null,
),
// endregion
)
}
data class TestModel(
val blockchain: Blockchain,
val derivationPath: String,
val expected: Long?,
)
@Nested
inner class RecognizeAsString {
@Test
fun `returns account node value for a derivation path with 5 nodes and UTXO blockchain`() {
fun `GIVEN empty derivation path THEN returns null`() {
// Arrange
val recognizer = AccountNodeRecognizer(utxoBlockchain)
val derivationPathValue = "m/44'/0'/1'/0/0"
// Act
val actual = recognizer.recognize(derivationPathValue)
// Assert
val expected = 1
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `returns account node value for a derivation path with 4 nodes and UTXO blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(utxoBlockchain)
val derivationPathValue = "m/44'/0'/1'/0"
val derivationPathValue = "invalid/path"
// Act
val actual = recognizer.recognize(derivationPathValue)
@ -181,115 +220,7 @@ internal class AccountNodeRecognizerTest {
}
@Test
fun `returns account node value for a derivation path with 5 nodes and non-UTXO blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(ethLikeBlockchain)
val derivationPathValue = "m/44'/0'/1'/2/3"
// Act
val actual = recognizer.recognize(derivationPathValue)
// Assert
val expected = 3
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `returns account node value for a derivation path with 4 nodes and non-UTXO blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(ethLikeBlockchain)
val derivationPathValue = "m/44'/0'/1'/2"
// Act
val actual = recognizer.recognize(derivationPathValue)
// Assert
Truth.assertThat(actual).isNull()
}
@Test
fun `returns account node value for a derivation path with 3 nodes and non-UTXO blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(ethLikeBlockchain)
val derivationPathValue = "m/44'/0'/1'"
// Act
val actual = recognizer.recognize(derivationPathValue)
// Assert
val expected = 1
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `returns account node value for a derivation path with 4 nodes and Tezos blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(Blockchain.Tezos)
val derivationPathValue = "m/44'/0'/0/5'"
// Act
val actual = recognizer.recognize(derivationPathValue)
// Assert
val expected = 0
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `returns account node value for a derivation path with 5 nodes and Tezos blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(Blockchain.Tezos)
val derivationPathValue = "m/44'/0'/0'/5/1"
// Act
val actual = recognizer.recognize(derivationPathValue)
// Assert
Truth.assertThat(actual).isNull()
}
@Test
fun `returns account node value for a derivation path with 4 nodes and Quai blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(Blockchain.Quai)
val derivationPathValue = "m/44'/0'/0/5'"
// Act
val actual = recognizer.recognize(derivationPathValue)
// Assert
Truth.assertThat(actual).isNull()
}
@Test
fun `returns account node value for a derivation path with 5 nodes and Quai blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(Blockchain.Quai)
val derivationPathValue = "m/44'/0'/0'/5/1"
// Act
val actual = recognizer.recognize(derivationPathValue)
// Assert
val expected = 0
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `returns null if derivation path is shorter than expected`() {
// Arrange
val recognizer = AccountNodeRecognizer(ethLikeBlockchain)
val derivationPathValue = "m/44'/0'"
// Act
val actual = recognizer.recognize(derivationPathValue)
// Assert
Truth.assertThat(actual).isNull()
}
@Test
fun `returns null if derivation path string is invalid`() {
fun `GIVEN invalid derivation path THEN returns null`() {
// Arrange
val recognizer = AccountNodeRecognizer(utxoBlockchain)
val derivationPathValue = "invalid/path"
@ -301,4 +232,34 @@ internal class AccountNodeRecognizerTest {
Truth.assertThat(actual).isNull()
}
}
@Nested
inner class RecognizeAsNetworkDerivationPath {
@Test
fun `GIVEN empty derivation path THEN returns null`() {
// Arrange
val recognizer = AccountNodeRecognizer(utxoBlockchain)
val derivationPath = Network.DerivationPath.None
// Act
val actual = recognizer.recognize(derivationPath)
// Assert
Truth.assertThat(actual).isNull()
}
@Test
fun `GIVEN invalid derivation path THEN returns null`() {
// Arrange
val recognizer = AccountNodeRecognizer(utxoBlockchain)
val derivationPathValue = Network.DerivationPath.Card("invalid/path")
// Act
val actual = recognizer.recognize(derivationPathValue)
// Assert
Truth.assertThat(actual).isNull()
}
}
}