Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-27 19:04:07 +04:00
parent 22fc180d9e
commit b004f8df6b
5 changed files with 83 additions and 14 deletions

View file

@ -1,7 +1,9 @@
package com.tangem.data.networks.converters
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.datasource.local.network.entity.NetworkStatusDM.CurrencyId
import com.tangem.datasource.local.network.entity.NetworkStatusDM.CurrencyId.Companion.CONTRACT_ADDRESS_DELIMITER
import com.tangem.domain.models.currency.CryptoCurrency
@ -22,6 +24,12 @@ internal class NetworkCurrencyIdConverter(
private val derivationPath: Network.DerivationPath,
) : TwoWayConverter<CurrencyId, CryptoCurrency.ID> {
// Cache stores blockchainId in legacy format (e.g. "BTC"), but runtime
// CryptoCurrency.ID expects the new network rawId (e.g. "bitcoin") matching
// Network.rawId built from Blockchain.toNetworkId(). Convert once on construction
// so that IDs reconstructed from cache match those built at runtime.
private val networkRawId: String = Blockchain.fromId(blockchainId).toNetworkId()
override fun convert(value: CurrencyId): CryptoCurrency.ID {
val suffixParts = value.value.split(CONTRACT_ADDRESS_DELIMITER)
@ -80,17 +88,17 @@ internal class NetworkCurrencyIdConverter(
return when (derivationPath) {
is Network.DerivationPath.Card -> {
CryptoCurrency.ID.Body.NetworkIdWithDerivationPath(
rawId = blockchainId,
rawId = networkRawId,
derivationPath = derivationPath.value,
)
}
is Network.DerivationPath.Custom -> {
CryptoCurrency.ID.Body.NetworkIdWithDerivationPath(
rawId = blockchainId,
rawId = networkRawId,
derivationPath = derivationPath.value,
)
}
is Network.DerivationPath.None -> CryptoCurrency.ID.Body.NetworkId(blockchainId)
is Network.DerivationPath.None -> CryptoCurrency.ID.Body.NetworkId(networkRawId)
}
}
}

View file

@ -16,10 +16,14 @@ import java.math.BigDecimal
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class NetworkAmountsConverterTest {
private val rawNetworkId = "ethereum"
// Cache stores the SDK-level Blockchain.id (legacy format, e.g. "ETH").
// The converter normalizes it to the canonical network rawId ("ethereum") via
// Blockchain.fromId(...).toNetworkId() so that resulting CryptoCurrency.IDs match those
// built at runtime from Network.rawId.
private val blockchainId = "ETH"
private val derivationPath = Network.DerivationPath.Card(value = "m/44'/60'/0'/0/0")
private val derivationPathHashCode = "-1843072795"
private val converter = NetworkAmountsConverter(blockchainId = rawNetworkId, derivationPath = derivationPath)
private val converter = NetworkAmountsConverter(blockchainId = blockchainId, derivationPath = derivationPath)
@Test
fun convert() {

View file

@ -6,6 +6,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.test.core.ProvideTestModels
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
@ -15,10 +16,14 @@ import org.junit.jupiter.params.ParameterizedTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class NetworkCurrencyIdConverterTest {
private val rawNetworkId = "ethereum"
// Legacy SDK format stored in cache (see NetworkStatusDataModelConverter:
// `value.network.toBlockchain().id`). Runtime CryptoCurrency.ID expects the canonical
// network rawId ("ethereum"), so the converter normalizes via Blockchain.fromId(...).toNetworkId().
private val blockchainId = "ETH"
private val canonicalNetworkRawId = "ethereum"
private val derivationPath = Network.DerivationPath.Card(value = "m/44'/60'/0'/0/0")
private val derivationPathHashCode = "-1843072795"
private val converter = NetworkCurrencyIdConverter(blockchainId = rawNetworkId, derivationPath = derivationPath)
private val converter = NetworkCurrencyIdConverter(blockchainId = blockchainId, derivationPath = derivationPath)
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ -54,13 +59,13 @@ class NetworkCurrencyIdConverterTest {
ConvertModel(
value = CurrencyId.createCoinId(""),
expected = Result.failure(
IllegalStateException("Coin id is null for $rawNetworkId with $derivationPath"),
IllegalStateException("Coin id is null for $blockchainId with $derivationPath"),
),
),
ConvertModel(
value = CurrencyId.createCoinId(" "),
expected = Result.failure(
IllegalStateException("Coin id is null for $rawNetworkId with $derivationPath"),
IllegalStateException("Coin id is null for $blockchainId with $derivationPath"),
),
),
// create token id
@ -184,6 +189,54 @@ class NetworkCurrencyIdConverterTest {
)
}
/**
* Regression coverage for [REDACTED_TASK_KEY]. Cache stores `blockchainId` in the legacy SDK format
* (`Blockchain.id`, e.g. "ETH"), but runtime [CryptoCurrency.ID] is built using the canonical
* network rawId (`Blockchain.toNetworkId()`, e.g. "ethereum"). The converter must bridge the
* two formats so that IDs reconstructed from cache equal those built at runtime otherwise
* `NetworkStatus.Verified.amounts[currency.id]` returns null and the wallet shimmer never clears.
*/
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class LegacyBlockchainIdNormalization {
@Test
fun `convert with legacy ETH blockchainId produces id with canonical ethereum rawId`() {
val cached = CurrencyId.createCoinId("ethereum")
val result = converter.convert(cached)
Truth.assertThat(result)
.isEqualTo(CryptoCurrency.ID.fromValue("coin⟨$canonicalNetworkRawId$derivationPathHashCode⟩ethereum"))
}
@Test
fun `convert with legacy BTC blockchainId produces id with canonical bitcoin rawId`() {
val btcDerivationPath = Network.DerivationPath.Card(value = "m/44'/0'/0'/0/0")
val btcDerivationHash = btcDerivationPath.value.hashCode()
val btcConverter = NetworkCurrencyIdConverter(
blockchainId = "BTC",
derivationPath = btcDerivationPath,
)
val cached = CurrencyId.createCoinId("bitcoin")
val result = btcConverter.convert(cached)
Truth.assertThat(result)
.isEqualTo(CryptoCurrency.ID.fromValue("coin⟨bitcoin→$btcDerivationHash⟩bitcoin"))
}
@Test
fun `convert and convertBack roundtrip preserves CurrencyId`() {
val cached = CurrencyId.createCoinId("ethereum")
val runtimeId = converter.convert(cached)
val roundTrip = converter.convertBack(runtimeId)
Truth.assertThat(roundTrip).isEqualTo(cached)
}
}
data class ConvertModel(val value: CurrencyId, val expected: Result<CryptoCurrency.ID>)
data class ConvertBackModel(val value: CryptoCurrency.ID, val expected: Result<CurrencyId>)

View file

@ -13,10 +13,14 @@ import java.math.BigDecimal
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class NetworkYieldSupplyStatusConverterTest {
private val rawNetworkId = "ethereum"
// Cache stores the SDK-level Blockchain.id (legacy format, e.g. "ETH").
// The converter normalizes it to the canonical network rawId ("ethereum") via
// Blockchain.fromId(...).toNetworkId() so that resulting CryptoCurrency.IDs match those
// built at runtime from Network.rawId.
private val blockchainId = "ETH"
private val derivationPath = Network.DerivationPath.Card(value = "m/44'/60'/0'/0/0")
private val derivationPathHashCode = "-1843072795"
private val converter = NetworkYieldSupplyStatusConverter(rawNetworkId, derivationPath)
private val converter = NetworkYieldSupplyStatusConverter(blockchainId, derivationPath)
private val domainStatus = YieldSupplyStatus(
isActive = true,

View file

@ -96,12 +96,12 @@ internal class SimpleNetworkStatusConverterTest {
),
),
amounts = mapOf(
ID.fromValue("coin⟨ETH→3046160⟩ethereum") to Amount.Loaded(value = BigDecimal.ZERO),
ID.fromValue("token⟨ETH→3046160⟩usdt⚓0x1") to Amount.Loaded(value = BigDecimal.ZERO),
ID.fromValue("coin⟨ethereum→3046160⟩ethereum") to Amount.Loaded(value = BigDecimal.ZERO),
ID.fromValue("token⟨ethereum→3046160⟩usdt⚓0x1") to Amount.Loaded(value = BigDecimal.ZERO),
),
pendingTransactions = emptyMap(),
yieldSupplyStatuses = mapOf(
ID.fromValue("coin⟨ETH→3046160⟩ethereum") to YieldSupplyStatus(
ID.fromValue("coin⟨ethereum→3046160⟩ethereum") to YieldSupplyStatus(
isActive = false,
isInitialized = false,
isAllowedToSpend = false,