diff --git a/packages/assets-controller/CHANGELOG.md b/packages/assets-controller/CHANGELOG.md index 208d91cd77f..bdf961a6a2d 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `isNativeAsset` utility that centralizes native asset detection across all CAIP-19 representations (`slip44:` namespace, known native asset IDs from `SPOT_PRICES_SUPPORT_INFO`, and `erc20:` with zero address) ([#8483](https://github.com/MetaMask/core/pull/8483)) + ### Changed - Bump `@metamask/keyring-api` from `^21.6.0` to `^23.0.1` ([#8464](https://github.com/MetaMask/core/pull/8464)) @@ -14,6 +18,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/keyring-snap-client` from `^8.2.0` to `^9.0.1` ([#8464](https://github.com/MetaMask/core/pull/8464)) - Bump `@metamask/transaction-controller` from `^64.2.0` to `^64.3.0` ([#8482](https://github.com/MetaMask/core/pull/8482)) +### Fixed + +- Native asset detection now correctly identifies native assets across all CAIP-19 representations, not just `slip44:` namespace checks ([#TBD](https://github.com/MetaMask/core/pull/TBD)) + - Previously, native assets represented as ERC-20 tokens (e.g., Polygon's POL at `0x…1010`) were not recognized as native, causing incorrect token type classification, balance handling, and missing entries in bridge exchange rates and transaction pay legacy formats. +- Legacy format conversions (bridge exchange rates and transaction pay) now use the correct chain-specific native token address via `getNativeTokenAddress()` instead of always using the zero address ([#TBD](https://github.com/MetaMask/core/pull/TBD)) + ## [6.0.0] ### Added diff --git a/packages/assets-controller/src/AssetsController.ts b/packages/assets-controller/src/AssetsController.ts index f7b4062960d..ab9b6664354 100644 --- a/packages/assets-controller/src/AssetsController.ts +++ b/packages/assets-controller/src/AssetsController.ts @@ -119,6 +119,7 @@ import type { BridgeExchangeRatesFormat, TransactionPayLegacyFormat, } from './utils'; +import { isNativeAsset } from './utils/isNativeAsset'; // ============================================================================ // PENDING TOKEN METADATA (UI input format for addCustomAsset) @@ -1520,7 +1521,7 @@ export class AssetsController extends BaseController< if (pendingMetadata) { const parsed = parseCaipAssetType(normalizedAssetId); let tokenType: FungibleAssetMetadata['type'] = 'erc20'; - if (parsed.assetNamespace === 'slip44') { + if (isNativeAsset(normalizedAssetId)) { tokenType = 'native'; } else if (parsed.assetNamespace === 'spl') { tokenType = 'spl'; @@ -2140,7 +2141,7 @@ export class AssetsController extends BaseController< } // Check if it's a native token (either by metadata type or assetId format) - const isNative = metadata.type === 'native' || assetId.includes('/slip44:'); + const isNative = metadata.type === 'native' || isNativeAsset(assetId); return isNative; } diff --git a/packages/assets-controller/src/data-sources/BackendWebsocketDataSource.ts b/packages/assets-controller/src/data-sources/BackendWebsocketDataSource.ts index 1edfafe3183..279b9c44e7e 100644 --- a/packages/assets-controller/src/data-sources/BackendWebsocketDataSource.ts +++ b/packages/assets-controller/src/data-sources/BackendWebsocketDataSource.ts @@ -24,6 +24,7 @@ import type { AssetBalance, DataResponse, } from '../types'; +import { isNativeAsset } from '../utils/isNativeAsset'; import { AbstractDataSource } from './AbstractDataSource'; import type { DataSourceState, @@ -639,7 +640,7 @@ export class BackendWebsocketDataSource extends AbstractDataSource< const assetId = asset.type as Caip19AssetId; // Determine token type from asset type string - const isNative = asset.type.includes('/slip44:'); + const isNative = isNativeAsset(assetId); const tokenType = isNative ? 'native' : 'erc20'; // We assume decimals are always present; skip malformed updates diff --git a/packages/assets-controller/src/data-sources/RpcDataSource.ts b/packages/assets-controller/src/data-sources/RpcDataSource.ts index 8094dd569d4..ab7e4d5c0c7 100644 --- a/packages/assets-controller/src/data-sources/RpcDataSource.ts +++ b/packages/assets-controller/src/data-sources/RpcDataSource.ts @@ -39,6 +39,7 @@ import type { } from '../types'; import { normalizeAssetId } from '../utils'; import { ZERO_ADDRESS } from '../utils/constants'; +import { isNativeAsset } from '../utils/isNativeAsset'; import { AbstractDataSource } from './AbstractDataSource'; import type { DataSourceState, @@ -343,7 +344,7 @@ export class RpcDataSource extends AbstractDataSource< const existingMetadata = this.#getExistingAssetsMetadata(); for (const balance of balances) { - const isNative = balance.assetId.includes('/slip44:'); + const isNative = isNativeAsset(balance.assetId); if (isNative) { const chainStatus = this.#chainStatuses[chainId]; @@ -1367,7 +1368,9 @@ export class RpcDataSource extends AbstractDataSource< 'NetworkEnablementController:getState', ); - return nativeAssetIdentifiers[chainId] ?? `${chainId}/slip44:60`; + return ( + nativeAssetIdentifiers[chainId] ?? `${chainId}/erc20:${ZERO_ADDRESS}` + ); } /** diff --git a/packages/assets-controller/src/data-sources/TokenDataSource.ts b/packages/assets-controller/src/data-sources/TokenDataSource.ts index 9ee8041906c..807dda1e4b1 100644 --- a/packages/assets-controller/src/data-sources/TokenDataSource.ts +++ b/packages/assets-controller/src/data-sources/TokenDataSource.ts @@ -17,6 +17,7 @@ import type { Middleware, FungibleAssetMetadata, } from '../types'; +import { isNativeAsset } from '../utils/isNativeAsset'; import { isStakingContractAssetId, reduceInBatchesSerially, @@ -85,13 +86,13 @@ export type TokenDataSourceAllowedActions = * @returns FungibleAssetMetadata for state storage. */ function transformV3AssetResponseToMetadata( - assetId: string, + assetId: Caip19AssetId, assetData: V3AssetResponse, ): AssetMetadata { - const parsed = parseCaipAssetType(assetId as CaipAssetType); + const parsed = parseCaipAssetType(assetId); let tokenType: 'native' | 'erc20' | 'spl' = 'erc20'; - if (parsed.assetNamespace === 'slip44') { + if (isNativeAsset(assetId)) { tokenType = 'native'; } else if (parsed.assetNamespace === 'spl') { tokenType = 'spl'; @@ -396,18 +397,17 @@ export class TokenDataSource { const nonEvmTokenIds: string[] = []; for (const assetData of metadataResponse) { - const { assetNamespace, chain } = parseCaipAssetType( - assetData.assetId as CaipAssetType, - ); - if (assetNamespace === CaipAssetNamespace.Slip44) { + const assetId = assetData.assetId as Caip19AssetId; + const { assetNamespace, chain } = parseCaipAssetType(assetId); + if (isNativeAsset(assetId)) { // Native assets are always kept — no filtering. } else if ( assetNamespace === CaipAssetNamespace.Erc20 && chain.namespace === KnownCaipNamespace.Eip155 ) { - evmErc20Ids.push(assetData.assetId); + evmErc20Ids.push(assetId); } else if (assetNamespace === CaipAssetNamespace.Token) { - nonEvmTokenIds.push(assetData.assetId); + nonEvmTokenIds.push(assetId); } } @@ -461,7 +461,7 @@ export class TokenDataSource { const caipAssetId = assetData.assetId as Caip19AssetId; response.assetsInfo[caipAssetId] = transformV3AssetResponseToMetadata( - assetData.assetId, + caipAssetId, assetData, ); } diff --git a/packages/assets-controller/src/data-sources/evm-rpc-services/services/BalanceFetcher.ts b/packages/assets-controller/src/data-sources/evm-rpc-services/services/BalanceFetcher.ts index 4c4a6a4b429..f11e5c83989 100644 --- a/packages/assets-controller/src/data-sources/evm-rpc-services/services/BalanceFetcher.ts +++ b/packages/assets-controller/src/data-sources/evm-rpc-services/services/BalanceFetcher.ts @@ -2,6 +2,7 @@ import { StaticIntervalPollingControllerOnly } from '@metamask/polling-controlle import { parseCaipAssetType } from '@metamask/utils'; import { ZERO_ADDRESS } from '../../../utils/constants'; +import { isNativeAsset } from '../../../utils/isNativeAsset'; import type { MulticallClient } from '../clients'; import type { AccountId, @@ -139,7 +140,6 @@ export class BalanceFetcher extends StaticIntervalPollingControllerOnly { conversionRate: 0.5, usdConversionRate: 0.5, }); - const nativeAddress = '0x0000000000000000000000000000000000000000'; - expect(result.marketData['0x89']?.[nativeAddress]?.currency).toBe('POL'); + const nativeAddressPol = '0x0000000000000000000000000000000000001010'; + expect(result.marketData['0x89']?.[nativeAddressPol]?.currency).toBe('POL'); }); it('includes EVM native asset in marketData and currencyRates', () => { diff --git a/packages/assets-controller/src/utils/formatExchangeRatesForBridge.ts b/packages/assets-controller/src/utils/formatExchangeRatesForBridge.ts index d3e5271ecf9..9eeb6ddbb96 100644 --- a/packages/assets-controller/src/utils/formatExchangeRatesForBridge.ts +++ b/packages/assets-controller/src/utils/formatExchangeRatesForBridge.ts @@ -5,11 +5,13 @@ import { MarketDataDetails, MultichainAssetsRatesControllerState, TokenRatesControllerState, + getNativeTokenAddress, } from '@metamask/assets-controllers'; import { Hex, KnownCaipNamespace, numberToHex } from '@metamask/utils'; import { parseCaipAssetType, parseCaipChainId } from '@metamask/utils'; import type { AssetPrice, FungibleAssetPrice, Caip19AssetId } from '../types'; +import { isNativeAsset } from './isNativeAsset'; /** * Exchange rates in the format expected by the bridge controller: @@ -83,15 +85,16 @@ export function formatExchangeRatesForBridge(params: { const expirationTime = lastUpdatedInSeconds + expirationOffsetInSeconds; try { + const isNative = isNativeAsset(assetId as Caip19AssetId); const parsed = parseCaipAssetType(assetId as Caip19AssetId); const chainIdParsed = parseCaipChainId(parsed.chainId); if (chainIdParsed.namespace === KnownCaipNamespace.Eip155) { const chainIdHex = numberToHex(parseInt(chainIdParsed.reference, 10)); - const nativeAssetId = nativeAssetIdentifiers[parsed.chainId] as - | Caip19AssetId - | undefined; + const nativeAssetId = ( + isNative ? assetId : nativeAssetIdentifiers[parsed.chainId] + ) as Caip19AssetId | undefined; const nativeCurrencySymbol = networkConfigurationsByChainId[chainIdHex]?.nativeCurrency; @@ -110,12 +113,12 @@ export function formatExchangeRatesForBridge(params: { let tokenAddress: Hex | undefined; if (parsed.assetNamespace === 'erc20') { - tokenAddress = toChecksumAddress(String(parsed.assetReference)); - } else if (parsed.assetNamespace === 'slip44') { - tokenAddress = '0x0000000000000000000000000000000000000000'; + tokenAddress = toChecksumAddress(parsed.assetReference); + } else if (isNative) { + tokenAddress = toChecksumAddress(getNativeTokenAddress(chainIdHex)); } - if (tokenAddress && nativeAssetId) { + if (tokenAddress) { const priceInNative = nativeAssetUsdPrice > 0 ? usdPrice / nativeAssetUsdPrice : usdPrice; if (!marketData[chainIdHex]) { @@ -131,7 +134,7 @@ export function formatExchangeRatesForBridge(params: { } as MarketDataDetails; } - if (parsed.assetNamespace === 'slip44' && nativeAssetId) { + if (isNative) { currencyRates[nativeCurrencySymbol] = { conversionDate: lastUpdatedInSeconds, conversionRate: price, diff --git a/packages/assets-controller/src/utils/formatStateForTransactionPay.ts b/packages/assets-controller/src/utils/formatStateForTransactionPay.ts index 211c9b72a72..04ae2845e4b 100644 --- a/packages/assets-controller/src/utils/formatStateForTransactionPay.ts +++ b/packages/assets-controller/src/utils/formatStateForTransactionPay.ts @@ -1,4 +1,5 @@ import { toChecksumAddress } from '@ethereumjs/util'; +import { getNativeTokenAddress } from '@metamask/assets-controllers'; import { numberToHex } from '@metamask/utils'; import { parseCaipAssetType, parseCaipChainId } from '@metamask/utils'; @@ -10,6 +11,7 @@ import type { } from '../types'; import { formatExchangeRatesForBridge } from './formatExchangeRatesForBridge'; import type { BridgeExchangeRatesFormat } from './formatExchangeRatesForBridge'; +import { isNativeAsset } from './isNativeAsset'; /** Account with id and address for mapping state to legacy format. */ export type AccountForLegacyFormat = { id: string; address: string }; @@ -115,9 +117,10 @@ export function formatStateForTransactionPay(params: { const amount = getAmountFromBalance(balance); const balanceHex = amountToHex(amount); - if (parsed.assetNamespace === 'slip44') { - const nativeAddress = - '0x0000000000000000000000000000000000000000' as const; + if (isNativeAsset(assetId as Caip19AssetId)) { + const nativeAddress = toChecksumAddress( + getNativeTokenAddress(chainIdHex), + ); const checksumAddress = toChecksumAddress(account.address); tokenBalances[accountAddressLower] ??= {}; tokenBalances[accountAddressLower][chainIdHex] ??= {}; @@ -148,10 +151,9 @@ export function formatStateForTransactionPay(params: { continue; } const chainIdHex = numberToHex(parseInt(chainIdParsed.reference, 10)); - const address = - parsed.assetNamespace === 'slip44' - ? '0x0000000000000000000000000000000000000000' - : toChecksumAddress(String(parsed.assetReference)); + const address = isNativeAsset(assetId as Caip19AssetId) + ? getNativeTokenAddress(chainIdHex) + : toChecksumAddress(String(parsed.assetReference)); const token: LegacyToken = { address, decimals: metadata.decimals, diff --git a/packages/assets-controller/src/utils/isNativeAsset.ts b/packages/assets-controller/src/utils/isNativeAsset.ts new file mode 100644 index 00000000000..04f1130745d --- /dev/null +++ b/packages/assets-controller/src/utils/isNativeAsset.ts @@ -0,0 +1,31 @@ +import { SPOT_PRICES_SUPPORT_INFO } from '@metamask/assets-controllers'; +import { parseCaipAssetType } from '@metamask/utils'; + +import type { Caip19AssetId } from '../types'; +import { ZERO_ADDRESS } from './constants'; + +export function isNativeAsset(assetId: Caip19AssetId): boolean { + const { assetNamespace, assetReference } = parseCaipAssetType(assetId); + + // All SLIP44 assets are native assets + if (assetNamespace === 'slip44') { + return true; + } + + // All assets in this list are native assets + if ( + Object.values(SPOT_PRICES_SUPPORT_INFO).some( + (nativeAssetId) => nativeAssetId.toLowerCase() === assetId.toLowerCase(), + ) + ) { + return true; + } + + // ERC20 assets with a zero address are native assets + if (assetNamespace === 'erc20' && assetReference === ZERO_ADDRESS) { + return true; + } + + // Not a native asset + return false; +} diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 1307f34f3b5..f30b9218cfe 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `MultichainAssetsController`: periodic Blockaid re-scan of stored SPL-style `token:` assets (default once per day) so tokens that become malicious after a prior scan are dropped; use constructor option `blockaidTokenRescanInterval` (ms), or `0` to disable. ([#8400](https://github.com/MetaMask/core/pull/8400)) +- Export `SPOT_PRICES_SUPPORT_INFO` from the token prices service ([#8483](https://github.com/MetaMask/core/pull/8483)) ### Changed diff --git a/packages/assets-controllers/src/index.ts b/packages/assets-controllers/src/index.ts index 7bfd3752fd3..6eff95c1ee6 100644 --- a/packages/assets-controllers/src/index.ts +++ b/packages/assets-controllers/src/index.ts @@ -166,6 +166,7 @@ export { CodefiTokenPricesServiceV2, SUPPORTED_CHAIN_IDS, getNativeTokenAddress, + SPOT_PRICES_SUPPORT_INFO, } from './token-prices-service'; export { searchTokens, diff --git a/packages/assets-controllers/src/token-prices-service/index.test.ts b/packages/assets-controllers/src/token-prices-service/index.test.ts index 28066404bd1..d67c48e53fe 100644 --- a/packages/assets-controllers/src/token-prices-service/index.test.ts +++ b/packages/assets-controllers/src/token-prices-service/index.test.ts @@ -10,6 +10,7 @@ describe('token-prices-service', () => { "fetchSupportedNetworks", "getSupportedNetworks", "resetSupportedNetworksCache", + "SPOT_PRICES_SUPPORT_INFO", ] `); }); diff --git a/packages/assets-controllers/src/token-prices-service/index.ts b/packages/assets-controllers/src/token-prices-service/index.ts index cb788010df2..3d32fe36183 100644 --- a/packages/assets-controllers/src/token-prices-service/index.ts +++ b/packages/assets-controllers/src/token-prices-service/index.ts @@ -9,4 +9,5 @@ export { fetchSupportedNetworks, getSupportedNetworks, resetSupportedNetworksCache, + SPOT_PRICES_SUPPORT_INFO, } from './codefi-v2';