diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index a9c2010b63..75a7c7f036 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -7,7 +7,7 @@ tangemBlockchainSdk = "develop-1213" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-560" +tangemCardSdk = "develop-561" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/crypto/build.gradle.kts b/libs/crypto/build.gradle.kts index 0278a0501c..2adf84b11c 100644 --- a/libs/crypto/build.gradle.kts +++ b/libs/crypto/build.gradle.kts @@ -10,17 +10,30 @@ android { namespace = "com.tangem.lib.crypto" } +tasks.withType().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 } \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt new file mode 100644 index 0000000000..6008e6ea58 --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt @@ -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 + } +} \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/MutableDerivationPath.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/MutableDerivationPath.kt new file mode 100644 index 0000000000..92ec3a5e41 --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/MutableDerivationPath.kt @@ -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 +} \ No newline at end of file diff --git a/libs/crypto/src/test/kotlin/com/tangem/lib/crypto/derivation/AccountNodeRecognizerTest.kt b/libs/crypto/src/test/kotlin/com/tangem/lib/crypto/derivation/AccountNodeRecognizerTest.kt new file mode 100644 index 0000000000..4295568a38 --- /dev/null +++ b/libs/crypto/src/test/kotlin/com/tangem/lib/crypto/derivation/AccountNodeRecognizerTest.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/libs/crypto/src/test/kotlin/com/tangem/lib/crypto/derivation/MutableDerivationPathTest.kt b/libs/crypto/src/test/kotlin/com/tangem/lib/crypto/derivation/MutableDerivationPathTest.kt new file mode 100644 index 0000000000..3a364785be --- /dev/null +++ b/libs/crypto/src/test/kotlin/com/tangem/lib/crypto/derivation/MutableDerivationPathTest.kt @@ -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) + } +} \ No newline at end of file