From b7ea0b5d2cd3627820e8315a305f8557057366af Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Jul 2026 18:47:45 +0500 Subject: [PATCH] Updated on 2026-08-14 --- data/markets/build.gradle.kts | 4 + .../converters/TokenMarketListConverter.kt | 4 +- .../TokenMarketListConverterTest.kt | 145 ++++++++++++++++++ .../blockchainsdk/compatibility/L2Networks.kt | 47 +++++- .../compatibility/L2NetworksTest.kt | 92 +++++++++++ 5 files changed, 283 insertions(+), 9 deletions(-) create mode 100644 data/markets/src/test/java/com/tangem/data/markets/converters/TokenMarketListConverterTest.kt create mode 100644 libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/compatibility/L2NetworksTest.kt diff --git a/data/markets/build.gradle.kts b/data/markets/build.gradle.kts index bce4fe8a70..e25a0fa07d 100644 --- a/data/markets/build.gradle.kts +++ b/data/markets/build.gradle.kts @@ -49,4 +49,8 @@ dependencies { ksp(deps.moshi.kotlin.codegen) kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) // endregion + + // region Tests dependencies + testImplementation(projects.test.core) + // endregion } diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt index 86b29f8006..0ef9c79726 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt @@ -1,5 +1,6 @@ package com.tangem.data.markets.converters +import com.tangem.blockchainsdk.compatibility.applyL2Compatibility import com.tangem.datasource.api.markets.models.response.TokenMarketListResponse import com.tangem.domain.markets.TokenMarket import com.tangem.domain.markets.TokenMarketListWithMaxApy @@ -19,7 +20,8 @@ internal object TokenMarketListConverter : Converter + val tokens = value.tokens.map { rawToken -> + val token = rawToken.applyL2Compatibility() val stakingRate = token.stakingOpportunities ?.mapNotNull { it.apy } ?.max() diff --git a/data/markets/src/test/java/com/tangem/data/markets/converters/TokenMarketListConverterTest.kt b/data/markets/src/test/java/com/tangem/data/markets/converters/TokenMarketListConverterTest.kt new file mode 100644 index 0000000000..e48c695c8e --- /dev/null +++ b/data/markets/src/test/java/com/tangem/data/markets/converters/TokenMarketListConverterTest.kt @@ -0,0 +1,145 @@ +package com.tangem.data.markets.converters + +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchainsdk.compatibility.l2BlockchainsList +import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.datasource.api.markets.models.response.TokenMarketListResponse +import com.tangem.domain.markets.TokenMarket +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +internal class TokenMarketListConverterTest { + + @Test + fun `GIVEN ethereum coin with networks WHEN convert THEN L2 networks are appended`() { + // Arrange + val response = createResponse( + createToken(id = "ethereum", networks = listOf(createNetwork(networkId = "ethereum"))), + ) + + // Act + val actual = TokenMarketListConverter.convert(response) + + // Assert + val networkIds = actual.tokens.single().networks?.map(TokenMarket.Network::networkId) + val expectedNetworkIds = listOf("ethereum") + l2BlockchainsList.map { it.toNetworkId() } + assertThat(networkIds).containsExactlyElementsIn(expectedNetworkIds) + assertThat(networkIds).containsAtLeast("arbitrum-one", "optimistic-ethereum", "base") + } + + @Test + fun `GIVEN ethereum coin with networks WHEN convert THEN appended L2 networks are native coin entries`() { + // Arrange + val response = createResponse( + createToken(id = "ethereum", networks = listOf(createNetwork(networkId = "ethereum"))), + ) + + // Act + val actual = TokenMarketListConverter.convert(response) + + // Assert + val l2Networks = actual.tokens.single().networks.orEmpty().filter { it.networkId != "ethereum" } + assertThat(l2Networks).isNotEmpty() + assertThat(l2Networks.mapNotNull(TokenMarket.Network::contractAddress)).isEmpty() + } + + @Test + fun `GIVEN ethereum coin with backend-provided L2 network WHEN convert THEN backend entry wins without duplicates`() { + // Arrange + val backendArbitrum = createNetwork(networkId = "arbitrum-one", decimalCount = 18) + val response = createResponse( + createToken(id = "ethereum", networks = listOf(createNetwork(networkId = "ethereum"), backendArbitrum)), + ) + + // Act + val actual = TokenMarketListConverter.convert(response) + + // Assert + val networks = actual.tokens.single().networks.orEmpty() + assertThat(networks.map(TokenMarket.Network::networkId)).containsNoDuplicates() + val arbitrum = networks.single { it.networkId == "arbitrum-one" } + assertThat(arbitrum.decimalCount).isEqualTo(18) + } + + @Test + fun `GIVEN non-ethereum token with networks WHEN convert THEN networks stay unchanged`() { + // Arrange + val tetherNetworks = listOf( + createNetwork( + networkId = "ethereum", + contractAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7", + decimalCount = 6, + ), + createNetwork( + networkId = "tron", + contractAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", + decimalCount = 6, + ), + ) + val response = createResponse(createToken(id = "tether", networks = tetherNetworks)) + + // Act + val actual = TokenMarketListConverter.convert(response) + + // Assert + val expected = tetherNetworks.map { network -> + TokenMarket.Network( + networkId = network.networkId, + contractAddress = network.contractAddress, + decimalCount = network.decimalCount, + ) + } + assertThat(actual.tokens.single().networks).isEqualTo(expected) + } + + @Test + fun `GIVEN ethereum coin without networks WHEN convert THEN networks stay null`() { + // Arrange + val response = createResponse(createToken(id = "ethereum", networks = null)) + + // Act + val actual = TokenMarketListConverter.convert(response) + + // Assert + assertThat(actual.tokens.single().networks).isNull() + } + + private fun createResponse(vararg tokens: TokenMarketListResponse.Token) = TokenMarketListResponse( + imageHost = "https://img.tangem.org/", + tokens = tokens.toList(), + total = tokens.size, + limit = 20, + offset = 0, + timestamp = 1L, + summary = null, + ) + + private fun createToken( + id: String, + networks: List?, + name: String = id, + symbol: String = id.take(n = 3).uppercase(), + ) = TokenMarketListResponse.Token( + id = id, + name = name, + symbol = symbol, + currentPrice = BigDecimal.ONE, + priceChangePercentage = null, + marketRating = null, + marketCap = null, + isUnderMarketCapLimit = null, + stakingOpportunities = null, + maxYieldApy = null, + networks = networks, + ) + + private fun createNetwork( + networkId: String, + contractAddress: String? = null, + decimalCount: Int? = null, + ) = TokenMarketListResponse.Token.Network( + networkId = networkId, + contractAddress = contractAddress, + decimalCount = decimalCount, + ) +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/compatibility/L2Networks.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/compatibility/L2Networks.kt index 782e3e88ff..f60e5218ee 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/compatibility/L2Networks.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/compatibility/L2Networks.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.datasource.api.markets.models.response.TokenMarketInfoResponse +import com.tangem.datasource.api.markets.models.response.TokenMarketListResponse import com.tangem.datasource.api.tangemTech.models.CoinsResponse import com.tangem.datasource.api.tangemTech.models.UserTokensResponse @@ -36,18 +37,48 @@ fun List.applyL2Compatibility(coinId: String): List< fun TokenMarketInfoResponse.applyL2Compatibility(coinId: String): TokenMarketInfoResponse { val networks = this.networks ?: return this - return if (coinId == ETHEREUM_COIN_ID) { - val l2Networks = l2BlockchainsList.map { blockchain -> + if (coinId != ETHEREUM_COIN_ID) return this + + val networksWithL2 = networks.appendMissingL2Networks( + networkId = { it.networkId }, + createNetwork = { networkId -> TokenMarketInfoResponse.Network( - networkId = blockchain.toNetworkId(), + networkId = networkId, contractAddress = null, decimalCount = null, ) - } - this.copy(networks = networks + l2Networks) - } else { - this - } + }, + ) + return this.copy(networks = networksWithL2) +} + +fun TokenMarketListResponse.Token.applyL2Compatibility(): TokenMarketListResponse.Token { + val networks = this.networks ?: return this + if (id != ETHEREUM_COIN_ID) return this + + val networksWithL2 = networks.appendMissingL2Networks( + networkId = { it.networkId }, + createNetwork = { networkId -> + TokenMarketListResponse.Token.Network( + networkId = networkId, + contractAddress = null, + decimalCount = null, + ) + }, + ) + return this.copy(networks = networksWithL2) +} + +private inline fun List.appendMissingL2Networks( + networkId: (T) -> String, + createNetwork: (networkId: String) -> T, +): List { + val existingNetworkIds = mapTo(hashSetOf(), networkId) + val missingL2Networks = l2BlockchainsList + .map { it.toNetworkId() } + .filterNot { it in existingNetworkIds } + .map(createNetwork) + return this + missingL2Networks } fun getTokenIdIfL2Network(tokenId: String): String { diff --git a/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/compatibility/L2NetworksTest.kt b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/compatibility/L2NetworksTest.kt new file mode 100644 index 0000000000..1dc7fda86c --- /dev/null +++ b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/compatibility/L2NetworksTest.kt @@ -0,0 +1,92 @@ +package com.tangem.blockchainsdk.compatibility + +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.datasource.api.markets.models.response.TokenMarketInfoResponse +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +internal class L2NetworksTest { + + @Test + fun `GIVEN ethereum info with networks WHEN applyL2Compatibility THEN missing L2 networks are appended`() { + // Arrange + val response = createInfoResponse(id = "ethereum", networks = listOf(createNetwork(networkId = "ethereum"))) + + // Act + val actual = response.applyL2Compatibility(coinId = "ethereum") + + // Assert + val networkIds = actual.networks?.map(TokenMarketInfoResponse.Network::networkId) + val expectedNetworkIds = listOf("ethereum") + l2BlockchainsList.map { it.toNetworkId() } + assertThat(networkIds).containsExactlyElementsIn(expectedNetworkIds) + } + + @Test + fun `GIVEN ethereum info with backend-provided L2 network WHEN applyL2Compatibility THEN backend entry wins without duplicates`() { + // Arrange + val backendArbitrum = createNetwork(networkId = "arbitrum-one", decimalCount = 18) + val response = createInfoResponse( + id = "ethereum", + networks = listOf(createNetwork(networkId = "ethereum"), backendArbitrum), + ) + + // Act + val actual = response.applyL2Compatibility(coinId = "ethereum") + + // Assert + val networks = actual.networks.orEmpty() + assertThat(networks.map(TokenMarketInfoResponse.Network::networkId)).containsNoDuplicates() + assertThat(networks.single { it.networkId == "arbitrum-one" }).isEqualTo(backendArbitrum) + } + + @Test + fun `GIVEN non-ethereum info WHEN applyL2Compatibility THEN networks stay unchanged`() { + // Arrange + val tetherNetworks = listOf( + createNetwork( + networkId = "ethereum", + contractAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7", + decimalCount = 6, + ), + ) + val response = createInfoResponse(id = "tether", networks = tetherNetworks) + + // Act + val actual = response.applyL2Compatibility(coinId = "tether") + + // Assert + assertThat(actual.networks).isEqualTo(tetherNetworks) + } + + private fun createInfoResponse( + id: String, + networks: List?, + ) = TokenMarketInfoResponse( + id = id, + name = id, + symbol = id.take(n = 3).uppercase(), + currentPrice = BigDecimal.ONE, + priceChangePercentage = null, + networks = networks, + shortDescription = null, + fullDescription = null, + insights = null, + metrics = null, + securityData = null, + links = null, + pricePerformance = null, + exchangesAmount = null, + ) + + private fun createNetwork( + networkId: String, + contractAddress: String? = null, + decimalCount: Int? = null, + ) = TokenMarketInfoResponse.Network( + networkId = networkId, + exchangeable = false, + contractAddress = contractAddress, + decimalCount = decimalCount, + ) +} \ No newline at end of file