Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-13 12:51:06 +04:00
parent 71380aec64
commit 1010c03899
3 changed files with 414 additions and 26 deletions

View file

@ -4,22 +4,44 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.isUTXO
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.models.network.Network
import timber.log.Timber
/**
* 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
* @property blockchain the blockchain for which the account node is to be recognized
*
* @see [iOS](https://github.com/tangem-developments/tangem-app-ios/blob/f5312a8177afbebda2ff2ed93771f11fae4837bb/Tangem/Domain/Accounts/Common/AccountDerivationPathHelper.swift)
[REDACTED_AUTHOR]
*/
class AccountNodeRecognizer(blockchain: Blockchain) {
class AccountNodeRecognizer(private val 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
/**
* Index of the account node in the [derivationPath]
*/
@Suppress("MagicNumber")
fun getAccountNodeIndex(derivationPath: DerivationPath): Int? {
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
}
if (index == null) {
Timber.e("Cannot determine account node index for ${blockchain.fullName}: ${derivationPath.rawPath}")
}
return index
}
/** Recognizes the account node value from the given [derivationPath] */
@ -42,6 +64,12 @@ class AccountNodeRecognizer(blockchain: Blockchain) {
/** Recognizes the account node value from the given [derivationPath] */
fun recognize(derivationPath: DerivationPath): Long? {
return runCatching {
if (!blockchain.isAccountsSupported()) {
Timber.e("Account derivation is not supported for blockchain: ${blockchain.fullName}")
return null
}
val accountNodeIndex = getAccountNodeIndex(derivationPath) ?: return null
val accountNode = derivationPath.nodes.getOrNull(accountNodeIndex)
accountNode?.getIndex(includeHardened = false)
@ -49,8 +77,180 @@ class AccountNodeRecognizer(blockchain: Blockchain) {
.getOrNull()
}
@Suppress("LongMethod")
private fun Blockchain.isAccountsSupported(): Boolean {
return when (this) {
Blockchain.Bitcoin,
Blockchain.Litecoin,
Blockchain.Stellar,
Blockchain.Ethereum,
Blockchain.EthereumPow,
Blockchain.Dischain,
Blockchain.EthereumClassic,
Blockchain.RSK,
Blockchain.BitcoinCash,
Blockchain.Binance,
Blockchain.Cardano,
Blockchain.XRP,
Blockchain.Ducatus,
Blockchain.Tezos,
Blockchain.Dogecoin,
Blockchain.BSC,
Blockchain.Polygon,
Blockchain.Avalanche,
Blockchain.Solana,
Blockchain.Fantom,
Blockchain.Polkadot,
Blockchain.Kusama,
Blockchain.AlephZero,
Blockchain.Tron,
Blockchain.Arbitrum,
Blockchain.Dash,
Blockchain.Gnosis,
Blockchain.Optimism,
Blockchain.TON,
Blockchain.Kava,
Blockchain.Kaspa,
Blockchain.Ravencoin,
Blockchain.Cosmos,
Blockchain.TerraV1,
Blockchain.TerraV2,
Blockchain.Cronos,
Blockchain.Telos,
Blockchain.OctaSpace,
Blockchain.Near,
Blockchain.Decimal,
Blockchain.VeChain,
Blockchain.XDC,
Blockchain.Algorand,
Blockchain.Shibarium,
Blockchain.Aptos,
Blockchain.Hedera,
Blockchain.Areon,
Blockchain.Playa3ull,
Blockchain.PulseChain,
Blockchain.Aurora,
Blockchain.Manta,
Blockchain.ZkSyncEra,
Blockchain.Moonbeam,
Blockchain.PolygonZkEVM,
Blockchain.Moonriver,
Blockchain.Mantle,
Blockchain.Flare,
Blockchain.Taraxa,
Blockchain.Radiant,
Blockchain.Base,
Blockchain.Joystream,
Blockchain.Bittensor,
Blockchain.Koinos,
Blockchain.InternetComputer,
Blockchain.Cyber,
Blockchain.Blast,
Blockchain.Sui,
Blockchain.Filecoin,
Blockchain.Sei,
Blockchain.EnergyWebChain,
Blockchain.EnergyWebX,
Blockchain.Core,
Blockchain.Canxium,
Blockchain.Casper,
Blockchain.Chiliz,
Blockchain.Xodex,
Blockchain.Clore,
Blockchain.Fact0rn,
Blockchain.OdysseyChain,
Blockchain.Bitrock,
Blockchain.ApeChain,
Blockchain.Sonic,
Blockchain.Alephium,
Blockchain.VanarChain,
Blockchain.ZkLinkNova,
Blockchain.Pepecoin,
Blockchain.Hyperliquid,
Blockchain.Scroll,
Blockchain.Linea,
Blockchain.ArbitrumNova,
Blockchain.Quai,
-> true
Blockchain.Nexa, // unsupported network
Blockchain.Chia,
-> false
// region Testnet
Blockchain.Unknown,
Blockchain.ArbitrumTestnet,
Blockchain.AvalancheTestnet,
Blockchain.BinanceTestnet,
Blockchain.BSCTestnet,
Blockchain.BitcoinTestnet,
Blockchain.BitcoinCashTestnet,
Blockchain.CosmosTestnet,
Blockchain.EthereumTestnet,
Blockchain.EthereumClassicTestnet,
Blockchain.FantomTestnet,
Blockchain.NearTestnet,
Blockchain.PolkadotTestnet,
Blockchain.KavaTestnet,
Blockchain.PolygonTestnet,
Blockchain.SeiTestnet,
Blockchain.StellarTestnet,
Blockchain.SolanaTestnet,
Blockchain.TronTestnet,
Blockchain.OptimismTestnet,
Blockchain.EthereumPowTestnet,
Blockchain.KaspaTestnet,
Blockchain.TelosTestnet,
Blockchain.TONTestnet,
Blockchain.RavencoinTestnet,
Blockchain.AlephZeroTestnet,
Blockchain.OctaSpaceTestnet,
Blockchain.ChiaTestnet,
Blockchain.DecimalTestnet,
Blockchain.XDCTestnet,
Blockchain.VeChainTestnet,
Blockchain.AptosTestnet,
Blockchain.ShibariumTestnet,
Blockchain.AlgorandTestnet,
Blockchain.HederaTestnet,
Blockchain.AuroraTestnet,
Blockchain.AreonTestnet,
Blockchain.PulseChainTestnet,
Blockchain.ZkSyncEraTestnet,
Blockchain.NexaTestnet,
Blockchain.MoonbeamTestnet,
Blockchain.MantaTestnet,
Blockchain.PolygonZkEVMTestnet,
Blockchain.BaseTestnet,
Blockchain.MoonriverTestnet,
Blockchain.MantleTestnet,
Blockchain.FlareTestnet,
Blockchain.TaraxaTestnet,
Blockchain.KoinosTestnet,
Blockchain.BlastTestnet,
Blockchain.CyberTestnet,
Blockchain.SuiTestnet,
Blockchain.EnergyWebChainTestnet,
Blockchain.EnergyWebXTestnet,
Blockchain.CasperTestnet,
Blockchain.CoreTestnet,
Blockchain.ChilizTestnet,
Blockchain.AlephiumTestnet,
Blockchain.VanarChainTestnet,
Blockchain.OdysseyChainTestnet,
Blockchain.BitrockTestnet,
Blockchain.SonicTestnet,
Blockchain.ApeChainTestnet,
Blockchain.ScrollTestnet,
Blockchain.ZkLinkNovaTestnet,
Blockchain.PepecoinTestnet,
Blockchain.HyperliquidTestnet,
Blockchain.QuaiTestnet,
Blockchain.LineaTestnet,
-> false
// endregion
}
}
private companion object {
const val UTXO_BLOCKCHAIN_NODE_INDEX = 2
const val NON_UTXO_BLOCKCHAIN_NODE_INDEX = 4
}
}

View file

@ -24,8 +24,8 @@ class MutableDerivationPath internal constructor(val value: DerivationPath) {
fun replaceAccountNode(value: Long, blockchain: Blockchain): MutableDerivationPath {
val mutableNodes = this@MutableDerivationPath.value.nodes.toMutableList()
val accountNodeIndex = AccountNodeRecognizer(blockchain).accountNodeIndex
val accountNode = mutableNodes.getOrNull(accountNodeIndex)
val accountNodeIndex = AccountNodeRecognizer(blockchain).getAccountNodeIndex(this@MutableDerivationPath.value)
val accountNode = accountNodeIndex?.let(mutableNodes::getOrNull)
if (accountNode != null) {
mutableNodes[accountNodeIndex] = when (accountNode) {

View file

@ -9,13 +9,13 @@ import org.junit.jupiter.api.Test
internal class AccountNodeRecognizerTest {
private val utxoBlockchain = Blockchain.Bitcoin
private val nonUtxoBlockchain = Blockchain.Ethereum
private val ethLikeBlockchain = Blockchain.Ethereum
@Nested
inner class RecognizeAsDerivationPath {
@Test
fun `returns account node value for UTXO blockchain`() {
fun `returns account node value for a derivation path with 5 nodes and UTXO blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(utxoBlockchain)
val derivationPath = DerivationPath(rawPath = "m/44'/0'/1'/0/0")
@ -29,10 +29,104 @@ internal class AccountNodeRecognizerTest {
}
@Test
fun `returns account node value for non-UTXO blockchain`() {
fun `returns account node value for a derivation path with 4 nodes and UTXO blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(nonUtxoBlockchain)
val derivationPath = DerivationPath(rawPath = "m/44'/0'/0'/0/0")
val recognizer = AccountNodeRecognizer(utxoBlockchain)
val derivationPath = DerivationPath(rawPath = "m/44'/0'/1'/0")
// 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 non-UTXO blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(ethLikeBlockchain)
val derivationPath = DerivationPath(rawPath = "m/44'/0'/1'/2/3")
// Act
val actual = recognizer.recognize(derivationPath)
// 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 derivationPath = DerivationPath(rawPath = "m/44'/0'/1'/2")
// Act
val actual = recognizer.recognize(derivationPath)
// 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 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)
@ -45,7 +139,7 @@ internal class AccountNodeRecognizerTest {
@Test
fun `returns null if derivation path is shorter than expected`() {
// Arrange
val recognizer = AccountNodeRecognizer(nonUtxoBlockchain)
val recognizer = AccountNodeRecognizer(ethLikeBlockchain)
val derivationPath = DerivationPath(rawPath = "m/44'/0'")
// Act
@ -60,13 +154,13 @@ internal class AccountNodeRecognizerTest {
inner class RecognizeAsString {
@Test
fun `returns account node value for UTXO blockchain`() {
fun `returns account node value for a derivation path with 5 nodes and UTXO blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(utxoBlockchain)
val derivationPath = "m/44'/0'/1'/0/0"
val derivationPathValue = "m/44'/0'/1'/0/0"
// Act
val actual = recognizer.recognize(derivationPath)
val actual = recognizer.recognize(derivationPathValue)
// Assert
val expected = 1
@ -74,13 +168,107 @@ internal class AccountNodeRecognizerTest {
}
@Test
fun `returns account node value for non-UTXO blockchain`() {
fun `returns account node value for a derivation path with 4 nodes and UTXO blockchain`() {
// Arrange
val recognizer = AccountNodeRecognizer(nonUtxoBlockchain)
val derivationPath = "m/44'/0'/0'/0/0"
val recognizer = AccountNodeRecognizer(utxoBlockchain)
val derivationPathValue = "m/44'/0'/1'/0"
// Act
val actual = recognizer.recognize(derivationPath)
val actual = recognizer.recognize(derivationPathValue)
// Assert
Truth.assertThat(actual).isNull()
}
@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
@ -90,11 +278,11 @@ internal class AccountNodeRecognizerTest {
@Test
fun `returns null if derivation path is shorter than expected`() {
// Arrange
val recognizer = AccountNodeRecognizer(nonUtxoBlockchain)
val derivationPath = "m/44'/0'"
val recognizer = AccountNodeRecognizer(ethLikeBlockchain)
val derivationPathValue = "m/44'/0'"
// Act
val actual = recognizer.recognize(derivationPath)
val actual = recognizer.recognize(derivationPathValue)
// Assert
Truth.assertThat(actual).isNull()