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 index 1312cb0581..37c9b75e4d 100644 --- 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 @@ -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 } } \ 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 index 92ec3a5e41..fe1f193f9a 100644 --- 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 @@ -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) { 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 index 4295568a38..89b8cfd373 100644 --- 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 @@ -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()