From e97f24aaf91d50acbaf42e393772411701f03ac4 Mon Sep 17 00:00:00 2001 From: bitgobot Date: Tue, 13 Oct 2026 23:51:48 +0000 Subject: [PATCH] fix(statics): dispatch EVM token creation by token kind The hbarevm family supports both ERC20 and ERC721, so its family name keyed both erc20ChainToNameMap and erc721ChainToNameMap inside createToken. The switch matched the ERC20 case first while the ERC721 loop had already replaced the family's initializer with erc721Token, so ERC721 tokens were constructed with the ERC20 argument layout: `features` landed in the wrong parameter and the coin failed its required account-model feature check. bga then logged "Skipping malformed token" and dropped erc721:hbarevmtoken and hbarevm:mclmongp. Dispatch map-covered families on the token's kind (asset === erc721) instead of on the family so each initializer receives its own argument layout, and drop the initializerMap overrides that made the family maps ambiguous. AMS tokens on hbarevm now create correctly for both kinds with no warnings. Ticket: CECHO-2453 Session-Id: 5215fb23-4ab1-4f77-835c-d7bdddff05cf Task-Id: 30c88a03-cff5-449c-9da3-5ad8a9e65771 --- modules/statics/src/coins.ts | 74 ++++++++++--------- modules/statics/test/unit/coins.ts | 40 ++++++++++ .../test/unit/resources/amsTokenConfig.ts | 51 +++++++++++++ 3 files changed, 130 insertions(+), 35 deletions(-) diff --git a/modules/statics/src/coins.ts b/modules/statics/src/coins.ts index 060b3902933..8526f2ffa09 100644 --- a/modules/statics/src/coins.ts +++ b/modules/statics/src/coins.ts @@ -31,7 +31,7 @@ import { erc721Token, } from './account'; import { ofcToken } from './ofc'; -import { BaseCoin, CoinFeature, DynamicCoin } from './base'; +import { BaseCoin, CoinFeature, DynamicCoin, UnderlyingAsset } from './base'; import { AmsNetworkConfigMap, AmsTokenConfig, TrimmedAmsTokenConfig } from './tokenConfig'; import { CoinMap } from './map'; import { BaseNetwork, getNetwork, getNetworksMap, NetworkType } from './networks'; @@ -160,20 +160,7 @@ function constructToken(token: AmsTokenConfig): Readonly | undefined { } } - // dynamically add erc20 token initializers for eth like chains to the initializer map - Object.keys(erc20ChainToNameMap).forEach((key) => { - initializerMap[key] = erc20Token; - }); - - Object.keys(erc721ChainToNameMap).forEach((key) => { - initializerMap[key] = erc721Token; - }); - const family = token.family; - const initializer = initializerMap[family] as (...args: unknown[]) => Readonly; - if (!initializer) { - return undefined; - } const commonArgs = [ token.id, @@ -188,28 +175,45 @@ function constructToken(token: AmsTokenConfig): Readonly | undefined { token.primaryKeyCurve, ]; + // A family can support both ERC20 and ERC721 tokens (e.g. hbarevm), so for + // families tracked by the ERC20/ERC721 chain maps the token's kind — not the + // family — picks the initializer and its argument layout. Dispatching on the + // family alone invoked erc721Token with the ERC20 layout for such families, + // handing it `token.asset` in place of `features`, which failed the coin's + // required account-model feature check and caused the token to be skipped. + if (erc20ChainToNameMap[family] || erc721ChainToNameMap[family]) { + const isErc721 = token.asset === UnderlyingAsset.ERC721; + const evmInitializer = (isErc721 ? erc721Token : erc20Token) as unknown as ( + ...args: unknown[] + ) => Readonly; + return isErc721 + ? evmInitializer( + ...commonArgs.slice(0, 3), // id, name, fullName + token.contractAddress, // contractAddress + token.network, + token.features, + token.prefix, + token.suffix, + token.primaryKeyCurve + ) + : evmInitializer( + ...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 + ); + } + + const initializer = initializerMap[family] as (...args: unknown[]) => Readonly; + if (!initializer) { + return undefined; + } + 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 erc721ChainToNameMap[family]: - return initializer( - ...commonArgs.slice(0, 3), // id, name, fullName - token.contractAddress, // contractAddress - token.network, - token.features, - token.prefix, - token.suffix, - token.primaryKeyCurve - ); case 'arbeth': case 'avaxc': case 'baseeth': diff --git a/modules/statics/test/unit/coins.ts b/modules/statics/test/unit/coins.ts index f234b255d26..649e0698c65 100644 --- a/modules/statics/test/unit/coins.ts +++ b/modules/statics/test/unit/coins.ts @@ -14,6 +14,7 @@ import { DynamicCoin, EosCoin, Erc20Coin, + EthLikeERC721Token, EthereumNetwork, EVM_TOKEN_FEATURES, EVM_TOKEN_FEATURES_NON_EIP1559, @@ -41,6 +42,7 @@ import { amsTokenWithUnsupportedNetwork, dynamicBaseChainFullConfig, dynamicTestNetwork, + hbarevmTokenConfigs, incorrectAmsTokenConfig, multipleTokensSameFamilyConfig, reducedAmsTokenConfig, @@ -50,6 +52,7 @@ import { import { EthLikeErc20Token } from '../../../sdk-coin-evm/src'; import { AccountCoin, + EthLikeERC20Token, getUnsupportedSolTokenExtensions, ProgramID, solToken, @@ -1874,6 +1877,43 @@ describe('create token map using config details', () => { } } }); + + it('should create hbarevm ERC721 and ERC20 AMS tokens instead of skipping them as malformed', () => { + // Regression for CECHO-2453: the hbarevm family supports both ERC20 and + // ERC721, and dispatching on the family alone invoked erc721Token with the + // ERC20 argument layout, so its `features` parameter received `token.asset` + // and the coin failed the required account-model feature check. Both token + // kinds were then skipped with a 'Skipping malformed token' warning. + const warnings: string[] = []; + const originalWarn = console.warn; + console.warn = (...args: unknown[]) => { + warnings.push(args.join(' ')); + }; + try { + const coinMap = createTokenMapUsingTrimmedConfigDetails(hbarevmTokenConfigs); + + const nft = coinMap.get('hbarevm:freshnft'); + (nft instanceof EthLikeERC721Token).should.be.true(); + nft.decimalPlaces.should.eql(0); + nft.features.should.containEql(CoinFeature.ACCOUNT_MODEL); + (nft as unknown as { contractAddress: string }).contractAddress.should.eql( + '0x1111111111111111111111111111111111111111' + ); + + const erc20 = coinMap.get('hbarevm:fresherc20'); + (erc20 instanceof EthLikeERC20Token).should.be.true(); + erc20.decimalPlaces.should.eql(6); + erc20.features.should.containEql(CoinFeature.ACCOUNT_MODEL); + (erc20 as unknown as { contractAddress: string }).contractAddress.should.eql( + '0x2222222222222222222222222222222222222222' + ); + + warnings.filter((message) => message.includes('Skipping malformed token')).should.be.empty(); + } finally { + console.warn = originalWarn; + } + }); + describe('zkSync Era Features', function () { it('should have ZKSYNCERA_FEATURES for zksyncera', function () { const coin = coins.get('zksyncera'); diff --git a/modules/statics/test/unit/resources/amsTokenConfig.ts b/modules/statics/test/unit/resources/amsTokenConfig.ts index c4d4f495237..e79ac2facb8 100644 --- a/modules/statics/test/unit/resources/amsTokenConfig.ts +++ b/modules/statics/test/unit/resources/amsTokenConfig.ts @@ -1,4 +1,5 @@ import { DynamicNetwork } from '../../../src/networks'; +import { TrimmedAmsTokenConfig } from '../../../src/tokenConfig'; export const dynamicTestNetwork = new DynamicNetwork({ name: 'MyDynamicTestnet', @@ -1324,3 +1325,53 @@ export const reducedTokenConfigForAllChains = { }, ], }; + +// Fresh hbarevm-family tokens (names and UUIDs not present in the static coin +// map) covering both token kinds on a family whose base coin supports both +// ERC20 and ERC721. +export const hbarevmTokenConfigs: Record = { + 'hbarevm:freshnft': [ + { + id: 'b1f0a7e2-6c1d-4f3b-9a2e-0d5c4b3a2c11', + fullName: 'Fresh Hedera EVM NFT', + name: 'hbarevm:freshnft', + prefix: '', + suffix: 'HBAREVM:FRESHNFT', + baseUnit: 'wei', + kind: 'crypto', + family: 'hbarevm', + isToken: true, + decimalPlaces: 0, + asset: 'erc721', + primaryKeyCurve: 'secp256k1', + contractAddress: '0x1111111111111111111111111111111111111111', + network: { + name: 'Hedera EVM', + }, + additionalFeatures: [], + excludedFeatures: [], + }, + ], + 'hbarevm:fresherc20': [ + { + id: 'b1f0a7e2-6c1d-4f3b-9a2e-0d5c4b3a2c12', + fullName: 'Fresh Hedera EVM ERC20', + name: 'hbarevm:fresherc20', + prefix: '', + suffix: 'HBAREVM:FRESHERC20', + baseUnit: 'wei', + kind: 'crypto', + family: 'hbarevm', + isToken: true, + decimalPlaces: 6, + asset: 'hbarevm:fresherc20', + primaryKeyCurve: 'secp256k1', + contractAddress: '0x2222222222222222222222222222222222222222', + network: { + name: 'Hedera EVM', + }, + additionalFeatures: [], + excludedFeatures: [], + }, + ], +};