Skip to content
Closed
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
74 changes: 39 additions & 35 deletions modules/statics/src/coins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -160,20 +160,7 @@ function constructToken(token: AmsTokenConfig): Readonly<BaseCoin> | 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<BaseCoin>;
if (!initializer) {
return undefined;
}

const commonArgs = [
token.id,
Expand All @@ -188,28 +175,45 @@ function constructToken(token: AmsTokenConfig): Readonly<BaseCoin> | 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<BaseCoin>;
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<BaseCoin>;
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':
Expand Down
40 changes: 40 additions & 0 deletions modules/statics/test/unit/coins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
DynamicCoin,
EosCoin,
Erc20Coin,
EthLikeERC721Token,
EthereumNetwork,
EVM_TOKEN_FEATURES,
EVM_TOKEN_FEATURES_NON_EIP1559,
Expand Down Expand Up @@ -41,6 +42,7 @@ import {
amsTokenWithUnsupportedNetwork,
dynamicBaseChainFullConfig,
dynamicTestNetwork,
hbarevmTokenConfigs,
incorrectAmsTokenConfig,
multipleTokensSameFamilyConfig,
reducedAmsTokenConfig,
Expand All @@ -50,6 +52,7 @@ import {
import { EthLikeErc20Token } from '../../../sdk-coin-evm/src';
import {
AccountCoin,
EthLikeERC20Token,
getUnsupportedSolTokenExtensions,
ProgramID,
solToken,
Expand Down Expand Up @@ -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');
Expand Down
51 changes: 51 additions & 0 deletions modules/statics/test/unit/resources/amsTokenConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DynamicNetwork } from '../../../src/networks';
import { TrimmedAmsTokenConfig } from '../../../src/tokenConfig';

export const dynamicTestNetwork = new DynamicNetwork({
name: 'MyDynamicTestnet',
Expand Down Expand Up @@ -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<string, TrimmedAmsTokenConfig[]> = {
'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: [],
},
],
};
Loading