Skip to content

fix(client-utils): resolve native metadata from chainlist - #9729

Open
n3ps wants to merge 6 commits into
mainfrom
n3ps/client-utils-native-fee-fallback
Open

fix(client-utils): resolve native metadata from chainlist#9729
n3ps wants to merge 6 commits into
mainfrom
n3ps/client-utils-native-fee-fallback

Conversation

@n3ps

@n3ps n3ps commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

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

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

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-chainlist so native symbol, decimals, and slip44 assetId come from the chain id (with @metamask/slip44 when chainlist omits slip44), instead of inferring native fees from valueTransfers.

Network fees and local transaction fees now attach native symbol/assetId from that lookup; unknown chains still get fee amounts only.

For API transaction mapping, STANDARD txs with empty valueTransfers get a synthesized native token from tx.value (e.g. zero-value sends). TRANSFER with no transfers still omits a token. ERC-20 rows missing contractAddress can pick up assetId from tx.to when encodable; withFallbackTokenAssetId is removed in favor of inline logic. Receive vs send also treats tx.to === subject as receive when appropriate.

Reviewed by Cursor Bugbot for commit bce593f. Bugbot is set up for automated code reviews on this repo. Configure here.

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>
@socket-security

socket-security Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedeth-chainlist@​0.0.795991008996100

View full report

n3ps and others added 2 commits July 31, 2026 01:07
Co-authored-by: Cursor <cursoragent@cursor.com>
@n3ps
n3ps marked this pull request as ready for review July 31, 2026 17:47
@n3ps
n3ps requested review from a team as code owners July 31, 2026 17:47
@n3ps
n3ps temporarily deployed to default-branch July 31, 2026 17:47 — with GitHub Actions Inactive
@n3ps n3ps changed the title fix(client-utils): resolve native fee/token metadata from eth-chainlist fix(client-utils): resolve native metadata from chainlist Jul 31, 2026
@n3ps
n3ps force-pushed the n3ps/client-utils-native-fee-fallback branch from 78e80b0 to d8c5dfe Compare July 31, 2026 18:12
@n3ps
n3ps force-pushed the n3ps/client-utils-native-fee-fallback branch from d8c5dfe to bce593f Compare July 31, 2026 18:16
to: apiTransactionFixtures.addresses.zeroValueSendRecipient,
token: {
direction: 'out',
amount: '0',

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zero value sends

@vinnyhoward

vinnyhoward commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review: PR #9729fix(client-utils): resolve native metadata from chainlist

What it does: replaces "scrape native symbol from valueTransfers" with a chainId → eth-chainlist lookup (getNativeAsset) for native symbol/decimals/slip44, falling back to @metamask/slip44 by symbol; synthesizes a native token for STANDARD txs with empty valueTransfers (the zero-value-send case).

Verified: ran the branch's suite — 257 tests pass, 100% coverage. Probed getNativeAsset against the real 2,664-entry chainlist dataset. withFallbackTokenAssetId was never in src/index.ts, so its removal is not breaking. Changelog is correct.


🔴 Sev0 — fix before merge

Both symptoms share one root cause: the two resolvers rank slip44 sources in opposite orders.

  • transactions.ts (toNetworkFee) prefers the symbol lookup: resolveNativeAssetId(...) ?? nativeAsset?.assetId
  • caip.ts (getNativeAsset) prefers chainlist's slip44

Sev0-1 · Every testnet native token gets slip44:1

604 of 2,664 chainlist entries carry slip44: 1 — the SLIP-44 Testnet coin type. That's every EVM testnet: Sepolia, Base/OP/Arbitrum/Linea Sepolia, Amoy, Holesky, Hoodi, Fuji, BSC testnet.

Impact: synthesized native tokens on testnets get eip155:11155111/slip44:1. The repo's canonical form is /slip44:60 (see packages/assets-controller/src/AssetsController.test.ts). Clients keying prices/icons off assetId miss.

Sev0-2 · Token and fee disagree within a single activity item

Probed output:

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: nativeTokenDecimalsnativeAsset?.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" with decimals: 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 (getFeestoNetworkFeegetNativeAsset, 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: toNetworkFeeAmountcalculateNetworkFee, buildBaseNetworkFeetoNetworkFee, slip44CoinTypeBySymbolslip44BySymbol, slip44CoinTypeForSymbolgetCoinType are pure renames that obscure the behavioral diff in a fix PR. getCoinType is also vaguer than the name it replaced.
  • Missing JSDoc on the new exported getNativeAsset. Not a lint failure (jsdoc/require-jsdoc is off; resolveNativeAssetId lacks one too), but this is precisely the function whose slip44 precedence needs documenting.
  • Dead type surface: types/eth-chainlist.d.ts declares getChainByNetworkId, getChainByName, getChainByShortName, rawChainData — none used. Marking nativeCurrency? optional is the right defensive call for untyped JSON; decimals should 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>
@n3ps
n3ps force-pushed the n3ps/client-utils-native-fee-fallback branch from 6889323 to a0d39a1 Compare July 31, 2026 20:37
…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>
@n3ps
n3ps force-pushed the n3ps/client-utils-native-fee-fallback branch from a0d39a1 to b5147cf Compare July 31, 2026 20:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants