Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-07 18:47:45 +05:00
parent 4d08c4404d
commit b7ea0b5d2c
5 changed files with 283 additions and 9 deletions

View file

@ -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
}

View file

@ -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<TokenMarketListResponse, To
}
}
val tokens = value.tokens.map { token ->
val tokens = value.tokens.map { rawToken ->
val token = rawToken.applyL2Compatibility()
val stakingRate = token.stakingOpportunities
?.mapNotNull { it.apy }
?.max()

View file

@ -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<TokenMarketListResponse.Token.Network>?,
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,
)
}

View file

@ -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<CoinsResponse.Coin.Network>.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,
)
},
)
return this.copy(networks = networksWithL2)
}
this.copy(networks = networks + l2Networks)
} else {
this
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 <T> List<T>.appendMissingL2Networks(
networkId: (T) -> String,
createNetwork: (networkId: String) -> T,
): List<T> {
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 {

View file

@ -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.Network>?,
) = 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,
)
}