fix(client-utils): resolve native metadata from chainlist - #9729
Conversation
Stop scraping native symbol from valueTransfers for fees and STANDARD sends. Use eth-chainlist (chainId → slip44/symbol) with @metamask/slip44 symbol fallback so ERC-20-only txs and empty-transfer STANDARD sends keep native fee/token metadata. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
78e80b0 to
d8c5dfe
Compare
d8c5dfe to
bce593f
Compare
| to: apiTransactionFixtures.addresses.zeroValueSendRecipient, | ||
| token: { | ||
| direction: 'out', | ||
| amount: '0', |
|
Review: PR #9729 — What it does: replaces "scrape native symbol from Verified: ran the branch's suite — 257 tests pass, 100% coverage. Probed 🔴 Sev0 — fix before mergeBoth symptoms share one root cause: the two resolvers rank slip44 sources in opposite orders.
Sev0-1 · Every testnet native token gets
|
| Chain | synthesized token assetId |
fee assetId |
canonical |
|---|---|---|---|
| Sepolia (11155111) | /slip44:1 ❌ |
/slip44:60 ✅ |
60 |
| Base Sepolia (84532) | /slip44:1 ❌ |
/slip44:60 ✅ |
60 |
| Avalanche (43114) | /slip44:9005 ✅ |
/slip44:9000 ❌ |
9005 |
| BSC testnet (97) | /slip44:1 ❌ |
/slip44:1 ❌ |
— |
Avalanche is the mirror image of the testnet case: chainlist's 9005 is what the repo already uses (token-prices-service/codefi-v2.ts, evm-rpc-services/services/BalanceFetcher.test.ts), but the fee path's symbol lookup returns 9000 (AVAX in the slip44 registry). The PR now has the right value and discards it.
Fix for both — chainlist-first, skipping the testnet coin type:
// caip.ts — getNativeAsset
const assetReference =
typeof slip44 === 'number' && slip44 !== 1
? String(slip44)
: getCoinType(nativeCurrency.symbol);…then invert the fee path to match: nativeAsset?.assetId ?? resolveNativeAssetId(chainId, symbol). Result: 9005 for Avalanche, 60 for Sepolia, token and fee agree.
🟠 Sev1 — should fix in this PR
Fee decimals now sourced from chainlist
toNetworkFee changed decimals: nativeTokenDecimals → nativeAsset?.decimals ?? nativeTokenDecimals. But gasUsed × effectiveGasPrice and tx.value are wei-denominated by EVM protocol, while 29 chainlist entries declare non-18 native decimals.
Probed chain 4160: fee
amount: "21000000000000"withdecimals: 6→ renders ~21M ALGO instead of 0.000021.
Fringe chains, but a regression from a previously-correct constant. Keep 18 for wei amounts; use chainlist decimals only where the amount is genuinely token-denominated.
getChainById is an uncached O(n) scan, called per transaction
The upstream implementation is chainData.filter(c => c.chainId === id)[0] — full 2,664-entry scan, intermediate array allocation, no short-circuit, no cache. Now called ≥2× per transaction (getFees → toNetworkFee → getNativeAsset, plus mapper synthesis).
A 100-item activity list ≈ 500k comparisons and ~200 throwaway arrays.
Memoize in caip.ts with a Map<number, NativeAsset | undefined> regardless of what else changes.
1.4 MB of JSON added to a published client package
eth-chainlist@0.0.795 is 1.65 MB unpacked (1.4 MB JSON), CJS, with a top-level require('./data/chain') — not tree-shakeable. The whole dataset lands in extension/mobile bundles and parses at startup, for what is effectively a chainId → {symbol, decimals, slip44} map.
The monorepo already solves this with a small seeded map plus a chainid.network fill: packages/assets-controller/src/utils/native-assets.ts. A static map over MetaMask-supported chains drops both the 1.4 MB and the supply-chain surface below.
🟡 Sev2 — worth addressing
| Finding | Detail |
|---|---|
| Supply-chain surface | Pre-1.0 (0.0.795), single-maintainer, auto-generated third-party package with no published types — hence the hand-rolled types/eth-chainlist.d.ts — added as a runtime dependency of a published @metamask/* package. Deserves an explicit decision, not an implicit one. |
| Unsafe cast regression | caip.ts: set('POL', coinTypeBySymbol.get('MATIC') as string) replaces a guarded conditional. Inserts a key with an undefined value if MATIC leaves the registry, and clobbers a real POL entry if one is added. The prior if (maticCoinType) was better. |
| Tests coupled to live data | caip.test.ts asserts dataset properties — "arbitrum omits slip44", eip155:1088 fails both lookups, eip155:999999991 unknown. Chainlist republishes ~daily (795 versions) and only 784/2,664 entries have slip44 today, so a routine bump silently flips these. The file already mocks getChainById — use it here. |
| Test proves the opposite of its title | transactions.test.ts "resolves native fee metadata from chain id" for chainId 1337: chainlist says slip44: 1 for Geth Testnet, so the asserted /slip44:60 comes from the symbol path. |
| Coverage gap | The deleted mapsAStandardInboundNativeTransfer fixture covered inbound STANDARD-without-transfers, which now synthesizes a token with direction: 'in'. Line coverage still reads 100%, but that behavior is unasserted — it's the receive-side counterpart of the headline change. |
⚪ Sev3 — nits
- Rename churn:
toNetworkFeeAmount→calculateNetworkFee,buildBaseNetworkFee→toNetworkFee,slip44CoinTypeBySymbol→slip44BySymbol,slip44CoinTypeForSymbol→getCoinTypeare pure renames that obscure the behavioral diff in a fix PR.getCoinTypeis also vaguer than the name it replaced. - Missing JSDoc on the new exported
getNativeAsset. Not a lint failure (jsdoc/require-jsdocis off;resolveNativeAssetIdlacks one too), but this is precisely the function whose slip44 precedence needs documenting. - Dead type surface:
types/eth-chainlist.d.tsdeclaresgetChainByNetworkId,getChainByName,getChainByShortName,rawChainData— none used. MarkingnativeCurrency?optional is the right defensive call for untyped JSON;decimalsshould get the same treatment.
Verdict
Direction is right and the zero-value-send fix is real. Block on Sev0 — those ship wrong asset IDs on every testnet and on Avalanche, and the one-line slip44 !== 1 guard plus a precedence flip fixes both. Get an explicit call on the 1.4 MB dependency (Sev1) before merge. Sev2/Sev3 are follow-up-able.
Ignore eth-chainlist slip44:1 (Testnet coin type) so Sepolia/etc. use ETH slip44:60. Prefer getNativeAsset for fee assetIds so Avalanche stays 9005 instead of the registry's AVAX 9000. Co-authored-by: Cursor <cursoragent@cursor.com>
6889323 to
a0d39a1
Compare
…back Keep wei decimals on network fees, skip chainlist testnet slip44:1, restore guarded POL alias, mock chainlist in unit tests, cover inbound STANDARD synthesis, and trim eth-chainlist typings. Co-authored-by: Cursor <cursoragent@cursor.com>
a0d39a1 to
b5147cf
Compare
Explanation
In the API response of zero-value sends, there is no valueTransfers and we assume native token is involved.
This adds a lookup using chainlist to fill in the assetId of the native token and fees involved.
References
Checklist
Note
Medium Risk
Changes how activity items expose native tokens and gas fees for indexed and local transactions, which can alter UI labels and asset IDs for consumers without touching auth or funds.
Overview
Adds
eth-chainlistso native symbol, decimals, and slip44assetIdcome from the chain id (with@metamask/slip44when chainlist omits slip44), instead of inferring native fees fromvalueTransfers.Network fees and local transaction fees now attach native symbol/
assetIdfrom that lookup; unknown chains still get fee amounts only.For API transaction mapping,
STANDARDtxs with emptyvalueTransfersget a synthesized native token fromtx.value(e.g. zero-value sends).TRANSFERwith no transfers still omits a token. ERC-20 rows missingcontractAddresscan pick upassetIdfromtx.towhen encodable;withFallbackTokenAssetIdis removed in favor of inline logic. Receive vs send also treatstx.to === subjectas receive when appropriate.Reviewed by Cursor Bugbot for commit bce593f. Bugbot is set up for automated code reviews on this repo. Configure here.