Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions modules/abstract-eth/src/abstractEthLikeNewCoins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions modules/statics/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
199 changes: 116 additions & 83 deletions modules/statics/src/map.ts
Original file line number Diff line number Diff line change
@@ -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<string, Readonly<BaseCoin>>();
Expand All @@ -12,6 +13,8 @@ export class CoinMap {
private readonly _coinByContractAddress = new Map<string, Readonly<BaseCoin>>();
// map of coin by NFT collection ID -> the key is the (t)family:nftCollectionID
private readonly _coinByNftCollectionID = new Map<string, Readonly<BaseCoin>>();
// Lazily initialized cache for chainId to coin name mapping (derived from network definitions)
private _coinByChainId: Map<number, string> | null = null;

private constructor() {
// Do not instantiate
Expand Down Expand Up @@ -75,89 +78,119 @@ export class CoinMap {
this.addCoin(coin);
}

static coinNameFromChainId(chainId: number): string {
const ethLikeCoinFromChainId: Record<number, string> = {
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',
196: 'okb',
1952: 'tokb',
5734951: 'jovayeth',
2019775: 'tjovayeth',
};
return ethLikeCoinFromChainId[chainId];
/**
* Hardcoded mapping for backward compatibility.
*/
private static readonly LEGACY_CHAIN_ID_MAP: Record<number, string> = {
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',
196: 'okb',
1952: 'tokb',
5734951: 'jovayeth',
2019775: 'tjovayeth',
};

private buildChainIdMap(): Map<number, string> {
const chainIdMap = new Map<number, string>();
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);
}

/**
Expand Down
45 changes: 44 additions & 1 deletion modules/statics/test/unit/coins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,48 @@ describe('CoinMap', function () {
coinMap.has(coin.name).should.be.false();
coinMap.has(newCoin.name).should.be.true();
});

describe('coinNameFromChainId', function () {
it('should return coin name from legacy hardcoded mappings', () => {
// Test backward compatibility with existing hardcoded mappings
const ethCoinName = coins.coinNameFromChainId(1);
should(ethCoinName).not.be.undefined();
ethCoinName!.should.equal('eth');
const polygonCoinName = coins.coinNameFromChainId(137);
should(polygonCoinName).not.be.undefined();
polygonCoinName!.should.equal('polygon');
const arbethCoinName = coins.coinNameFromChainId(42161);
should(arbethCoinName).not.be.undefined();
arbethCoinName!.should.equal('arbeth');
const baseethCoinName = coins.coinNameFromChainId(8453);
should(baseethCoinName).not.be.undefined();
baseethCoinName!.should.equal('baseeth');
});

it('should return coin name from dynamic lookup when not in legacy map', () => {
// Test dynamic lookup for coins defined in networks.ts but not in legacy map
// tdogeos is not present in legacy map but has chainId 6281971 in networks.ts
const coinName = coins.coinNameFromChainId(6281971);
should(coinName).not.be.undefined();
coinName!.should.equal('tdogeos');

// Verify the coin exists and has the correct chainId
const coin = coins.get(coinName!);
const network = coin.network as EthereumNetwork;
network.chainId.should.equal(6281971);
});

it('should return undefined for non-existent chainId', () => {
const result = coins.coinNameFromChainId(999999);
should(result).be.undefined();
});

it('should prioritize legacy mappings over dynamic lookup', () => {
const ethCoinName = coins.coinNameFromChainId(1);
should(ethCoinName).not.be.undefined();
ethCoinName!.should.equal('eth');
});
});
});

coins.forEach((coin, coinName) => {
Expand Down Expand Up @@ -1295,7 +1337,8 @@ describe('create token map using config details', () => {
token?.family.should.eql(coin.family);
token?.decimalPlaces.should.eql(coin.decimalPlaces);
if (token instanceof EthLikeErc20Token) {
(token as EthLikeErc20Token).tokenContractAddress.should.eql(coin?.contractAddress);
const erc20Coin = coin as Erc20Coin;
(token as EthLikeErc20Token).tokenContractAddress.should.eql(erc20Coin.contractAddress);
}
}
});
Expand Down