From 4e9b97788d35c14f8d0247af3608eaf77adadf5c Mon Sep 17 00:00:00 2001 From: prithvishet2503 Date: Thu, 3 Sep 2026 15:21:07 +0530 Subject: [PATCH 1/5] feat(statics): derive EVM token features from ERC20 support for AMS onboarding networkFeatureMapForTokens.ts required every EVM chain family to be hand-added before AMS could onboard ERC20 tokens for it, silently skipping any unlisted family (e.g. baseeth). getNetworkFeatures() now falls back to a shared EVM_TOKEN_FEATURES set for any family whose base coin has CoinFeature.SUPPORTS_ERC20, registered from coins.ts via a callback to avoid a circular import with allCoinsAndTokens.ts. TICKET: CSHLD-1601 --- modules/statics/src/coins.ts | 7 ++- modules/statics/src/index.ts | 2 + .../statics/src/networkFeatureMapForTokens.ts | 63 ++++++++++--------- modules/statics/test/unit/coins.ts | 26 ++++++++ .../test/unit/resources/amsTokenConfig.ts | 24 +++++++ 5 files changed, 91 insertions(+), 31 deletions(-) diff --git a/modules/statics/src/coins.ts b/modules/statics/src/coins.ts index b6aed9d175..17ab16b6e8 100644 --- a/modules/statics/src/coins.ts +++ b/modules/statics/src/coins.ts @@ -35,7 +35,7 @@ import { BaseCoin, CoinFeature, DynamicCoin } from './base'; import { AmsNetworkConfigMap, AmsTokenConfig, TrimmedAmsTokenConfig } from './tokenConfig'; import { CoinMap } from './map'; import { BaseNetwork, getNetwork, getNetworksMap, NetworkType } from './networks'; -import { getNetworkFeatures } from './networkFeatureMapForTokens'; +import { getNetworkFeatures, registerErc20FamilyChecker } from './networkFeatureMapForTokens'; import { ofcErc20Coins, tOfcErc20Coins } from './coins/ofcErc20Coins'; import { ofcHoodethTokens } from './coins/ofcHoodethTokens'; import { ofcCoins } from './coins/ofcCoins'; @@ -76,6 +76,11 @@ allCoinsAndTokens.forEach((coin) => { } }); +// Let getNetworkFeatures() (in networkFeatureMapForTokens.ts) fall back to EVM_TOKEN_FEATURES for any +// family whose base coin supports ERC20, so AMS token onboarding doesn't require hand-maintaining that +// map for every new EVM family (see erc20ChainToNameMap above, built from the same statics data). +registerErc20FamilyChecker((family) => family in erc20ChainToNameMap); + export function createToken(token: AmsTokenConfig): Readonly | undefined { if (!token.isToken) { try { diff --git a/modules/statics/src/index.ts b/modules/statics/src/index.ts index 3394ab8a39..83ab01ff35 100644 --- a/modules/statics/src/index.ts +++ b/modules/statics/src/index.ts @@ -53,8 +53,10 @@ export { CoinMap } from './map'; export { networkFeatureMapForTokens, registerNetworkFeatures, + registerErc20FamilyChecker, getNetworkFeatures, getTokenFeatures, + EVM_TOKEN_FEATURES, } from './networkFeatureMapForTokens'; export { generateErc20Coin, diff --git a/modules/statics/src/networkFeatureMapForTokens.ts b/modules/statics/src/networkFeatureMapForTokens.ts index 9ea4ad163c..54533d200f 100644 --- a/modules/statics/src/networkFeatureMapForTokens.ts +++ b/modules/statics/src/networkFeatureMapForTokens.ts @@ -21,13 +21,40 @@ export function registerNetworkFeatures(family: string, features: CoinFeature[]) dynamicNetworkFeaturesMap.set(family, features); } +/** Default token feature set shared by "plain" EVM-compatible chain families (no bespoke features). */ +export const EVM_TOKEN_FEATURES: CoinFeature[] = [ + ...EVM_FEATURES, + CoinFeature.SHARED_EVM_SIGNING, + CoinFeature.SHARED_EVM_SDK, + CoinFeature.EVM_COMPATIBLE_IMS, + CoinFeature.EVM_COMPATIBLE_UI, + CoinFeature.EVM_COMPATIBLE_WP, + CoinFeature.SUPPORTS_ERC20, +]; + +// Set by coins.ts (which has access to the full coin map) so getNetworkFeatures() can fall back to +// EVM_TOKEN_FEATURES for any family whose base coin supports ERC20, without this module needing to +// import the coin map itself (that would create a circular import: coins.ts -> networkFeatureMapForTokens.ts +// -> allCoinsAndTokens.ts -> coins/botTokens.ts -> networkFeatureMapForTokens.ts). +let isErc20SupportedFamily: ((family: string) => boolean) | undefined; + +/** Register a predicate used to detect whether a family's base coin carries CoinFeature.SUPPORTS_ERC20. */ +export function registerErc20FamilyChecker(checker: (family: string) => boolean): void { + isErc20SupportedFamily = checker; +} + /** * Look up token features for a family. - * Checks static map first, then falls back to dynamic map. - * Returns undefined if the family is not registered in either map. + * Checks the static map first, then the dynamic map, then falls back to EVM_TOKEN_FEATURES for any + * family whose base coin supports ERC20 (see registerErc20FamilyChecker). Returns undefined if the + * family isn't recognized by any of the three. */ export function getNetworkFeatures(family: string): CoinFeature[] | undefined { - return networkFeatureMapForTokens[family as CoinFamily] ?? dynamicNetworkFeaturesMap.get(family); + return ( + networkFeatureMapForTokens[family as CoinFamily] ?? + dynamicNetworkFeaturesMap.get(family) ?? + (isErc20SupportedFamily?.(family) ? EVM_TOKEN_FEATURES : undefined) + ); } /** @@ -56,36 +83,12 @@ export const networkFeatureMapForTokens: Partial { }); }); +describe('getNetworkFeatures EVM fallback (drift guard)', () => { + it('should return EVM_TOKEN_FEATURES for every mainnet family that supports ERC20 but has no explicit entry in networkFeatureMapForTokens', () => { + const erc20Families = new Set( + allCoinsAndTokens + .filter( + (coin) => + !coin.isToken && + coin.network.type === NetworkType.MAINNET && + coin.features.includes(CoinFeature.SUPPORTS_ERC20) + ) + .map((coin) => coin.family) + ); + + erc20Families.forEach((family) => { + const features = getNetworkFeatures(family); + features?.should.not.be.undefined(); + }); + + // baseeth is the concrete gap this fallback closes: it has no hand-written entry in + // networkFeatureMapForTokens, but its base coin supports ERC20. + getNetworkFeatures('baseeth')?.should.deepEqual(EVM_TOKEN_FEATURES); + }); +}); + describe('create token map contract address de-duplication', () => { function firstStaticErc20(): Readonly { for (const [, coin] of coins) { diff --git a/modules/statics/test/unit/resources/amsTokenConfig.ts b/modules/statics/test/unit/resources/amsTokenConfig.ts index 12de379b45..06b5d637e4 100644 --- a/modules/statics/test/unit/resources/amsTokenConfig.ts +++ b/modules/statics/test/unit/resources/amsTokenConfig.ts @@ -1232,4 +1232,28 @@ export const reducedTokenConfigForAllChains = { excludedFeatures: [], }, ], + // 'baseeth' has no explicit entry in networkFeatureMapForTokens; this exercises the + // SUPPORTS_ERC20-derived EVM_TOKEN_FEATURES fallback in getNetworkFeatures(). + 'tbaseeth:faketoken': [ + { + id: 'b3a6f7d2-5c1e-4b9a-8f0d-1e2a3b4c5d6e', + fullName: 'Base Testnet Faketoken', + name: 'tbaseeth:faketoken', + prefix: '', + suffix: 'TBASEETH:FAKETOKEN', + baseUnit: 'wei', + kind: 'crypto', + family: 'baseeth', + isToken: true, + decimalPlaces: 18, + asset: 'tbaseeth:faketoken', + primaryKeyCurve: 'secp256k1', + contractAddress: '0x1234567890abcdef1234567890abcdef12345678', + network: { + name: 'BaseChainTestnet', + }, + additionalFeatures: [], + excludedFeatures: [], + }, + ], }; From 834199291d75dccf9230355cf3949e1e3bd83c06 Mon Sep 17 00:00:00 2001 From: prithvishet2503 Date: Fri, 4 Sep 2026 13:34:06 +0530 Subject: [PATCH 2/5] refactor(statics): backfill networkFeatureMapForTokens directly instead of a predicate fallback Replace the isErc20SupportedFamily predicate/checker in getNetworkFeatures() with registerErc20Families(), which mutates networkFeatureMapForTokens in place for any ERC20-supporting family not already listed. Simpler lookup, same circular-import-safe registration pattern from coins.ts. TICKET: CSHLD-1601 --- modules/statics/src/coins.ts | 10 +++--- modules/statics/src/index.ts | 2 +- .../statics/src/networkFeatureMapForTokens.ts | 33 +++++++++---------- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/modules/statics/src/coins.ts b/modules/statics/src/coins.ts index 17ab16b6e8..fee195414f 100644 --- a/modules/statics/src/coins.ts +++ b/modules/statics/src/coins.ts @@ -35,7 +35,7 @@ import { BaseCoin, CoinFeature, DynamicCoin } from './base'; import { AmsNetworkConfigMap, AmsTokenConfig, TrimmedAmsTokenConfig } from './tokenConfig'; import { CoinMap } from './map'; import { BaseNetwork, getNetwork, getNetworksMap, NetworkType } from './networks'; -import { getNetworkFeatures, registerErc20FamilyChecker } from './networkFeatureMapForTokens'; +import { getNetworkFeatures, registerErc20Families } from './networkFeatureMapForTokens'; import { ofcErc20Coins, tOfcErc20Coins } from './coins/ofcErc20Coins'; import { ofcHoodethTokens } from './coins/ofcHoodethTokens'; import { ofcCoins } from './coins/ofcCoins'; @@ -76,10 +76,10 @@ allCoinsAndTokens.forEach((coin) => { } }); -// Let getNetworkFeatures() (in networkFeatureMapForTokens.ts) fall back to EVM_TOKEN_FEATURES for any -// family whose base coin supports ERC20, so AMS token onboarding doesn't require hand-maintaining that -// map for every new EVM family (see erc20ChainToNameMap above, built from the same statics data). -registerErc20FamilyChecker((family) => family in erc20ChainToNameMap); +// Backfill networkFeatureMapForTokens with EVM_TOKEN_FEATURES for any family whose base coin +// supports ERC20 (see erc20ChainToNameMap above, built from the same statics data), so AMS token +// onboarding doesn't require hand-maintaining that map for every new EVM family. +registerErc20Families(Object.keys(erc20ChainToNameMap)); export function createToken(token: AmsTokenConfig): Readonly | undefined { if (!token.isToken) { diff --git a/modules/statics/src/index.ts b/modules/statics/src/index.ts index 83ab01ff35..bbff515bba 100644 --- a/modules/statics/src/index.ts +++ b/modules/statics/src/index.ts @@ -53,7 +53,7 @@ export { CoinMap } from './map'; export { networkFeatureMapForTokens, registerNetworkFeatures, - registerErc20FamilyChecker, + registerErc20Families, getNetworkFeatures, getTokenFeatures, EVM_TOKEN_FEATURES, diff --git a/modules/statics/src/networkFeatureMapForTokens.ts b/modules/statics/src/networkFeatureMapForTokens.ts index 54533d200f..668f40813e 100644 --- a/modules/statics/src/networkFeatureMapForTokens.ts +++ b/modules/statics/src/networkFeatureMapForTokens.ts @@ -32,29 +32,28 @@ export const EVM_TOKEN_FEATURES: CoinFeature[] = [ CoinFeature.SUPPORTS_ERC20, ]; -// Set by coins.ts (which has access to the full coin map) so getNetworkFeatures() can fall back to -// EVM_TOKEN_FEATURES for any family whose base coin supports ERC20, without this module needing to -// import the coin map itself (that would create a circular import: coins.ts -> networkFeatureMapForTokens.ts -// -> allCoinsAndTokens.ts -> coins/botTokens.ts -> networkFeatureMapForTokens.ts). -let isErc20SupportedFamily: ((family: string) => boolean) | undefined; - -/** Register a predicate used to detect whether a family's base coin carries CoinFeature.SUPPORTS_ERC20. */ -export function registerErc20FamilyChecker(checker: (family: string) => boolean): void { - isErc20SupportedFamily = checker; +/** + * Populate networkFeatureMapForTokens with EVM_TOKEN_FEATURES for every family whose base coin + * carries CoinFeature.SUPPORTS_ERC20 and isn't already explicitly listed below. Called once from + * coins.ts (which has access to the full coin map) so this module doesn't need to import it + * directly (that would create a circular import: coins.ts -> networkFeatureMapForTokens.ts -> + * allCoinsAndTokens.ts -> coins/botTokens.ts -> networkFeatureMapForTokens.ts). + */ +export function registerErc20Families(families: Iterable): void { + for (const family of families) { + if (!(family in networkFeatureMapForTokens)) { + networkFeatureMapForTokens[family as CoinFamily] = EVM_TOKEN_FEATURES; + } + } } /** * Look up token features for a family. - * Checks the static map first, then the dynamic map, then falls back to EVM_TOKEN_FEATURES for any - * family whose base coin supports ERC20 (see registerErc20FamilyChecker). Returns undefined if the - * family isn't recognized by any of the three. + * Checks the static map first (including entries backfilled by registerErc20Families), then the + * dynamic map. Returns undefined if the family isn't recognized by either. */ export function getNetworkFeatures(family: string): CoinFeature[] | undefined { - return ( - networkFeatureMapForTokens[family as CoinFamily] ?? - dynamicNetworkFeaturesMap.get(family) ?? - (isErc20SupportedFamily?.(family) ? EVM_TOKEN_FEATURES : undefined) - ); + return networkFeatureMapForTokens[family as CoinFamily] ?? dynamicNetworkFeaturesMap.get(family); } /** From 122e906820b1c6e92f299e3b24f7730c428775da Mon Sep 17 00:00:00 2001 From: prithvishet2503 Date: Fri, 4 Sep 2026 14:04:44 +0530 Subject: [PATCH 3/5] fix(statics): derive EIP1559 support per family in ERC20 token feature backfill registerErc20Families() previously assigned the same EVM_TOKEN_FEATURES bundle (which includes CoinFeature.EIP1559) to every ERC20-supporting family, regardless of whether that chain actually supports EIP1559 (e.g. xdc does not). Now coins.ts derives EIP1559 support per family from the base coin's own features, and registerErc20Families picks between EVM_TOKEN_FEATURES and the new EVM_TOKEN_FEATURES_NON_EIP1559 accordingly. TICKET: CSHLD-1601 --- modules/statics/src/coins.ts | 5 ++++- modules/statics/src/index.ts | 1 + .../statics/src/networkFeatureMapForTokens.ts | 18 +++++++++++++----- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/modules/statics/src/coins.ts b/modules/statics/src/coins.ts index fee195414f..d5c24593e3 100644 --- a/modules/statics/src/coins.ts +++ b/modules/statics/src/coins.ts @@ -54,6 +54,8 @@ export const coins = CoinMap.fromCoins([ // Build a map of ERC20-supporting chain family names to their mainnet coin names // Maps family -> coin name (e.g., 'ip' -> 'ip') const erc20ChainToNameMap: Record = {}; +// Tracks whether each ERC20-supporting family's base coin also supports EIP1559 (e.g. xdc does not). +const erc20FamilySupportsEip1559 = new Map(); allCoinsAndTokens.forEach((coin) => { if ( @@ -62,6 +64,7 @@ allCoinsAndTokens.forEach((coin) => { !coin.isToken ) { erc20ChainToNameMap[coin.family] = coin.name; + erc20FamilySupportsEip1559.set(coin.family, coin.features.includes(CoinFeature.EIP1559)); } }); @@ -79,7 +82,7 @@ allCoinsAndTokens.forEach((coin) => { // Backfill networkFeatureMapForTokens with EVM_TOKEN_FEATURES for any family whose base coin // supports ERC20 (see erc20ChainToNameMap above, built from the same statics data), so AMS token // onboarding doesn't require hand-maintaining that map for every new EVM family. -registerErc20Families(Object.keys(erc20ChainToNameMap)); +registerErc20Families(erc20FamilySupportsEip1559); export function createToken(token: AmsTokenConfig): Readonly | undefined { if (!token.isToken) { diff --git a/modules/statics/src/index.ts b/modules/statics/src/index.ts index bbff515bba..3b198d807d 100644 --- a/modules/statics/src/index.ts +++ b/modules/statics/src/index.ts @@ -57,6 +57,7 @@ export { getNetworkFeatures, getTokenFeatures, EVM_TOKEN_FEATURES, + EVM_TOKEN_FEATURES_NON_EIP1559, } from './networkFeatureMapForTokens'; export { generateErc20Coin, diff --git a/modules/statics/src/networkFeatureMapForTokens.ts b/modules/statics/src/networkFeatureMapForTokens.ts index 668f40813e..749e12f6aa 100644 --- a/modules/statics/src/networkFeatureMapForTokens.ts +++ b/modules/statics/src/networkFeatureMapForTokens.ts @@ -32,17 +32,25 @@ export const EVM_TOKEN_FEATURES: CoinFeature[] = [ CoinFeature.SUPPORTS_ERC20, ]; +/** Same as EVM_TOKEN_FEATURES, minus EIP1559, for EVM-compatible families that don't support it (e.g. xdc). */ +export const EVM_TOKEN_FEATURES_NON_EIP1559: CoinFeature[] = EVM_TOKEN_FEATURES.filter( + (feature) => feature !== CoinFeature.EIP1559 +); + /** - * Populate networkFeatureMapForTokens with EVM_TOKEN_FEATURES for every family whose base coin - * carries CoinFeature.SUPPORTS_ERC20 and isn't already explicitly listed below. Called once from + * Populate networkFeatureMapForTokens for every family whose base coin carries + * CoinFeature.SUPPORTS_ERC20 and isn't already explicitly listed below, using EVM_TOKEN_FEATURES + * (or its non-EIP1559 variant, mirroring the base coin's own EIP1559 support). Called once from * coins.ts (which has access to the full coin map) so this module doesn't need to import it * directly (that would create a circular import: coins.ts -> networkFeatureMapForTokens.ts -> * allCoinsAndTokens.ts -> coins/botTokens.ts -> networkFeatureMapForTokens.ts). */ -export function registerErc20Families(families: Iterable): void { - for (const family of families) { +export function registerErc20Families(families: Iterable<[family: string, supportsEip1559: boolean]>): void { + for (const [family, supportsEip1559] of families) { if (!(family in networkFeatureMapForTokens)) { - networkFeatureMapForTokens[family as CoinFamily] = EVM_TOKEN_FEATURES; + networkFeatureMapForTokens[family as CoinFamily] = supportsEip1559 + ? EVM_TOKEN_FEATURES + : EVM_TOKEN_FEATURES_NON_EIP1559; } } } From 5ceb56c526729f91473ff60b9c0b6eb3f0bc5253 Mon Sep 17 00:00:00 2001 From: prithvishet2503 Date: Fri, 4 Sep 2026 15:09:30 +0530 Subject: [PATCH 4/5] fix(statics): derive TokenNetwork EVM family coverage from SUPPORTS_ERC20, add drift guard TokenNetwork hand-listed ~19 "plain EVM" families (polygon, baseeth, og, flow, xdc, ...) with an identical { tokens: EthLikeTokenConfig[] } shape, so any new EVM family (e.g. zksyncera, mantle, gasevm) failed to type-check even though getFormattedTokensByNetwork already populates a bucket for it at runtime via getEthLikeTokens's SUPPORTS_ERC20-based scan. Replaced the hand-listed EVM entries with a Partial> overlay covering every CoinFamily not otherwise explicitly shaped (NFTs, confidential tokens, MPT tokens, non-EVM configs). Also added a drift-guard test asserting every mainnet SUPPORTS_ERC20 family gets a bucket in getFormattedTokens's output, mirroring the existing getNetworkFeatures drift guard. TICKET: CSHLD-1601 --- modules/statics/src/tokenConfig.ts | 59 ++++++++++++------- modules/statics/test/unit/tokenConfigTests.ts | 28 +++++++++ 2 files changed, 66 insertions(+), 21 deletions(-) diff --git a/modules/statics/src/tokenConfig.ts b/modules/statics/src/tokenConfig.ts index ea8ab62564..b15d9c0e4d 100644 --- a/modules/statics/src/tokenConfig.ts +++ b/modules/statics/src/tokenConfig.ts @@ -209,7 +209,43 @@ export type TokenConfig = | Tip20TokenConfig | Erc7984TokenConfig; -export interface TokenNetwork { +/** Default bucket shape for a "plain" EVM-compatible family (ERC20 tokens only, no NFTs/confidential tokens). */ +export type EvmTokenBucket = { tokens: EthLikeTokenConfig[] }; + +/** Families with a non-generic bucket shape (NFTs, confidential tokens, MPT tokens, or a non-EVM token config type). */ +type TokenNetworkExplicitFamily = + | CoinFamily.ETH + | CoinFamily.XLM + | CoinFamily.ALGO + | CoinFamily.OFC + | CoinFamily.CELO + | CoinFamily.EOS + | CoinFamily.AVAXC + | CoinFamily.SOL + | CoinFamily.HBAR + | CoinFamily.ADA + | CoinFamily.TRX + | CoinFamily.XRP + | CoinFamily.SUI + | CoinFamily.TAO + | CoinFamily.POLYX + | CoinFamily.APT + | CoinFamily.STX + | CoinFamily.NEAR + | CoinFamily.VET + | CoinFamily.TON + | CoinFamily.TEMPO + | CoinFamily.CANTON; + +/** + * Explicitly-shaped families above, intersected with a `Partial>` covering + * every other family so that any EVM family not listed here (current or future, e.g. + * gasevm/katanaeth/scrolleth/zksyncera/mantle/...) still type-checks — `getFormattedTokensByNetwork` + * already populates a bucket for every family whose base coin carries `CoinFeature.SUPPORTS_ERC20`/ + * `SUPPORTS_ERC721` via `getEthLikeTokens`, so the type should not require hand-enumerating every such + * family either. + */ +export type TokenNetwork = { eth: { tokens: Erc20TokenConfig[]; nfts: EthLikeTokenConfig[]; @@ -221,33 +257,14 @@ export interface TokenNetwork { 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[]; mptTokens: XrpMptTokenConfig[] }; - zketh: { tokens: EthLikeTokenConfig[] }; sui: { tokens: SuiTokenConfig[] }; tao: { tokens: TaoTokenConfig[] }; polyx: { tokens: PolyxTokenConfig[] }; - bera: { tokens: EthLikeTokenConfig[] }; - mon: { tokens: EthLikeTokenConfig[] }; - xdc: { tokens: EthLikeTokenConfig[] }; - hypeevm: { tokens: EthLikeTokenConfig[] }; - ip: { tokens: EthLikeTokenConfig[] }; apt: { tokens: AptTokenConfig[]; nftCollections: AptNFTCollectionConfig[]; @@ -262,7 +279,7 @@ export interface TokenNetwork { ton: { tokens: JettonTokenConfig[] }; tempo: { tokens: Tip20TokenConfig[] }; canton: { tokens: CantonTokenConfig[] }; -} +} & Partial, EvmTokenBucket>>; export interface Tokens { bitcoin: TokenNetwork; diff --git a/modules/statics/test/unit/tokenConfigTests.ts b/modules/statics/test/unit/tokenConfigTests.ts index 590629bb34..383e65df63 100644 --- a/modules/statics/test/unit/tokenConfigTests.ts +++ b/modules/statics/test/unit/tokenConfigTests.ts @@ -22,6 +22,8 @@ import { BaseContractAddressConfig, } from '../../src/tokenConfig'; import { EthLikeERC20Token } from '../../src/account'; +import { allCoinsAndTokens } from '../../src/allCoinsAndTokens'; +import { NetworkType } from '../../src/networks'; describe('EthLike Token Config Functions', function () { describe('getEthLikeTokenConfig', function () { @@ -762,3 +764,29 @@ describe('EthLike Token Config Functions', function () { }); }); }); + +describe('getFormattedTokensByNetwork EVM family coverage (drift guard)', () => { + it('should emit a bucket for every mainnet family that supports ERC20, even without a hand-written entry', () => { + const erc20Families = new Set( + allCoinsAndTokens + .filter( + (coin) => + !coin.isToken && + coin.network.type === NetworkType.MAINNET && + coin.features.includes(CoinFeature.SUPPORTS_ERC20) + ) + .map((coin) => coin.family) + ); + + const formattedTokens = getFormattedTokens(); + + erc20Families.forEach((family) => { + should(formattedTokens.bitcoin[family]).not.be.undefined(); + should(formattedTokens.bitcoin[family]?.tokens).be.an.Array(); + }); + + // baseeth is the concrete gap this closes: it has no hand-written entry in + // getFormattedTokensByNetwork's returned object, but its base coin supports ERC20. + should(formattedTokens.bitcoin.baseeth).not.be.undefined(); + }); +}); From af5f2dbeaeef88cee777a7d88f94dc12b8353bde Mon Sep 17 00:00:00 2001 From: prithvishet2503 Date: Fri, 4 Sep 2026 16:14:40 +0530 Subject: [PATCH 5/5] test(statics): assert AMS token feature composition for EVM fallback families Verifies createTokenUsingTrimmedConfigDetails correctly composes EVM_TOKEN_FEATURES/EVM_TOKEN_FEATURES_NON_EIP1559 with AMS additionalFeatures/excludedFeatures for baseeth and prividiumeth, the two fallback families exercising each branch of the EIP1559 split. TICKET: CSHLD-1601 --- modules/statics/test/unit/coins.ts | 52 ++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/modules/statics/test/unit/coins.ts b/modules/statics/test/unit/coins.ts index 148db64a18..e185819296 100644 --- a/modules/statics/test/unit/coins.ts +++ b/modules/statics/test/unit/coins.ts @@ -15,6 +15,7 @@ import { Erc20Coin, EthereumNetwork, EVM_TOKEN_FEATURES, + EVM_TOKEN_FEATURES_NON_EIP1559, getFormattedTokenConfigForCoin, getFormattedTokens, getNetworkFeatures, @@ -26,6 +27,7 @@ import { SolCoin, SuiCoin, tokens, + TrimmedAmsTokenConfig, UnderlyingAsset, UtxoCoin, XrpCoin, @@ -1694,6 +1696,56 @@ describe('getNetworkFeatures EVM fallback (drift guard)', () => { }); }); +describe('AMS token feature composition for EVM fallback families (drift guard)', () => { + function trimmedConfigFor(family: string, networkName: string): TrimmedAmsTokenConfig { + return { + id: 'f1a6f7d2-5c1e-4b9a-8f0d-1e2a3b4c5d6f', + fullName: `${family} Faketoken`, + name: `t${family}:faketoken`, + prefix: '', + suffix: `T${family.toUpperCase()}:FAKETOKEN`, + baseUnit: 'wei', + kind: 'crypto', + family, + isToken: true, + decimalPlaces: 18, + asset: `t${family}:faketoken`, + primaryKeyCurve: 'secp256k1', + contractAddress: '0x1234567890abcdef1234567890abcdef12345678', + network: { name: networkName }, + additionalFeatures: [CoinFeature.STAKING], + excludedFeatures: [CoinFeature.SHARED_EVM_SDK], + }; + } + + it('should compose EVM_TOKEN_FEATURES + additionalFeatures - excludedFeatures for baseeth (EIP1559-supporting fallback family)', () => { + const token = createTokenUsingTrimmedConfigDetails(trimmedConfigFor('baseeth', 'BaseChainTestnet')); + token?.should.not.be.undefined(); + + const expectedFeatures = new Set(EVM_TOKEN_FEATURES); + expectedFeatures.add(CoinFeature.STAKING); + expectedFeatures.delete(CoinFeature.SHARED_EVM_SDK); + + token?.features.should.have.length(expectedFeatures.size); + expectedFeatures.forEach((feature) => token?.features.should.containEql(feature)); + token?.features.should.not.containEql(CoinFeature.SHARED_EVM_SDK); + }); + + it('should compose EVM_TOKEN_FEATURES_NON_EIP1559 + additionalFeatures - excludedFeatures for prividiumeth (non-EIP1559 fallback family)', () => { + const token = createTokenUsingTrimmedConfigDetails(trimmedConfigFor('prividiumeth', 'Prividium Ethereum Testnet')); + token?.should.not.be.undefined(); + + const expectedFeatures = new Set(EVM_TOKEN_FEATURES_NON_EIP1559); + expectedFeatures.add(CoinFeature.STAKING); + expectedFeatures.delete(CoinFeature.SHARED_EVM_SDK); + + token?.features.should.have.length(expectedFeatures.size); + expectedFeatures.forEach((feature) => token?.features.should.containEql(feature)); + token?.features.should.not.containEql(CoinFeature.EIP1559); + token?.features.should.not.containEql(CoinFeature.SHARED_EVM_SDK); + }); +}); + describe('create token map contract address de-duplication', () => { function firstStaticErc20(): Readonly { for (const [, coin] of coins) {