Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-01 21:47:00 +04:00
parent e0a8e25779
commit aa9eda0758
6 changed files with 321 additions and 10 deletions

View file

@ -10,17 +10,30 @@ android {
namespace = "com.tangem.lib.crypto"
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
/** Coroutines */
implementation(deps.kotlin.coroutines)
/** SDK */
implementation(tangemDeps.blockchain)
/** Core */
// region Project
implementation(projects.core.utils)
/** Libs */
implementation(projects.libs.blockchainSdk)
// endregion
// region Tangem SDKs
implementation(tangemDeps.card.core)
implementation(tangemDeps.blockchain)
// endregion
// region Other deps
implementation(deps.kotlin.coroutines)
implementation(deps.timber)
// endregion
// region Test libraries
testImplementation(deps.test.junit5)
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.truth)
// endregion
}

View file

@ -0,0 +1,46 @@
package com.tangem.lib.crypto.derivation
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.isUTXO
import com.tangem.crypto.hdWallet.DerivationPath
/**
* Utility class to recognize the account node in a derivation path based on the blockchain type.
* Derivation path schema: [ m / purpose' / coin_type' / account' / change / address_index ].
*
* @param blockchain the blockchain for which the account node is to be recognized
*
[REDACTED_AUTHOR]
*/
class AccountNodeRecognizer(blockchain: Blockchain) {
/** Index of the account node in the derivation path */
val accountNodeIndex: Int = if (blockchain.isUTXO) {
UTXO_BLOCKCHAIN_NODE_INDEX
} else {
NON_UTXO_BLOCKCHAIN_NODE_INDEX
}
/** Recognizes the account node value from the given derivation path string [derivationPathValue] */
fun recognize(derivationPathValue: String): Long? {
return runCatching {
recognize(derivationPath = DerivationPath(rawPath = derivationPathValue))
}
.getOrNull()
}
/** Recognizes the account node value from the given [derivationPath] */
fun recognize(derivationPath: DerivationPath): Long? {
return runCatching {
val accountNode = derivationPath.nodes.getOrNull(accountNodeIndex)
accountNode?.getIndex(includeHardened = false)
}
.getOrNull()
}
private companion object {
const val UTXO_BLOCKCHAIN_NODE_INDEX = 2
const val NON_UTXO_BLOCKCHAIN_NODE_INDEX = 4
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.lib.crypto.derivation
import com.tangem.blockchain.common.Blockchain
import com.tangem.crypto.hdWallet.DerivationNode
import com.tangem.crypto.hdWallet.DerivationPath
import timber.log.Timber
/** Extension function to convert a [DerivationPath] into a [MutableDerivationPath] */
fun DerivationPath.toMutable(): MutableDerivationPath = MutableDerivationPath(value = this)
/**
* A mutable representation of a derivation path, allowing modifications to specific nodes
*
* @property value the initial derivationPath to be used
*/
class MutableDerivationPath internal constructor(val value: DerivationPath) {
/**
* Replaces the account node in the derivation path with a new value
*
* @param value the new index to set for the account node
* @param blockchain the blockchain used to determine the account node index
*/
fun replaceAccountNode(value: Long, blockchain: Blockchain): MutableDerivationPath {
val mutableNodes = this@MutableDerivationPath.value.nodes.toMutableList()
val accountNodeIndex = AccountNodeRecognizer(blockchain).accountNodeIndex
val accountNode = mutableNodes.getOrNull(accountNodeIndex)
if (accountNode != null) {
mutableNodes[accountNodeIndex] = when (accountNode) {
is DerivationNode.Hardened -> DerivationNode.Hardened(value)
is DerivationNode.NonHardened -> DerivationNode.NonHardened(value)
}
} else {
Timber.e("Account node not found in the derivation path: ${this@MutableDerivationPath.value}")
}
return DerivationPath(path = mutableNodes).toMutable()
}
/** Applies the changes and returns it as an immutable [DerivationPath] */
fun apply(): DerivationPath = value
}

View file

@ -0,0 +1,116 @@
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 org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
internal class AccountNodeRecognizerTest {
private val utxoBlockchain = Blockchain.Bitcoin
private val nonUtxoBlockchain = Blockchain.Ethereum
@Nested
inner class RecognizeAsDerivationPath {
@Test
fun `returns account node value for UTXO blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(utxoBlockchain)
val derivationPath = DerivationPath(rawPath = "m/44'/0'/1'/0/0")
// Act
val actual = recognizer.recognize(derivationPath)
// Assert
val expected = 1
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `returns account node value for non-UTXO blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(nonUtxoBlockchain)
val derivationPath = DerivationPath(rawPath = "m/44'/0'/0'/0/0")
// 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(nonUtxoBlockchain)
val derivationPath = DerivationPath(rawPath = "m/44'/0'")
// Act
val actual = recognizer.recognize(derivationPath)
// Assert
Truth.assertThat(actual).isNull()
}
}
@Nested
inner class RecognizeAsString {
@Test
fun `returns account node value for UTXO blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(utxoBlockchain)
val derivationPath = "m/44'/0'/1'/0/0"
// Act
val actual = recognizer.recognize(derivationPath)
// Assert
val expected = 1
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `returns account node value for non-UTXO blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(nonUtxoBlockchain)
val derivationPath = "m/44'/0'/0'/0/0"
// 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(nonUtxoBlockchain)
val derivationPath = "m/44'/0'"
// Act
val actual = recognizer.recognize(derivationPath)
// Assert
Truth.assertThat(actual).isNull()
}
@Test
fun `returns null if derivation path string is invalid`() {
// Arrange
val recognizer = AccountNodeRecognizer(utxoBlockchain)
val derivationPathValue = "invalid/path"
// Act
val actual = recognizer.recognize(derivationPathValue)
// Assert
Truth.assertThat(actual).isNull()
}
}
}

View file

@ -0,0 +1,92 @@
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 org.junit.jupiter.api.Test
internal class MutableDerivationPathTest {
private val utxoBlockchain = Blockchain.Bitcoin
private val nonUtxoBlockchain = Blockchain.Ethereum
@Test
fun `replaces account node with hardened value`() {
// Arrange
val derivationPath = DerivationPath(rawPath = "m/44'/0'/0'/0/0")
val mutablePath = derivationPath.toMutable()
// Act
val actual = mutablePath
.replaceAccountNode(value = 1, blockchain = utxoBlockchain)
.apply()
// Assert
val expected = DerivationPath(rawPath = "m/44'/0'/1'/0/0")
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `replaces account node with non hardened value`() {
// Arrange
val derivationPath = DerivationPath(rawPath = "m/44'/0'/0/0/0")
val mutablePath = derivationPath.toMutable()
// Act
val actual = mutablePath
.replaceAccountNode(value = 1, blockchain = utxoBlockchain)
.apply()
// Assert
val expected = DerivationPath(rawPath = "m/44'/0'/1/0/0")
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `does nothing if account node not found`() {
// Arrange
val derivationPath = DerivationPath(rawPath = "m/44'/0'")
val mutablePath = derivationPath.toMutable()
// Act
val actual = mutablePath
.replaceAccountNode(value = 1, blockchain = utxoBlockchain)
.apply()
// Assert
val expected = DerivationPath(rawPath = "m/44'/0'")
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `replaces account node with hardened value for non-utxo blockchain`() {
// Arrange
val derivationPath = DerivationPath(rawPath = "m/44'/60'/0'/0/0")
val mutablePath = derivationPath.toMutable()
// Act
val actual = mutablePath
.replaceAccountNode(value = 2, blockchain = nonUtxoBlockchain)
.apply()
// Assert
val expected = DerivationPath(rawPath = "m/44'/60'/0'/0/2")
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `does nothing if account node not found for non-utxo blockchain`() {
// Arrange
val derivationPath = DerivationPath(rawPath = "m/44'/60'")
val mutablePath = derivationPath.toMutable()
// Act
val actual = mutablePath
.replaceAccountNode(value = 2, blockchain = nonUtxoBlockchain)
.apply()
// Assert
val expected = DerivationPath(rawPath = "m/44'/60'")
Truth.assertThat(actual).isEqualTo(expected)
}
}