Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-04 13:59:41 +04:00
parent 7cda9d6726
commit e5825252f2
5 changed files with 88 additions and 30 deletions

View file

@ -7,6 +7,7 @@ import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.network.NetworkFactory
import com.tangem.data.managetokens.utils.HederaTokenAddressResolver
import com.tangem.data.managetokens.utils.TokenAddressesConverter
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
@ -32,6 +33,7 @@ internal class DefaultCustomTokensRepository(
private val excludedBlockchains: ExcludedBlockchains,
private val dispatchers: CoroutineDispatcherProvider,
private val networkFactory: NetworkFactory,
hederaTokenAddressResolver: HederaTokenAddressResolver,
) : CustomTokensRepository {
private val excludedBlockchainsForCustom = setOf(
@ -40,7 +42,7 @@ internal class DefaultCustomTokensRepository(
)
private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains)
private val tokenAddressConverter = TokenAddressesConverter()
private val tokenAddressConverter = TokenAddressesConverter(hederaTokenAddressResolver)
override suspend fun validateContractAddress(contractAddress: String, networkId: Network.ID): Boolean =
withContext(dispatchers.io) {
@ -53,6 +55,8 @@ internal class DefaultCustomTokensRepository(
Blockchain.TerraV2,
-> true
Blockchain.Cardano,
Blockchain.Hedera,
Blockchain.HederaTestnet,
Blockchain.Sui,
Blockchain.Stellar,
Blockchain.XRP,
@ -89,30 +93,39 @@ internal class DefaultCustomTokensRepository(
.filter(Blockchain::canHandleTokens)
.map(Blockchain::toNetworkId)
val response = tangemTechApi.getCoins(
contractAddress = contractAddress,
networkIds = network.rawId,
active = true,
).getOrThrow()
response.coins.firstNotNullOfOrNull { coin ->
val coinNetwork = coin.networks.firstOrNull { network ->
(network.contractAddress != null || network.decimalCount != null) &&
network.contractAddress.equals(tokenAddress, ignoreCase = true) &&
network.networkId in supportedTokenNetworkIds
val addressesToTry = buildList {
add(tokenAddress)
if (!contractAddress.equals(tokenAddress, ignoreCase = true)) {
add(contractAddress)
}
}
if (coinNetwork != null) {
cryptoCurrencyFactory.createToken(
network = network,
rawId = CryptoCurrency.RawID(coin.id),
name = coin.name,
symbol = coin.symbol,
decimals = requireNotNull(coinNetwork.decimalCount).toInt(),
contractAddress = tokenAddress,
)
} else {
null
addressesToTry.firstNotNullOfOrNull { searchAddress ->
val response = tangemTechApi.getCoins(
contractAddress = searchAddress,
networkIds = network.rawId,
active = true,
).getOrThrow()
response.coins.firstNotNullOfOrNull { coin ->
val coinNetwork = coin.networks.firstOrNull { network ->
(network.contractAddress != null || network.decimalCount != null) &&
network.contractAddress.equals(tokenAddress, ignoreCase = true) &&
network.networkId in supportedTokenNetworkIds
}
if (coinNetwork != null) {
cryptoCurrencyFactory.createToken(
network = network,
rawId = CryptoCurrency.RawID(coin.id),
name = coin.name,
symbol = coin.symbol,
decimals = requireNotNull(coinNetwork.decimalCount).toInt(),
contractAddress = tokenAddress,
)
} else {
null
}
}
}
}

View file

@ -1,10 +1,12 @@
package com.tangem.data.managetokens.di
import com.tangem.blockchainsdk.providers.BlockchainProviderTypesStore
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.common.network.NetworkFactory
import com.tangem.data.managetokens.DefaultCustomTokensRepository
import com.tangem.data.managetokens.DefaultManageTokensRepository
import com.tangem.data.managetokens.utils.HederaTokenAddressResolver
import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.config.testnet.TestnetTokensStorage
@ -54,6 +56,7 @@ internal object ManageTokensDataModule {
dispatchers: CoroutineDispatcherProvider,
excludedBlockchains: ExcludedBlockchains,
networkFactory: NetworkFactory,
blockchainProviderTypesStore: BlockchainProviderTypesStore,
): CustomTokensRepository {
return DefaultCustomTokensRepository(
tangemTechApi = tangemTechApi,
@ -61,6 +64,7 @@ internal object ManageTokensDataModule {
excludedBlockchains = excludedBlockchains,
dispatchers = dispatchers,
networkFactory = networkFactory,
hederaTokenAddressResolver = HederaTokenAddressResolver(blockchainProviderTypesStore),
)
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.data.managetokens.utils
import com.tangem.blockchain.blockchains.hedera.HederaContractIdResolver
import com.tangem.blockchain.blockchains.hedera.HederaTokenAddressConverter
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.network.providers.ProviderType
import com.tangem.blockchainsdk.providers.BlockchainProviderTypesStore
import java.util.concurrent.ConcurrentHashMap
internal class HederaTokenAddressResolver(
private val blockchainProviderTypesStore: BlockchainProviderTypesStore,
) {
private val tokenAddressConverter = HederaTokenAddressConverter()
private val cache = ConcurrentHashMap<String, String>()
suspend fun resolveAddress(blockchain: Blockchain, contractAddress: String): String {
cache[contractAddress.lowercase()]?.let { return it }
val resolver = HederaContractIdResolver(baseUrl = getBaseUrl(blockchain))
val resolved = tokenAddressConverter.resolveTokenId(contractAddress) { resolver.resolve(it) }
require(!resolved.startsWith("0x", ignoreCase = true)) {
"Failed to resolve Hedera contract ID for EVM address: $contractAddress"
}
cache[contractAddress.lowercase()] = resolved
return resolved
}
private fun getBaseUrl(blockchain: Blockchain): String {
val providerTypes = blockchainProviderTypesStore.get().value
return requireNotNull(
providerTypes[blockchain]
?.filterIsInstance<ProviderType.Public>()
?.firstOrNull()
?.url,
) {
"Hedera provider URL not found for $blockchain"
}
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.data.managetokens.utils
import com.tangem.blockchain.blockchains.cardano.CardanoTokenAddressConverter
import com.tangem.blockchain.blockchains.hedera.HederaTokenAddressConverter
import com.tangem.blockchain.blockchains.stellar.StellarTokenAddressConverter
import com.tangem.blockchain.blockchains.sui.SuiTokenAddressConverter
import com.tangem.blockchain.blockchains.xrp.XrpTokenAddressConverter
@ -9,18 +8,19 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.domain.models.network.Network
internal class TokenAddressesConverter {
private val hederaTokenAddressConverter = HederaTokenAddressConverter()
internal class TokenAddressesConverter(
private val hederaTokenAddressResolver: HederaTokenAddressResolver,
) {
private val cardanoTokenAddressConverter = CardanoTokenAddressConverter()
private val xrpTokenAddressConverter = XrpTokenAddressConverter()
private val stellarTokenAddressConverter = StellarTokenAddressConverter()
private val suiTokenAddressConverter = SuiTokenAddressConverter()
fun convertTokenAddress(networkId: Network.ID, contractAddress: String, symbol: String?): String {
val convertedAddress = when (networkId.toBlockchain()) {
suspend fun convertTokenAddress(networkId: Network.ID, contractAddress: String, symbol: String?): String {
val convertedAddress = when (val blockchain = networkId.toBlockchain()) {
Blockchain.Hedera,
Blockchain.HederaTestnet,
-> hederaTokenAddressConverter.convertToTokenId(contractAddress)
-> hederaTokenAddressResolver.resolveAddress(blockchain, contractAddress)
Blockchain.Sui,
Blockchain.SuiTestnet,
-> suiTokenAddressConverter.normalizeAddress(contractAddress)