Skip to content
Draft
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
10 changes: 9 additions & 1 deletion modules/statics/src/coins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, registerErc20Families } from './networkFeatureMapForTokens';
import { ofcErc20Coins, tOfcErc20Coins } from './coins/ofcErc20Coins';
import { ofcHoodethTokens } from './coins/ofcHoodethTokens';
import { ofcCoins } from './coins/ofcCoins';
Expand All @@ -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<string, string> = {};
// Tracks whether each ERC20-supporting family's base coin also supports EIP1559 (e.g. xdc does not).
const erc20FamilySupportsEip1559 = new Map<string, boolean>();

allCoinsAndTokens.forEach((coin) => {
if (
Expand All @@ -62,6 +64,7 @@ allCoinsAndTokens.forEach((coin) => {
!coin.isToken
) {
erc20ChainToNameMap[coin.family] = coin.name;
erc20FamilySupportsEip1559.set(coin.family, coin.features.includes(CoinFeature.EIP1559));
}
});

Expand All @@ -76,6 +79,11 @@ 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(erc20FamilySupportsEip1559);

export function createToken(token: AmsTokenConfig): Readonly<BaseCoin> | undefined {
if (!token.isToken) {
try {
Expand Down
3 changes: 3 additions & 0 deletions modules/statics/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,11 @@ export { CoinMap } from './map';
export {
networkFeatureMapForTokens,
registerNetworkFeatures,
registerErc20Families,
getNetworkFeatures,
getTokenFeatures,
EVM_TOKEN_FEATURES,
EVM_TOKEN_FEATURES_NON_EIP1559,
} from './networkFeatureMapForTokens';
export {
generateErc20Coin,
Expand Down
68 changes: 39 additions & 29 deletions modules/statics/src/networkFeatureMapForTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,44 @@ 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,
];

/** 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 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<[family: string, supportsEip1559: boolean]>): void {
for (const [family, supportsEip1559] of families) {
if (!(family in networkFeatureMapForTokens)) {
networkFeatureMapForTokens[family as CoinFamily] = supportsEip1559
? EVM_TOKEN_FEATURES
: EVM_TOKEN_FEATURES_NON_EIP1559;
}
}
}

/**
* 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 (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);
Expand Down Expand Up @@ -56,36 +90,12 @@ export const networkFeatureMapForTokens: Partial<Record<CoinFamily, CoinFeature[
celo: AccountCoin.DEFAULT_FEATURES,
eth: AccountCoin.DEFAULT_FEATURES,
eos: AccountCoin.DEFAULT_FEATURES,
gasevm: [
...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,
],
gasevm: EVM_TOKEN_FEATURES,
hbar: AccountCoin.DEFAULT_FEATURES,
opeth: [...AccountCoin.DEFAULT_FEATURES, CoinFeature.EIP1559],
katanaeth: [
...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,
],
katanaeth: EVM_TOKEN_FEATURES,
polygon: POLYGON_TOKEN_FEATURES,
scrolleth: [
...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,
],
scrolleth: EVM_TOKEN_FEATURES,
sol: SOL_TOKEN_FEATURES,
stx: STX_TOKEN_FEATURES,
sui: SUI_TOKEN_FEATURES,
Expand Down
59 changes: 38 additions & 21 deletions modules/statics/src/tokenConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<..., EvmTokenBucket>>` 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[];
Expand All @@ -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[];
Expand All @@ -262,7 +279,7 @@ export interface TokenNetwork {
ton: { tokens: JettonTokenConfig[] };
tempo: { tokens: Tip20TokenConfig[] };
canton: { tokens: CantonTokenConfig[] };
}
} & Partial<Record<Exclude<CoinFamily, TokenNetworkExplicitFamily>, EvmTokenBucket>>;

export interface Tokens {
bitcoin: TokenNetwork;
Expand Down
78 changes: 78 additions & 0 deletions modules/statics/test/unit/coins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ import {
EosCoin,
Erc20Coin,
EthereumNetwork,
EVM_TOKEN_FEATURES,
EVM_TOKEN_FEATURES_NON_EIP1559,
getFormattedTokenConfigForCoin,
getFormattedTokens,
getNetworkFeatures,
HederaToken,
KeyCurve,
Networks,
Expand All @@ -24,6 +27,7 @@ import {
SolCoin,
SuiCoin,
tokens,
TrimmedAmsTokenConfig,
UnderlyingAsset,
UtxoCoin,
XrpCoin,
Expand Down Expand Up @@ -1668,6 +1672,80 @@ describe('create token map using config details', () => {
});
});

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('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<Erc20Coin> {
for (const [, coin] of coins) {
Expand Down
24 changes: 24 additions & 0 deletions modules/statics/test/unit/resources/amsTokenConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
},
],
};
28 changes: 28 additions & 0 deletions modules/statics/test/unit/tokenConfigTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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();
});
});
Loading