From b76e2475826f5e7b341b8a248807b869bb2f68a9 Mon Sep 17 00:00:00 2001 From: ArunBala-Bitgo Date: Wed, 19 Nov 2025 11:39:28 +0530 Subject: [PATCH] refactor: code optimisation for erc20 Ticket: WIN-7914 --- modules/bitgo/src/v2/coinFactory.ts | 8 + modules/statics/src/tokenConfig.ts | 683 ++++++------------ modules/statics/test/unit/tokenConfigTests.ts | 484 +++++++++++++ 3 files changed, 714 insertions(+), 461 deletions(-) create mode 100644 modules/statics/test/unit/tokenConfigTests.ts diff --git a/modules/bitgo/src/v2/coinFactory.ts b/modules/bitgo/src/v2/coinFactory.ts index 0f2c815457..1bff1cf45f 100644 --- a/modules/bitgo/src/v2/coinFactory.ts +++ b/modules/bitgo/src/v2/coinFactory.ts @@ -903,7 +903,15 @@ export function getCoinConstructor(coinName: string): CoinConstructor | undefine } } +// TODO: add IP token here and test changes (Ticket: https://bitgoinc.atlassian.net/browse/WIN-7835) +const ethLikeChainToTestnetMap: Record = {}; export function getTokenConstructor(tokenConfig: TokenConfig): CoinConstructor | undefined { + if (tokenConfig.coin in ethLikeChainToTestnetMap) { + return EthLikeErc20Token.createTokenConstructor(tokenConfig as EthLikeTokenConfig, { + Mainnet: tokenConfig.coin, + Testnet: ethLikeChainToTestnetMap[tokenConfig.coin], + }); + } switch (tokenConfig.coin) { case 'eth': case 'hteth': diff --git a/modules/statics/src/tokenConfig.ts b/modules/statics/src/tokenConfig.ts index 65140bc873..7d7eedf4d6 100644 --- a/modules/statics/src/tokenConfig.ts +++ b/modules/statics/src/tokenConfig.ts @@ -33,8 +33,9 @@ import { VetNFTCollection, AdaToken, JettonToken, + AccountCoin, } from './account'; -import { CoinFamily, CoinKind, BaseCoin } from './base'; +import { CoinFamily, CoinKind, BaseCoin, CoinFeature } from './base'; import { coins } from './coins'; import { Networks, NetworkType } from './networks'; import { OfcCoin } from './ofc'; @@ -172,246 +173,59 @@ export type TokenConfig = | TaoTokenConfig | PolyxTokenConfig | JettonTokenConfig; - -export interface Tokens { - bitcoin: { - eth: { - tokens: Erc20TokenConfig[]; - nfts: EthLikeTokenConfig[]; - }; - xlm: { - tokens: StellarTokenConfig[]; - }; - algo: { - tokens: AlgoTokenConfig[]; - }; - ofc: { - tokens: OfcTokenConfig[]; - }; - celo: { - tokens: CeloTokenConfig[]; - }; - eos: { - tokens: EosTokenConfig[]; - }; - avaxc: { - tokens: AvaxcTokenConfig[]; - }; - polygon: { - tokens: EthLikeTokenConfig[]; - }; - soneium: { - tokens: EthLikeTokenConfig[]; - }; - bsc: { - tokens: EthLikeTokenConfig[]; - }; - arbeth: { - tokens: EthLikeTokenConfig[]; - }; - opeth: { - tokens: EthLikeTokenConfig[]; - }; - baseeth: { - tokens: EthLikeTokenConfig[]; - }; - og: { - tokens: EthLikeTokenConfig[]; - }; - flow: { - tokens: EthLikeTokenConfig[]; - }; - lineaeth: { - tokens: EthLikeTokenConfig[]; - }; - seievm: { - tokens: EthLikeTokenConfig[]; - }; - coredao: { - tokens: EthLikeTokenConfig[]; - }; - world: { - tokens: EthLikeTokenConfig[]; - }; - flr: { - tokens: EthLikeTokenConfig[]; - }; - sol: { - tokens: SolTokenConfig[]; - }; - hbar: { - tokens: HbarTokenConfig[]; - }; - ada: { - tokens: AdaTokenConfig[]; - }; - trx: { - tokens: TrxTokenConfig[]; - }; - xrp: { - tokens: XrpTokenConfig[]; - }; - zketh: { - tokens: EthLikeTokenConfig[]; - }; - sui: { - tokens: SuiTokenConfig[]; - }; - tao: { - tokens: TaoTokenConfig[]; - }; - polyx: { - tokens: PolyxTokenConfig[]; - }; - bera: { - tokens: EthLikeTokenConfig[]; - }; - mon: { - tokens: EthLikeTokenConfig[]; - }; - xdc: { - tokens: EthLikeTokenConfig[]; - }; - apt: { - tokens: AptTokenConfig[]; - nftCollections: AptNFTCollectionConfig[]; - }; - stx: { - tokens: Sip10TokenConfig[]; - }; - near: { - tokens: Nep141TokenConfig[]; - }; - vet: { - tokens: VetTokenConfig[]; - nftCollections: VetNFTCollectionConfig[]; - }; - cosmos: { - tokens: CosmosTokenConfig[]; - }; - ton: { - tokens: JettonTokenConfig[]; - }; +export interface TokenNetwork { + eth: { + tokens: Erc20TokenConfig[]; + nfts: EthLikeTokenConfig[]; }; - testnet: { - eth: { - tokens: Erc20TokenConfig[]; - nfts: EthLikeTokenConfig[]; - }; - xlm: { - tokens: StellarTokenConfig[]; - }; - algo: { - tokens: AlgoTokenConfig[]; - }; - ofc: { - tokens: OfcTokenConfig[]; - }; - celo: { - tokens: CeloTokenConfig[]; - }; - bsc: { - tokens: EthLikeTokenConfig[]; - }; - mon: { - tokens: EthLikeTokenConfig[]; - }; - xdc: { - tokens: EthLikeTokenConfig[]; - }; - eos: { - tokens: EosTokenConfig[]; - }; - avaxc: { - tokens: AvaxcTokenConfig[]; - }; - polygon: { - tokens: EthLikeTokenConfig[]; - }; - soneium: { - tokens: EthLikeTokenConfig[]; - }; - arbeth: { - tokens: EthLikeTokenConfig[]; - }; - opeth: { - tokens: EthLikeTokenConfig[]; - }; - baseeth: { - tokens: EthLikeTokenConfig[]; - }; - og: { - tokens: EthLikeTokenConfig[]; - }; - flow: { - tokens: EthLikeTokenConfig[]; - }; - lineaeth: { - tokens: EthLikeTokenConfig[]; - }; - seievm: { - tokens: EthLikeTokenConfig[]; - }; - sol: { - tokens: SolTokenConfig[]; - }; - hbar: { - tokens: HbarTokenConfig[]; - }; - ada: { - tokens: AdaTokenConfig[]; - }; - trx: { - tokens: TrxTokenConfig[]; - }; - xrp: { - tokens: XrpTokenConfig[]; - }; - zketh: { - tokens: EthLikeTokenConfig[]; - }; - sui: { - tokens: SuiTokenConfig[]; - }; - tao: { - tokens: TaoTokenConfig[]; - }; - polyx: { - tokens: PolyxTokenConfig[]; - }; - bera: { - tokens: EthLikeTokenConfig[]; - }; - coredao: { - tokens: EthLikeTokenConfig[]; - }; - world: { - tokens: EthLikeTokenConfig[]; - }; - flr: { - tokens: EthLikeTokenConfig[]; - }; - apt: { - tokens: AptTokenConfig[]; - nftCollections: AptNFTCollectionConfig[]; - }; - stx: { - tokens: Sip10TokenConfig[]; - }; - near: { - tokens: Nep141TokenConfig[]; - }; - vet: { - tokens: VetTokenConfig[]; - nftCollections: VetNFTCollectionConfig[]; - }; - cosmos: { - tokens: CosmosTokenConfig[]; - }; - ton: { - tokens: JettonTokenConfig[]; - }; + xlm: { tokens: StellarTokenConfig[] }; + algo: { tokens: AlgoTokenConfig[] }; + ofc: { tokens: OfcTokenConfig[] }; + celo: { tokens: CeloTokenConfig[] }; + eos: { tokens: EosTokenConfig[] }; + avaxc: { tokens: AvaxcTokenConfig[] }; + polygon: { tokens: EthLikeTokenConfig[] }; + soneium: { tokens: EthLikeTokenConfig[] }; + bsc: { tokens: EthLikeTokenConfig[] }; + arbeth: { tokens: EthLikeTokenConfig[] }; + opeth: { tokens: EthLikeTokenConfig[] }; + baseeth: { tokens: EthLikeTokenConfig[] }; + og: { tokens: EthLikeTokenConfig[] }; + flow: { tokens: EthLikeTokenConfig[] }; + lineaeth: { tokens: EthLikeTokenConfig[] }; + seievm: { tokens: EthLikeTokenConfig[] }; + coredao: { tokens: EthLikeTokenConfig[] }; + world: { tokens: EthLikeTokenConfig[] }; + flr: { tokens: EthLikeTokenConfig[] }; + sol: { tokens: SolTokenConfig[] }; + hbar: { tokens: HbarTokenConfig[] }; + ada: { tokens: AdaTokenConfig[] }; + trx: { tokens: TrxTokenConfig[] }; + xrp: { tokens: XrpTokenConfig[] }; + zketh: { tokens: EthLikeTokenConfig[] }; + sui: { tokens: SuiTokenConfig[] }; + tao: { tokens: TaoTokenConfig[] }; + polyx: { tokens: PolyxTokenConfig[] }; + bera: { tokens: EthLikeTokenConfig[] }; + mon: { tokens: EthLikeTokenConfig[] }; + xdc: { tokens: EthLikeTokenConfig[] }; + apt: { + tokens: AptTokenConfig[]; + nftCollections: AptNFTCollectionConfig[]; + }; + stx: { tokens: Sip10TokenConfig[] }; + near: { tokens: Nep141TokenConfig[] }; + vet: { + tokens: VetTokenConfig[]; + nftCollections: VetNFTCollectionConfig[]; }; + cosmos: { tokens: CosmosTokenConfig[] }; + ton: { tokens: JettonTokenConfig[] }; +} + +export interface Tokens { + bitcoin: TokenNetwork; + testnet: TokenNetwork; } export interface AmsTokenConfig { @@ -1254,259 +1068,206 @@ const getFormattedJettonTokens = (customCoinMap = coins) => return acc; }, []); +function getEthLikeTokenConfig(coin: EthLikeERC20Token): EthLikeTokenConfig { + return { + type: coin.name, + coin: coin.name.split(':')[0].toLowerCase(), + network: coin.network.type === NetworkType.MAINNET ? 'Mainnet' : 'Testnet', + name: coin.fullName, + tokenContractAddress: coin.contractAddress.toString().toLowerCase(), + decimalPlaces: coin.decimalPlaces, + }; +} + +export const getFormattedEthLikeTokenConfig = (customCoinMap = coins): EthLikeTokenConfig[] => + customCoinMap.reduce((acc: EthLikeTokenConfig[], coin) => { + if (coin instanceof EthLikeERC20Token) { + acc.push(getEthLikeTokenConfig(coin)); + } + return acc; + }, []); + +type EthLikeTokenMap = { + [K in CoinFamily]: { tokens: EthLikeTokenConfig[] }; +}; + +/* Get all tokens of a given eth like coin for a given network */ +export const getEthLikeTokens = (network: 'Mainnet' | 'Testnet'): EthLikeTokenMap => { + const networkTokens = getFormattedEthLikeTokenConfig().filter((token) => token.network === network); + const ethLikeTokenMap = {} as EthLikeTokenMap; + // TODO: add IP token here and test changes (Ticket: https://bitgoinc.atlassian.net/browse/WIN-7835) + const enabledChains = ['ip'] as string[]; + + coins.forEach((coin) => { + // TODO: remove enabled chains once changes are done (Ticket: https://bitgoinc.atlassian.net/browse/WIN-7835) + if ( + coin instanceof AccountCoin && + coin.features.includes(CoinFeature.SUPPORTS_ERC20) && + enabledChains.includes(coin.family) + ) { + const coinName = coin.family; + const coinNameForNetwork = network === 'Testnet' ? `t${coinName}` : coinName; + + ethLikeTokenMap[coin.family] = { + tokens: networkTokens.filter((token) => token.coin === coinNameForNetwork), + }; + } + }); + + return ethLikeTokenMap; +}; + +const getFormattedTokensByNetwork = (network: 'Mainnet' | 'Testnet', coinMap: typeof coins) => { + const networkType = network === 'Mainnet' ? NetworkType.MAINNET : NetworkType.TESTNET; + + return { + eth: { + tokens: getFormattedErc20Tokens(coinMap).filter((token) => token.network === network), + nfts: getFormattedErc721Tokens(coinMap).filter((token) => token.network === network), + }, + xlm: { + tokens: getFormattedStellarTokens(coinMap).filter((token) => token.network === network), + }, + algo: { + tokens: getFormattedAlgoTokens(coinMap).filter((token) => token.network === network), + }, + ofc: { + tokens: getFormattedOfcCoins(coinMap).filter((token) => coinMap.get(token.type).network.type === networkType), + }, + celo: { + tokens: getFormattedCeloTokens(coinMap).filter((token) => token.network === network), + }, + bsc: { + tokens: getFormattedBscTokens(coinMap).filter((token) => token.network === network), + }, + eos: { + tokens: getFormattedEosTokens(coinMap).filter((token) => token.network === network), + }, + avaxc: { + tokens: getFormattedAvaxCTokens(coinMap).filter((token) => token.network === network), + }, + polygon: { + tokens: getFormattedPolygonTokens(coinMap).filter((token) => token.network === network), + }, + soneium: { + tokens: getFormattedSoneiumTokens(coinMap).filter((token) => token.network === network), + }, + arbeth: { + tokens: getFormattedArbethTokens(coinMap).filter((token) => token.network === network), + }, + opeth: { + tokens: getFormattedOpethTokens(coinMap).filter((token) => token.network === network), + }, + baseeth: { + tokens: getFormattedBaseethTokens(coinMap).filter((token) => token.network === network), + }, + og: { + tokens: getFormattedOgTokens(coinMap).filter((token) => token.network === network), + }, + flow: { + tokens: getFormattedFlowTokens(coinMap).filter((token) => token.network === network), + }, + mon: { + tokens: getFormattedMonadTokens(coinMap).filter((token) => token.network === network), + }, + xdc: { + tokens: getFormattedXdcTokens(coinMap).filter((token) => token.network === network), + }, + lineaeth: { + tokens: getFormattedLineaethTokens(coinMap).filter((token) => token.network === network), + }, + seievm: { + tokens: getFormattedSeievmTokens(coinMap).filter((token) => token.network === network), + }, + zketh: { + tokens: getFormattedZkethTokens(coinMap).filter((token) => token.network === network), + }, + sol: { + tokens: getFormattedSolTokens(coinMap).filter((token) => token.network === network), + }, + hbar: { + tokens: getFormattedHbarTokens(coinMap).filter((token) => token.network === network), + }, + ada: { + tokens: getFormattedAdaTokens(coinMap).filter((token) => token.network === network), + }, + trx: { + tokens: getFormattedTrxTokens(coinMap).filter((token) => token.network === network), + }, + xrp: { + tokens: getFormattedXrpTokens(coinMap).filter((token) => token.network === network), + }, + sui: { + tokens: getFormattedSuiTokens(coinMap).filter((token) => token.network === network), + }, + tao: { + tokens: getFormattedTaoTokens(coinMap).filter((token) => token.network === network), + }, + polyx: { + tokens: getFormattedPolyxTokens(coinMap).filter((token) => token.network === network), + }, + bera: { + tokens: getFormattedBeraTokens(coinMap).filter((token) => token.network === network), + }, + coredao: { + tokens: getFormattedCoredaoTokens(coinMap).filter((token) => token.network === network), + }, + world: { + tokens: getFormattedWorldTokens(coinMap).filter((token) => token.network === network), + }, + flr: { + tokens: getFormattedFlrTokens(coinMap).filter((token) => token.network === network), + }, + + stx: { + tokens: getFormattedSip10Tokens(coinMap).filter((token) => token.network === network), + }, + near: { + tokens: getFormattedNep141Tokens(coinMap).filter((token) => token.network === network), + }, + cosmos: { + tokens: getFormattedCosmosChainTokens(coinMap).filter((token) => token.network === network), + }, + ton: { + tokens: getFormattedJettonTokens(coinMap).filter((token) => token.network === network), + }, + }; +}; + export const getFormattedTokens = (coinMap = coins): Tokens => { const formattedAptNFTCollections = getFormattedAptNFTCollections(coinMap); const formattedVetNFTCollections = getFormattedVetNFTCollections(coinMap); + return { bitcoin: { - eth: { - tokens: getFormattedErc20Tokens(coinMap).filter((token) => token.network === 'Mainnet'), - nfts: getFormattedErc721Tokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - xlm: { - tokens: getFormattedStellarTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - algo: { - tokens: getFormattedAlgoTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - ofc: { - tokens: getFormattedOfcCoins(coinMap).filter( - (token) => coinMap.get(token.type).network.type === NetworkType.MAINNET - ), - }, - celo: { - tokens: getFormattedCeloTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - bsc: { - tokens: getFormattedBscTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - eos: { - tokens: getFormattedEosTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - avaxc: { - tokens: getFormattedAvaxCTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - polygon: { - tokens: getFormattedPolygonTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - soneium: { - tokens: getFormattedSoneiumTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - arbeth: { - tokens: getFormattedArbethTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - opeth: { - tokens: getFormattedOpethTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - baseeth: { - tokens: getFormattedBaseethTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - og: { - tokens: getFormattedOgTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - flow: { - tokens: getFormattedFlowTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - mon: { - tokens: getFormattedMonadTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - xdc: { - tokens: getFormattedXdcTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - lineaeth: { - tokens: getFormattedLineaethTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - seievm: { - tokens: getFormattedSeievmTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - zketh: { - tokens: getFormattedZkethTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - sol: { - tokens: getFormattedSolTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - hbar: { - tokens: getFormattedHbarTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - ada: { - tokens: getFormattedAdaTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - trx: { - tokens: getFormattedTrxTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - xrp: { - tokens: getFormattedXrpTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - sui: { - tokens: getFormattedSuiTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - tao: { - tokens: getFormattedTaoTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - polyx: { - tokens: getFormattedPolyxTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - bera: { - tokens: getFormattedBeraTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - coredao: { - tokens: getFormattedCoredaoTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - world: { - tokens: getFormattedWorldTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - flr: { - tokens: getFormattedFlrTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, + ...getFormattedTokensByNetwork('Mainnet', coinMap), apt: { tokens: getFormattedAptTokens(coinMap).filter((token) => token.network === 'Mainnet'), nftCollections: formattedAptNFTCollections.filter( (nftCollection: AptNFTCollectionConfig) => nftCollection.network === 'Mainnet' ), }, - stx: { - tokens: getFormattedSip10Tokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - near: { - tokens: getFormattedNep141Tokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, vet: { tokens: getFormattedVetTokens(coinMap).filter((token) => token.network === 'Mainnet'), nftCollections: formattedVetNFTCollections.filter( (nftCollection: VetNFTCollectionConfig) => nftCollection.network === 'Mainnet' ), }, - cosmos: { - tokens: getFormattedCosmosChainTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, - ton: { - tokens: getFormattedJettonTokens(coinMap).filter((token) => token.network === 'Mainnet'), - }, }, testnet: { - eth: { - tokens: getFormattedErc20Tokens(coinMap).filter((token) => token.network === 'Testnet'), - nfts: getFormattedErc721Tokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - xlm: { - tokens: getFormattedStellarTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - algo: { - tokens: getFormattedAlgoTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - ofc: { - tokens: getFormattedOfcCoins(coinMap).filter( - (token) => coinMap.get(token.type).network.type === NetworkType.TESTNET - ), - }, - celo: { - tokens: getFormattedCeloTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - bsc: { - tokens: getFormattedBscTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - eos: { - tokens: getFormattedEosTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - avaxc: { - tokens: getFormattedAvaxCTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - polygon: { - tokens: getFormattedPolygonTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - soneium: { - tokens: getFormattedSoneiumTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - arbeth: { - tokens: getFormattedArbethTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - opeth: { - tokens: getFormattedOpethTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - baseeth: { - tokens: getFormattedBaseethTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - og: { - tokens: getFormattedOgTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - flow: { - tokens: getFormattedFlowTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - mon: { - tokens: getFormattedMonadTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - xdc: { - tokens: getFormattedXdcTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - lineaeth: { - tokens: getFormattedLineaethTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - seievm: { - tokens: getFormattedSeievmTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - zketh: { - tokens: getFormattedZkethTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - sol: { - tokens: getFormattedSolTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - hbar: { - tokens: getFormattedHbarTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - ada: { - tokens: getFormattedAdaTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - trx: { - tokens: getFormattedTrxTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - xrp: { - tokens: getFormattedXrpTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - sui: { - tokens: getFormattedSuiTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - tao: { - tokens: getFormattedTaoTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - polyx: { - tokens: getFormattedPolyxTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - bera: { - tokens: getFormattedBeraTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, + ...getFormattedTokensByNetwork('Testnet', coinMap), apt: { tokens: getFormattedAptTokens(coinMap).filter((token) => token.network === 'Testnet'), nftCollections: formattedAptNFTCollections.filter( (nftCollection: AptNFTCollectionConfig) => nftCollection.network === 'Testnet' ), }, - stx: { - tokens: getFormattedSip10Tokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - coredao: { - tokens: getFormattedCoredaoTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - world: { - tokens: getFormattedWorldTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - flr: { - tokens: getFormattedFlrTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - near: { - tokens: getFormattedNep141Tokens(coinMap).filter((token) => token.network === 'Testnet'), - }, vet: { tokens: getFormattedVetTokens(coinMap).filter((token) => token.network === 'Testnet'), nftCollections: formattedVetNFTCollections.filter( (nftCollection: VetNFTCollectionConfig) => nftCollection.network === 'Testnet' ), }, - cosmos: { - tokens: getFormattedCosmosChainTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, - ton: { - tokens: getFormattedJettonTokens(coinMap).filter((token) => token.network === 'Testnet'), - }, }, }; }; diff --git a/modules/statics/test/unit/tokenConfigTests.ts b/modules/statics/test/unit/tokenConfigTests.ts new file mode 100644 index 0000000000..50486ec48e --- /dev/null +++ b/modules/statics/test/unit/tokenConfigTests.ts @@ -0,0 +1,484 @@ +import 'should'; +import { + coins, + CoinFamily, + Networks, + AccountCoin, + CoinFeature, + UnderlyingAsset, + BaseUnit, + KeyCurve, + CoinMap, +} from '../../src'; +import { + getFormattedEthLikeTokenConfig, + getEthLikeTokens, + getFormattedTokens, + EthLikeTokenConfig, +} from '../../src/tokenConfig'; +import { EthLikeERC20Token } from '../../src/account'; + +describe('EthLike Token Config Functions', function () { + describe('getEthLikeTokenConfig', function () { + it('should convert an EthLikeERC20Token to EthLikeTokenConfig for mainnet', function () { + // Create a mock mainnet EthLikeERC20Token + const mockMainnetToken = new EthLikeERC20Token({ + id: '12345678-1234-4234-8234-123456789012', + name: 'ip:testtoken', + fullName: 'Test Token', + network: Networks.main.ip, + contractAddress: '0x1234567890123456789012345678901234567890', + decimalPlaces: 18, + asset: UnderlyingAsset.IP, + features: [...AccountCoin.DEFAULT_FEATURES, CoinFeature.EIP1559], + prefix: '', + suffix: 'TESTTOKEN', + primaryKeyCurve: KeyCurve.Secp256k1, + isToken: true, + baseUnit: BaseUnit.ETH, + }); + + const config = getFormattedEthLikeTokenConfig(CoinMap.fromCoins([mockMainnetToken]))[0]; + + config.should.not.be.undefined(); + config.type.should.equal('ip:testtoken'); + config.coin.should.equal('ip'); + config.network.should.equal('Mainnet'); + config.name.should.equal('Test Token'); + config.tokenContractAddress.should.equal('0x1234567890123456789012345678901234567890'); + config.decimalPlaces.should.equal(18); + }); + + it('should convert an EthLikeERC20Token to EthLikeTokenConfig for testnet', function () { + // Create a mock testnet EthLikeERC20Token + const mockTestnetToken = new EthLikeERC20Token({ + id: '22345678-2234-4234-9234-223456789012', + name: 'tip:testtoken', + fullName: 'Test Token Testnet', + network: Networks.test.ip, + contractAddress: '0xabcdef1234567890abcdef1234567890abcdef12', + decimalPlaces: 6, + asset: UnderlyingAsset.IP, + features: [...AccountCoin.DEFAULT_FEATURES, CoinFeature.EIP1559], + prefix: '', + suffix: 'TESTTOKEN', + primaryKeyCurve: KeyCurve.Secp256k1, + isToken: true, + baseUnit: BaseUnit.ETH, + }); + + const config = getFormattedEthLikeTokenConfig(CoinMap.fromCoins([mockTestnetToken]))[0]; + + config.should.not.be.undefined(); + config.type.should.equal('tip:testtoken'); + config.coin.should.equal('tip'); + config.network.should.equal('Testnet'); + config.name.should.equal('Test Token Testnet'); + config.tokenContractAddress.should.equal('0xabcdef1234567890abcdef1234567890abcdef12'); + config.decimalPlaces.should.equal(6); + }); + + it('should lowercase the contract address', function () { + const mockToken = new EthLikeERC20Token({ + id: '32345678-3234-4234-a234-323456789012', + name: 'ip:uppercase', + fullName: 'Uppercase Token', + network: Networks.main.ip, + contractAddress: '0xabcdef1234567890abcdef1234567890abcdef12', + decimalPlaces: 18, + asset: UnderlyingAsset.IP, + features: [...AccountCoin.DEFAULT_FEATURES, CoinFeature.EIP1559], + prefix: '', + suffix: 'UPPERCASE', + primaryKeyCurve: KeyCurve.Secp256k1, + isToken: true, + baseUnit: BaseUnit.ETH, + }); + + const config = getFormattedEthLikeTokenConfig(CoinMap.fromCoins([mockToken]))[0]; + + config.tokenContractAddress.should.equal('0xabcdef1234567890abcdef1234567890abcdef12'); + config.tokenContractAddress.should.not.match(/[A-F]/); + }); + + it('should extract coin name from token name using split', function () { + const mockToken = new EthLikeERC20Token({ + id: '42345678-4234-4234-b234-423456789012', + name: 'ip:usdc', + fullName: 'USD Coin', + network: Networks.main.ip, + contractAddress: '0x1234567890123456789012345678901234567890', + decimalPlaces: 6, + asset: UnderlyingAsset.IP, + features: [...AccountCoin.DEFAULT_FEATURES, CoinFeature.EIP1559], + prefix: '', + suffix: 'USDC', + primaryKeyCurve: KeyCurve.Secp256k1, + isToken: true, + baseUnit: BaseUnit.ETH, + }); + + const config = getFormattedEthLikeTokenConfig(CoinMap.fromCoins([mockToken]))[0]; + + config.coin.should.equal('ip'); + config.type.should.equal('ip:usdc'); + }); + }); + + describe('getFormattedEthLikeTokenConfig', function () { + it('should filter only EthLikeERC20Token instances from mixed coin types', function () { + const mockEthLikeToken = new EthLikeERC20Token({ + id: '52345678-5234-4234-8234-523456789012', + name: 'ip:token1', + fullName: 'Token 1', + network: Networks.main.ip, + contractAddress: '0x1111111111111111111111111111111111111111', + decimalPlaces: 18, + asset: UnderlyingAsset.IP, + features: [...AccountCoin.DEFAULT_FEATURES, CoinFeature.EIP1559], + prefix: '', + suffix: 'TOKEN1', + primaryKeyCurve: KeyCurve.Secp256k1, + isToken: true, + baseUnit: BaseUnit.ETH, + }); + + const mockAccountCoin = new AccountCoin({ + id: '62345678-6234-4234-9234-623456789012', + name: 'btc', + fullName: 'Bitcoin', + network: Networks.main.bitcoin, + decimalPlaces: 8, + asset: UnderlyingAsset.BTC, + features: [...AccountCoin.DEFAULT_FEATURES], + prefix: '', + suffix: 'BTC', + primaryKeyCurve: KeyCurve.Secp256k1, + isToken: false, + baseUnit: BaseUnit.BTC, + }); + + const result = getFormattedEthLikeTokenConfig(CoinMap.fromCoins([mockEthLikeToken, mockAccountCoin])); + + result.length.should.equal(1); + result[0].type.should.equal('ip:token1'); + }); + + it('should handle multiple EthLikeERC20Token instances', function () { + const mockToken1 = new EthLikeERC20Token({ + id: '72345678-7234-4234-a234-723456789012', + name: 'ip:token1', + fullName: 'Token 1', + network: Networks.main.ip, + contractAddress: '0x1111111111111111111111111111111111111111', + decimalPlaces: 18, + asset: UnderlyingAsset.IP, + features: [...AccountCoin.DEFAULT_FEATURES, CoinFeature.EIP1559], + prefix: '', + suffix: 'TOKEN1', + primaryKeyCurve: KeyCurve.Secp256k1, + isToken: true, + baseUnit: BaseUnit.ETH, + }); + + const mockToken2 = new EthLikeERC20Token({ + id: '82345678-8234-4234-b234-823456789012', + name: 'tip:token2', + fullName: 'Token 2', + network: Networks.test.ip, + contractAddress: '0x2222222222222222222222222222222222222222', + decimalPlaces: 6, + asset: UnderlyingAsset.IP, + features: [...AccountCoin.DEFAULT_FEATURES, CoinFeature.EIP1559], + prefix: '', + suffix: 'TOKEN2', + primaryKeyCurve: KeyCurve.Secp256k1, + isToken: true, + baseUnit: BaseUnit.ETH, + }); + + const result = getFormattedEthLikeTokenConfig(CoinMap.fromCoins([mockToken1, mockToken2])); + + result.length.should.equal(2); + result[0].type.should.equal('ip:token1'); + result[0].network.should.equal('Mainnet'); + result[1].type.should.equal('tip:token2'); + result[1].network.should.equal('Testnet'); + }); + + it('should use default coins map when no parameter is provided', function () { + const result = getFormattedEthLikeTokenConfig(); + + result.should.be.an.Array(); + // Check that it filters coins from the default coin map + result.forEach((config: EthLikeTokenConfig) => { + config.should.have.property('type'); + config.should.have.property('coin'); + config.should.have.property('network'); + config.should.have.property('name'); + config.should.have.property('tokenContractAddress'); + config.should.have.property('decimalPlaces'); + }); + }); + + it('should return configs with all required properties', function () { + const mockToken = new EthLikeERC20Token({ + id: '92345678-9234-4234-8234-923456789012', + name: 'ip:proptest', + fullName: 'Property Test Token', + network: Networks.main.ip, + contractAddress: '0x3333333333333333333333333333333333333333', + decimalPlaces: 12, + asset: UnderlyingAsset.IP, + features: [...AccountCoin.DEFAULT_FEATURES, CoinFeature.EIP1559], + prefix: '', + suffix: 'PROPTEST', + primaryKeyCurve: KeyCurve.Secp256k1, + isToken: true, + baseUnit: BaseUnit.ETH, + }); + + const result = getFormattedEthLikeTokenConfig(CoinMap.fromCoins([mockToken])); + + result[0].should.have.property('type').which.is.a.String(); + result[0].should.have.property('coin').which.is.a.String(); + result[0].should.have.property('network').which.is.a.String(); + result[0].should.have.property('name').which.is.a.String(); + result[0].should.have.property('tokenContractAddress').which.is.a.String(); + result[0].should.have.property('decimalPlaces').which.is.a.Number(); + }); + }); + + describe('getEthLikeTokens', function () { + it('should return a map with tokens for enabled chains', function () { + const result = getEthLikeTokens('Mainnet'); + + result.should.be.an.Object(); + // The function filters by enabledChains which currently includes 'ip' + if (result.ip) { + result.ip.should.have.property('tokens'); + result.ip.tokens.should.be.an.Array(); + } + }); + + it('should filter mainnet tokens correctly', function () { + const result = getEthLikeTokens('Mainnet'); + + Object.values(result).forEach((chainData) => { + chainData.tokens.forEach((token) => { + token.network.should.equal('Mainnet'); + }); + }); + }); + + it('should filter testnet tokens correctly', function () { + const result = getEthLikeTokens('Testnet'); + + Object.values(result).forEach((chainData) => { + chainData.tokens.forEach((token) => { + token.network.should.equal('Testnet'); + }); + }); + }); + + it('should prepend "t" to coin name for testnet tokens', function () { + const result = getEthLikeTokens('Testnet'); + + if (result.ip && result.ip.tokens.length > 0) { + result.ip.tokens.forEach((token) => { + token.coin.should.equal('tip'); + }); + } + }); + + it('should not prepend "t" to coin name for mainnet tokens', function () { + const result = getEthLikeTokens('Mainnet'); + + if (result.ip && result.ip.tokens.length > 0) { + result.ip.tokens.forEach((token) => { + token.coin.should.equal('ip'); + }); + } + }); + + it('should only include tokens from chains with SUPPORTS_ERC20 feature', function () { + const result = getEthLikeTokens('Mainnet'); + + // Verify that all included chains are AccountCoins with SUPPORTS_ERC20 feature + Object.keys(result).forEach((family) => { + const coin = coins.get(family as CoinFamily); + if (coin) { + coin.should.be.instanceOf(AccountCoin); + coin.features.should.containEql(CoinFeature.SUPPORTS_ERC20); + } + }); + }); + + it('should only include tokens from enabled chains', function () { + const mainnetResult = getEthLikeTokens('Mainnet'); + const testnetResult = getEthLikeTokens('Testnet'); + + // Current implementation only enables 'ip' chain + const enabledChains = ['ip']; + + Object.keys(mainnetResult).forEach((family) => { + enabledChains.should.containEql(family); + }); + + Object.keys(testnetResult).forEach((family) => { + enabledChains.should.containEql(family); + }); + }); + + it('should return empty tokens array for chains without tokens', function () { + const result = getEthLikeTokens('Mainnet'); + + // If a chain is in the result but has no tokens, it should have an empty array + Object.values(result).forEach((chainData) => { + chainData.tokens.should.be.an.Array(); + }); + }); + + it('should group tokens by their coin family', function () { + const result = getEthLikeTokens('Mainnet'); + + if (result.ip && result.ip.tokens.length > 0) { + result.ip.tokens.forEach((token) => { + // All tokens in ip group should have coin 'ip' + token.coin.should.equal('ip'); + }); + } + }); + + it('should return tokens with correct structure', function () { + const mainnetResult = getEthLikeTokens('Mainnet'); + + Object.values(mainnetResult).forEach((chainData) => { + chainData.should.have.property('tokens'); + chainData.tokens.should.be.an.Array(); + + chainData.tokens.forEach((token) => { + token.should.have.property('type'); + token.should.have.property('coin'); + token.should.have.property('network'); + token.should.have.property('name'); + token.should.have.property('tokenContractAddress'); + token.should.have.property('decimalPlaces'); + }); + }); + }); + + it('should handle both Mainnet and Testnet parameters', function () { + const mainnetResult = getEthLikeTokens('Mainnet'); + const testnetResult = getEthLikeTokens('Testnet'); + + mainnetResult.should.be.an.Object(); + testnetResult.should.be.an.Object(); + + // Verify network segregation + Object.values(mainnetResult).forEach((chainData) => { + chainData.tokens.forEach((token) => { + token.network.should.equal('Mainnet'); + }); + }); + + Object.values(testnetResult).forEach((chainData) => { + chainData.tokens.forEach((token) => { + token.network.should.equal('Testnet'); + }); + }); + }); + + it('should not mix mainnet and testnet tokens', function () { + const mainnetResult = getEthLikeTokens('Mainnet'); + const testnetResult = getEthLikeTokens('Testnet'); + + // Get all token types from mainnet + const mainnetTokenTypes = new Set(); + Object.values(mainnetResult).forEach((chainData) => { + chainData.tokens.forEach((token) => { + mainnetTokenTypes.add(token.type); + }); + }); + + // Verify testnet tokens are different (should start with 't' prefix typically) + Object.values(testnetResult).forEach((chainData) => { + chainData.tokens.forEach((token) => { + // Testnet token types should not be in mainnet set + if (mainnetTokenTypes.has(token.type)) { + // This would be an error - same token in both networks + throw new Error(`Token ${token.type} appears in both mainnet and testnet`); + } + }); + }); + }); + }); + + describe('Integration with real coins', function () { + it('should work with EthLikeERC20Token instances from the coins map', function () { + const ethLikeTokens = Array.from(coins).filter((coin) => coin instanceof EthLikeERC20Token); + + if (ethLikeTokens.length > 0) { + const configs = getFormattedEthLikeTokenConfig(coins); + + configs.length.should.be.greaterThanOrEqual(0); + + configs.forEach((config) => { + config.should.have.property('type'); + config.should.have.property('coin'); + config.should.have.property('network'); + config.should.have.property('name'); + config.should.have.property('tokenContractAddress'); + config.should.have.property('decimalPlaces'); + + // Verify network is either Mainnet or Testnet + ['Mainnet', 'Testnet'].should.containEql(config.network); + + // Verify contract address is lowercase + config.tokenContractAddress.should.equal(config.tokenContractAddress.toLowerCase()); + }); + } + }); + + it('should correctly identify EthLikeERC20Token instances in coins map', function () { + let ethLikeTokenCount = 0; + + coins.forEach((coin) => { + if (coin instanceof EthLikeERC20Token) { + ethLikeTokenCount++; + } + }); + + const formattedConfigs = getFormattedEthLikeTokenConfig(coins); + formattedConfigs.length.should.equal(ethLikeTokenCount); + }); + }); + + describe('getFormattedTokens', function () { + it('should return bitcoin and testnet properties with the same keys', function () { + const tokens = getFormattedTokens(); + + tokens.should.have.property('bitcoin'); + tokens.should.have.property('testnet'); + + // Get all keys from bitcoin and testnet + const bitcoinKeys = Object.keys(tokens.bitcoin).sort(); + const testnetKeys = Object.keys(tokens.testnet).sort(); + + // Both should have the same number of keys + bitcoinKeys.length.should.equal(testnetKeys.length); + + // Both should have the exact same keys + bitcoinKeys.should.deepEqual(testnetKeys); + + // Verify all keys are present in both + bitcoinKeys.forEach((key) => { + tokens.testnet.should.have.property(key); + }); + + testnetKeys.forEach((key) => { + tokens.bitcoin.should.have.property(key); + }); + }); + }); +});