Updated on 2026-08-14
This commit is contained in:
parent
e0a8e25779
commit
aa9eda0758
6 changed files with 321 additions and 10 deletions
|
|
@ -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 ^
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue