diff --git a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts index ebf6c0d623..931f5f388a 100644 --- a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts +++ b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts @@ -48,8 +48,8 @@ import { getDerivationPath } from '@bitgo/sdk-lib-mpc'; import { bip32 } from '@bitgo/secp256k1'; import { BaseCoin as StaticsBaseCoin, + ChainIdNotFoundError, CoinFeature, - CoinMap, coins, EthereumNetwork as EthLikeNetwork, ethGasConfigs, @@ -545,7 +545,10 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { * @returns {EthLikeCommon.default} */ static getCustomChainCommon(chainId: number): EthLikeCommon.default { - const coinName = CoinMap.coinNameFromChainId(chainId); + const coinName = coins.coinNameFromChainId(chainId); + if (!coinName) { + throw new ChainIdNotFoundError(chainId); + } const coin = coins.get(coinName); const ethLikeCommon = getCommon(coin.network as EthLikeNetwork); return ethLikeCommon; diff --git a/modules/statics/src/errors.ts b/modules/statics/src/errors.ts index 046b22576f..07b393d5f8 100644 --- a/modules/statics/src/errors.ts +++ b/modules/statics/src/errors.ts @@ -76,3 +76,10 @@ export class ConflictingCoinFeaturesError extends BitGoStaticsError { Object.setPrototypeOf(this, ConflictingCoinFeaturesError.prototype); } } + +export class ChainIdNotFoundError extends BitGoStaticsError { + public constructor(chainId: number) { + super(`chain '${chainId}' not found`); + Object.setPrototypeOf(this, CoinNotDefinedError.prototype); + } +} diff --git a/modules/statics/src/map.ts b/modules/statics/src/map.ts index a101a21af6..288dbb96b0 100644 --- a/modules/statics/src/map.ts +++ b/modules/statics/src/map.ts @@ -1,6 +1,7 @@ import { BaseCoin } from './base'; import { DuplicateCoinDefinitionError, CoinNotDefinedError, DuplicateCoinIdDefinitionError } from './errors'; import { ContractAddressDefinedToken, NFTCollectionIdDefinedToken } from './account'; +import { EthereumNetwork } from './networks'; export class CoinMap { private readonly _map = new Map>(); @@ -12,6 +13,8 @@ export class CoinMap { private readonly _coinByContractAddress = new Map>(); // map of coin by NFT collection ID -> the key is the (t)family:nftCollectionID private readonly _coinByNftCollectionID = new Map>(); + // Lazily initialized cache for chainId to coin name mapping (derived from network definitions) + private _coinByChainId: Map | null = null; private constructor() { // Do not instantiate @@ -75,85 +78,115 @@ export class CoinMap { this.addCoin(coin); } - static coinNameFromChainId(chainId: number): string { - const ethLikeCoinFromChainId: Record = { - 1: 'eth', - 42: 'teth', - 5: 'gteth', - 560048: 'hteth', - 10001: 'ethw', - 80002: 'tpolygon', - 137: 'polygon', - 56: 'bsc', - 97: 'tbsc', - 42161: 'arbeth', - 421614: 'tarbeth', - 10: 'opeth', - 11155420: 'topeth', - 1116: 'coredao', - 1114: 'tcoredao', - 248: 'oas', - 9372: 'toas', - 14: 'flr', - 114: 'tflr', - 19: 'sgb', - 16: 'tsgb', - 1111: 'wemix', - 1112: 'twemix', - 50: 'xdc', - 51: 'txdc', - 80094: 'bera', - 80069: 'tbera', - 42220: 'celo', - 11142220: 'tcelo', - 2222: 'kava', - 2221: 'tkava', - 43114: 'avax', - 43113: 'tavax', - 100: 'gno', - 130: 'uni', - 324: 'zketh', - 8453: 'baseeth', - 84532: 'tbaseeth', - 30143: 'mon', - 10143: 'tmon', - 480: 'world', - 4801: 'tworld', - 5031: 'somi', - 50312: 'tstt', - 1868: 'soneium', - 1946: 'tsoneium', - 33111: 'tapechain', - 33139: 'apechain', - 688688: 'tphrs', - 102030: 'ctc', - 102031: 'tctc', - 998: 'thypeevm', - 999: 'hypeevm', - 16602: 'tog', - 16661: 'og', - 9746: 'txpl', - 9745: 'xpl', - 14601: 'tsonic', - 146: 'sonic', - 1328: 'tseievm', - 1329: 'seievm', - 1001: 'tkaia', - 8217: 'kaia', - 1270: 'tirys', - 59141: 'tlineaeth', - 59144: 'lineaeth', - 1315: 'tip', - 1514: 'ip', - 545: 'tflow', - 747: 'flow', - 98867: 'tplume', - 98866: 'plume', - 6342: 'tmegaeth', - 295: 'hbarevm', - 296: 'thbarevm', - }; - return ethLikeCoinFromChainId[chainId]; + /** + * Hardcoded mapping for backward compatibility. + */ + private static readonly LEGACY_CHAIN_ID_MAP: Record = { + 1: 'eth', + 42: 'teth', + 5: 'gteth', + 560048: 'hteth', + 10001: 'ethw', + 80002: 'tpolygon', + 137: 'polygon', + 56: 'bsc', + 97: 'tbsc', + 42161: 'arbeth', + 421614: 'tarbeth', + 10: 'opeth', + 11155420: 'topeth', + 1116: 'coredao', + 1114: 'tcoredao', + 248: 'oas', + 9372: 'toas', + 14: 'flr', + 114: 'tflr', + 19: 'sgb', + 16: 'tsgb', + 1111: 'wemix', + 1112: 'twemix', + 50: 'xdc', + 51: 'txdc', + 80094: 'bera', + 80069: 'tbera', + 42220: 'celo', + 11142220: 'tcelo', + 2222: 'kava', + 2221: 'tkava', + 43114: 'avax', + 43113: 'tavax', + 100: 'gno', + 130: 'uni', + 324: 'zketh', + 8453: 'baseeth', + 84532: 'tbaseeth', + 30143: 'mon', + 10143: 'tmon', + 480: 'world', + 4801: 'tworld', + 5031: 'somi', + 50312: 'tstt', + 1868: 'soneium', + 1946: 'tsoneium', + 33111: 'tapechain', + 33139: 'apechain', + 688688: 'tphrs', + 102030: 'ctc', + 102031: 'tctc', + 998: 'thypeevm', + 999: 'hypeevm', + 16602: 'tog', + 16661: 'og', + 9746: 'txpl', + 9745: 'xpl', + 14601: 'tsonic', + 146: 'sonic', + 1328: 'tseievm', + 1329: 'seievm', + 1001: 'tkaia', + 8217: 'kaia', + 1270: 'tirys', + 59141: 'tlineaeth', + 59144: 'lineaeth', + 1315: 'tip', + 1514: 'ip', + 545: 'tflow', + 747: 'flow', + 98867: 'tplume', + 98866: 'plume', + 6342: 'tmegaeth', + 295: 'hbarevm', + 296: 'thbarevm', + }; + + private buildChainIdMap(): Map { + const chainIdMap = new Map(); + this._map.forEach((coin, coinName) => { + // Skip tokens - they share the same chainId as their parent chain + if (coin.isToken) { + return; + } + const network = coin.network; + if ('chainId' in network && typeof (network as EthereumNetwork).chainId === 'number') { + const chainId = (network as EthereumNetwork).chainId; + if (!chainIdMap.has(chainId)) { + chainIdMap.set(chainId, coinName); + } + } + }); + return chainIdMap; + } + + public coinNameFromChainId(chainId: number): string | undefined { + const coinName = CoinMap.LEGACY_CHAIN_ID_MAP[chainId]; + if (coinName) { + return coinName; + } + + if (this._coinByChainId === null) { + this._coinByChainId = this.buildChainIdMap(); + } + return this._coinByChainId.get(chainId); } /**