From e4852bc692267d0f99305b7e8c2cf3a5f2dc1112 Mon Sep 17 00:00:00 2001 From: ArunBala-Bitgo Date: Tue, 25 Nov 2025 18:05:50 +0530 Subject: [PATCH] fix: getTokenConstructor to work for testnet tokens + test cases Ticket: WIN-7914 --- modules/bitgo/src/v2/coinFactory.ts | 27 +++++++++++----- .../bitgo/test/v2/resources/amsTokenConfig.ts | 22 +++++++++++++ modules/bitgo/test/v2/unit/ams/ams.ts | 31 +++++++++++++++++++ modules/statics/src/coins.ts | 12 ++++++- modules/statics/src/tokenConfig.ts | 2 ++ modules/statics/test/unit/coins.ts | 19 ++++++++++++ 6 files changed, 105 insertions(+), 8 deletions(-) diff --git a/modules/bitgo/src/v2/coinFactory.ts b/modules/bitgo/src/v2/coinFactory.ts index a8799a2f85..ad00aa679c 100644 --- a/modules/bitgo/src/v2/coinFactory.ts +++ b/modules/bitgo/src/v2/coinFactory.ts @@ -904,8 +904,12 @@ export function getCoinConstructor(coinName: string): CoinConstructor | undefine } } -export const buildEthLikeChainToTestnetMap = (): Record => { - const map: Record = {}; +export const buildEthLikeChainToTestnetMap = (): { + mainnetToTestnetMap: Record; + testnetToMainnetMap: Record; +} => { + const testnetToMainnetMap: Record = {}; + const mainnetToTestnetMap: Record = {}; const enabledEvmCoins = ['ip']; @@ -913,22 +917,31 @@ export const buildEthLikeChainToTestnetMap = (): Record => { coins.forEach((coin) => { if (coin.network.type === NetworkType.TESTNET && !coin.isToken && enabledEvmCoins.includes(coin.family)) { if (coins.get(coin.family)?.features.includes(CoinFeature.SUPPORTS_ERC20)) { - map[coin.family] = `${coin.name}`; + mainnetToTestnetMap[coin.family] = `${coin.name}`; + testnetToMainnetMap[coin.name] = `${coin.family}`; } } }); - return map; + return { mainnetToTestnetMap, testnetToMainnetMap }; }; // TODO: add IP token here and test changes (Ticket: https://bitgoinc.atlassian.net/browse/WIN-7835) -const ethLikeChainToTestnetMap: Record = buildEthLikeChainToTestnetMap(); +const { mainnetToTestnetMap, testnetToMainnetMap } = buildEthLikeChainToTestnetMap(); export function getTokenConstructor(tokenConfig: TokenConfig): CoinConstructor | undefined { - if (tokenConfig.coin in ethLikeChainToTestnetMap) { + const testnetCoin = mainnetToTestnetMap[tokenConfig.coin]; + if (testnetCoin) { return EthLikeErc20Token.createTokenConstructor(tokenConfig as EthLikeTokenConfig, { Mainnet: tokenConfig.coin, - Testnet: ethLikeChainToTestnetMap[tokenConfig.coin], + Testnet: testnetCoin, + }); + } + const mainnetCoin = testnetToMainnetMap[tokenConfig.coin]; + if (mainnetCoin) { + return EthLikeErc20Token.createTokenConstructor(tokenConfig as EthLikeTokenConfig, { + Mainnet: mainnetCoin, + Testnet: tokenConfig.coin, }); } switch (tokenConfig.coin) { diff --git a/modules/bitgo/test/v2/resources/amsTokenConfig.ts b/modules/bitgo/test/v2/resources/amsTokenConfig.ts index 7d13be7e2b..86a28306cc 100644 --- a/modules/bitgo/test/v2/resources/amsTokenConfig.ts +++ b/modules/bitgo/test/v2/resources/amsTokenConfig.ts @@ -21,4 +21,26 @@ export const reducedAmsTokenConfig = { contractAddress: '0x89a959b9184b4f8c8633646d5dfd049d2ebc983a', }, ], + 'tip:faketoken': [ + { + id: 'a1b2c3d4-e5f6-4789-8abc-def123456789', + fullName: 'Story Testnet Faketoken', + name: 'tip:faketoken', + prefix: '', + suffix: 'TIP:FAKETOKEN', + baseUnit: 'wei', + kind: 'crypto', + family: 'ip', + isToken: true, + additionalFeatures: [], + excludedFeatures: [], + decimalPlaces: 18, + asset: 'tip:faketoken', + network: { + name: 'BaseChainTestnet', + }, + primaryKeyCurve: 'secp256k1', + contractAddress: '0x1234567890123456789012345678901234567890', + }, + ], }; diff --git a/modules/bitgo/test/v2/unit/ams/ams.ts b/modules/bitgo/test/v2/unit/ams/ams.ts index 2ad09ad410..764f91d3ce 100644 --- a/modules/bitgo/test/v2/unit/ams/ams.ts +++ b/modules/bitgo/test/v2/unit/ams/ams.ts @@ -100,5 +100,36 @@ describe('Asset metadata service', () => { const coin = bitgo.coin(tokenName); should.exist(coin); }); + + it('should register a EVM coin token from statics library if available', async () => { + const bitgo = TestBitGo.decorate(BitGo, { env: 'mock', microservicesUri, useAms: true } as BitGoOptions); + bitgo.initializeTestVars(); + await bitgo.registerToken('tip:usdc'); + const coin = bitgo.coin('tip:usdc'); + should.exist(coin); + }); + + it('should register an EVM coin token from AMS', async () => { + const bitgo = TestBitGo.decorate(BitGo, { env: 'mock', microservicesUri, useAms: true } as BitGoOptions); + bitgo.initializeTestVars(); + + const tokenName = 'tip:faketoken'; + + // Setup nocks for AMS API call + nock(microservicesUri).get(`/api/v1/assets/name/${tokenName}`).reply(200, reducedAmsTokenConfig[tokenName][0]); + + await bitgo.registerToken(tokenName); + const coin = bitgo.coin(tokenName); + should.exist(coin); + coin.type.should.equal(tokenName); + const staticsCoin = coin.getConfig(); + staticsCoin.name.should.equal('tip'); + staticsCoin.decimalPlaces.should.equal(18); + // For EVM tokens, contractAddress is available on the statics coin + if ('contractAddress' in staticsCoin && staticsCoin.contractAddress) { + (staticsCoin.contractAddress as string).should.equal('0x1234567890123456789012345678901234567890'); + } + staticsCoin.family.should.equal('ip'); + }); }); }); diff --git a/modules/statics/src/coins.ts b/modules/statics/src/coins.ts index b0bafae197..36b869b6c5 100644 --- a/modules/statics/src/coins.ts +++ b/modules/statics/src/coins.ts @@ -137,6 +137,17 @@ export function createToken(token: AmsTokenConfig): Readonly | undefin ]; switch (family) { + case erc20ChainToNameMap[family]: + return initializer( + ...commonArgs.slice(0, 4), // id, name, fullName, decimalPlaces + token.contractAddress || token.tokenAddress, // contractAddress + token.asset, + token.network, + token.features, + token.prefix, + token.suffix, + token.primaryKeyCurve + ); case 'arbeth': case 'avaxc': case 'baseeth': @@ -152,7 +163,6 @@ export function createToken(token: AmsTokenConfig): Readonly | undefin case 'opeth': case 'polygon': case 'trx': - case erc20ChainToNameMap[family]: return initializer( ...commonArgs.slice(0, 4), // id, name, fullName, decimalPlaces token.contractAddress || token.tokenAddress, // contractAddress diff --git a/modules/statics/src/tokenConfig.ts b/modules/statics/src/tokenConfig.ts index 89abc95935..5a8cbccc56 100644 --- a/modules/statics/src/tokenConfig.ts +++ b/modules/statics/src/tokenConfig.ts @@ -1385,6 +1385,8 @@ export function getFormattedTokenConfigForCoin(coin: Readonly): TokenC return getJettonTokenConfig(coin); } else if (coin instanceof FlrERC20Token) { return getFlrTokenConfig(coin); + } else if (coin instanceof EthLikeERC20Token) { + return getEthLikeTokenConfig(coin); } return undefined; } diff --git a/modules/statics/test/unit/coins.ts b/modules/statics/test/unit/coins.ts index 916b7ee0a6..9d483b982c 100644 --- a/modules/statics/test/unit/coins.ts +++ b/modules/statics/test/unit/coins.ts @@ -35,6 +35,8 @@ import { reducedAmsTokenConfig, reducedTokenConfigForAllChains, } from './resources/amsTokenConfig'; +import { EthLikeErc20Token } from '../../../sdk-coin-evm/src'; +import { allCoinsAndTokens } from '../../src/allCoinsAndTokens'; interface DuplicateCoinObject { name: string; @@ -1280,4 +1282,21 @@ describe('create token map using config details', () => { } } }); + + it('should create tokens for all EVM coins using createToken', () => { + const evmCoinTokens = allCoinsAndTokens + .filter((coin) => coin.isToken && coins.get(coin.family)?.features.includes(CoinFeature.SUPPORTS_ERC20)) + .map((coin) => coin); + + for (const coin of evmCoinTokens) { + const token = createToken(coin); + token?.should.not.be.undefined(); + token?.name.should.eql(coin.name); + token?.family.should.eql(coin.family); + token?.decimalPlaces.should.eql(coin.decimalPlaces); + if (token instanceof EthLikeErc20Token) { + (token as EthLikeErc20Token).tokenContractAddress.should.eql(coin?.contractAddress); + } + } + }); });