Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/assets-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Bump `@metamask/keyring-controller` from `^25.3.0` to `^25.4.0` ([#8665](https://github.com/MetaMask/core/pull/8665))
- Bump `@metamask/accounts-controller` from `^37.2.0` to `^38.0.0` ([#8665](https://github.com/MetaMask/core/pull/8665))
- Bump `@metamask/account-tree-controller` from `^7.1.0` to `^7.2.0` ([#8665](https://github.com/MetaMask/core/pull/8665))
- Update `RpcDataSource` to prevent native `getEthBalance` fetching for Tempo chains ([#8638](https://github.com/MetaMask/core/pull/8638))

## [6.3.0]

Expand Down
8 changes: 2 additions & 6 deletions packages/assets-controller/src/AssetsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type {
} from '@metamask/base-controller';
import type { ClientControllerStateChangeEvent } from '@metamask/client-controller';
import { clientControllerSelectors } from '@metamask/client-controller';
import { CHAIN_IDS_WITH_NO_NATIVE_TOKEN } from '@metamask/controller-utils';
import type { TraceCallback } from '@metamask/controller-utils';
import type {
ApiPlatformClient,
Expand Down Expand Up @@ -73,6 +72,7 @@ import type {
import type { AccountsApiDataSourceConfig } from './data-sources/AccountsApiDataSource';
import { AccountsApiDataSource } from './data-sources/AccountsApiDataSource';
import { BackendWebsocketDataSource } from './data-sources/BackendWebsocketDataSource';
import { shouldSkipNativeForCaipChainId } from './data-sources/evm-rpc-services/utils/assets';
import type { PriceDataSourceConfig } from './data-sources/PriceDataSource';
import { PriceDataSource } from './data-sources/PriceDataSource';
import type { RpcDataSourceConfig } from './data-sources/RpcDataSource';
Expand Down Expand Up @@ -2377,11 +2377,7 @@ export class AssetsController extends BaseController<
*/
#shouldHideNativeToken(chainId: ChainId, metadata: AssetMetadata): boolean {
// Check if it's a chain that should skip native tokens
if (
!CHAIN_IDS_WITH_NO_NATIVE_TOKEN.includes(
chainId as (typeof CHAIN_IDS_WITH_NO_NATIVE_TOKEN)[number],
)
) {
if (!shouldSkipNativeForCaipChainId(chainId)) {
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
BalanceFetchResult,
TokenDetectionResult,
} from './evm-rpc-services';
import { shouldSkipNativeForCaipChainId } from './evm-rpc-services/utils/assets';
import type { RpcDataSourceOptions } from './RpcDataSource';
import {
RpcDataSource,
Expand Down Expand Up @@ -247,6 +248,10 @@ jest.mock('@ethersproject/providers', () => ({
})),
}));

jest.mock('./evm-rpc-services/utils/assets', () => ({
shouldSkipNativeForCaipChainId: jest.fn().mockReturnValue(false),
}));

describe('caipChainIdToHex', () => {
it('returns hex unchanged when given hex string', () => {
expect(caipChainIdToHex('0x1')).toBe('0x1');
Expand Down Expand Up @@ -469,6 +474,18 @@ describe('RpcDataSource', () => {
});
});

it('fetches balances for accounts except native for native skip chain', async () => {
jest.mocked(shouldSkipNativeForCaipChainId).mockReturnValue(true);
await withController(async ({ controller }) => {
const response = await controller.fetch(createDataRequest());
expect(response).toBeDefined();
expect(response.assetsBalance).toBeDefined();
expect(response.assetsBalance?.[MOCK_ACCOUNT_ID]).not.toHaveProperty(
'eip155:1/slip44:60',
);
});
});
Comment thread
cursor[bot] marked this conversation as resolved.

it('converts fetched balances to human-readable and merges metadata', async () => {
const nativeAssetId = 'eip155:1/slip44:60' as Caip19AssetId;
await withController(async ({ controller }) => {
Expand Down Expand Up @@ -506,6 +523,30 @@ describe('RpcDataSource', () => {
});
});

it('skips native asset call even in the getBalance fallback when Multicall aggregate3 fails', async () => {
const { Web3Provider } = jest.requireMock('@ethersproject/providers');
jest.mocked(shouldSkipNativeForCaipChainId).mockReturnValue(true);

const mockCall = jest
.fn()
.mockRejectedValueOnce(new Error('aggregate3 unavailable'))
.mockResolvedValue('0x0');
const mockGetBalance = jest
.fn()
.mockResolvedValue({ toString: () => '1000000000000000000' });
(Web3Provider as jest.Mock).mockImplementationOnce(() => ({
call: mockCall,
getBalance: mockGetBalance,
}));

await withController(async ({ controller }) => {
const response = await controller.fetch(createDataRequest());
expect(response.assetsBalance).toBeDefined();
expect(response.assetsBalance?.[MOCK_ACCOUNT_ID]).toStrictEqual({});
expect(mockGetBalance).not.toHaveBeenCalled();
});
});

it('uses getBalance when Multicall aggregate3 fails (#getMulticallProvider getBalance)', async () => {
const { Web3Provider } = jest.requireMock('@ethersproject/providers');
const mockCall = jest
Expand Down Expand Up @@ -576,6 +617,25 @@ describe('RpcDataSource', () => {
});
});

it('initializes assetsBalance[accountId] with no native in catch when first fetch for account throws on native skip chain', async () => {
await withController(async ({ controller }) => {
jest
.spyOn(BalanceFetcher.prototype, 'fetchBalancesForAssets')
.mockRejectedValue(new Error('RPC unavailable'));
// Indicates that we want to skip native for that chain
jest.mocked(shouldSkipNativeForCaipChainId).mockReturnValue(true);
const request = createDataRequest();
const response = await controller.fetch(request);
expect(response.errors).toBeDefined();
expect(response.errors?.[MOCK_CHAIN_ID_CAIP]).toBe('RPC fetch failed');
expect(response.assetsBalance).toBeDefined();
expect(response.assetsBalance?.[MOCK_ACCOUNT_ID]).toBeDefined();
expect(response.assetsBalance?.[MOCK_ACCOUNT_ID]).not.toHaveProperty(
'eip155:1/slip44:60',
);
});
});

it('returns undefined from #getProvider when network client has no provider', async () => {
const networkState = createMockNetworkState(NetworkStatus.Available);
(networkState.networkConfigurationsByChainId as Record<string, unknown>)[
Expand Down
38 changes: 22 additions & 16 deletions packages/assets-controller/src/data-sources/RpcDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
BalanceFetchResult,
TokenDetectionResult,
} from './evm-rpc-services/types';
import { shouldSkipNativeForCaipChainId } from './evm-rpc-services/utils/assets';

const CONTROLLER_NAME = 'RpcDataSource';
const DEFAULT_BALANCE_INTERVAL = 30_000; // 30 seconds
Expand Down Expand Up @@ -919,12 +920,14 @@

for (const chainId of chainsForAccount) {
const hexChainId = caipChainIdToHex(chainId);

// Build a single AssetFetchEntry[] for native + custom ERC-20s
const nativeAssetId = this.#getNativeAssetForChain(chainId);
const assetsToFetch: AssetFetchEntry[] = [
{ assetId: nativeAssetId, address: ZERO_ADDRESS },
];

const shouldSkipNative = shouldSkipNativeForCaipChainId(chainId);
const assetsToFetch: AssetFetchEntry[] = [];
if (!shouldSkipNative) {
// Build a single AssetFetchEntry[] for native + custom ERC-20s
assetsToFetch.push({ assetId: nativeAssetId, address: ZERO_ADDRESS });
}
Comment thread
cursor[bot] marked this conversation as resolved.

if (request.customAssets) {
const existingMetadata = this.#getExistingAssetsMetadata();
Expand Down Expand Up @@ -1019,17 +1022,20 @@
if (!assetsBalance[accountId]) {
assetsBalance[accountId] = {};
}
assetsBalance[accountId][nativeAssetId] = { amount: '0' };

// Even on error, include native token metadata
const chainStatus = this.#chainStatuses[chainId];
if (chainStatus) {
assetsInfo[nativeAssetId] = {
type: 'native',
symbol: chainStatus.nativeCurrency,
name: chainStatus.nativeCurrency,
decimals: 18,
};

if (!shouldSkipNative) {
assetsBalance[accountId][nativeAssetId] = { amount: '0' };

Check warning

Code scanning / CodeQL

Prototype-polluting assignment Medium

This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.

// Even on error, include native token metadata
const chainStatus = this.#chainStatuses[chainId];
if (chainStatus) {
assetsInfo[nativeAssetId] = {
type: 'native',
symbol: chainStatus.nativeCurrency,
name: chainStatus.nativeCurrency,
decimals: 18,
};
}
}

if (!failedChains.includes(chainId)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { CHAIN_IDS_WITH_NO_NATIVE_TOKEN } from '@metamask/controller-utils';
import { CaipChainId } from '@metamask/utils';

export function shouldSkipNativeForCaipChainId(
caipChainId: CaipChainId,
): boolean {
return (CHAIN_IDS_WITH_NO_NATIVE_TOKEN as readonly string[]).includes(
caipChainId,
);
}
1 change: 1 addition & 0 deletions packages/assets-controllers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Bump `@metamask/keyring-controller` from `^25.3.0` to `^25.4.0` ([#8665](https://github.com/MetaMask/core/pull/8665))
- Bump `@metamask/accounts-controller` from `^37.2.0` to `^38.0.0` ([#8665](https://github.com/MetaMask/core/pull/8665))
- Bump `@metamask/account-tree-controller` from `^7.1.0` to `^7.2.0` ([#8665](https://github.com/MetaMask/core/pull/8665))
- Modify `SPOT_PRICES_SUPPORT_INFO` entries for Tempo chains (`0x1079` and `0xa5bf`) ([#8638](https://github.com/MetaMask/core/pull/8638))

## [105.1.0]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,6 @@ const chainIdToNativeTokenAddress: Record<Hex, Hex> = {
'0x64': '0xe91d153e0b41518a2ce8dd3d7944fa863463a97d', // Gnosis
'0x1e': '0x542fda317318ebf1d3deaf76e0b632741a7e677d', // Rootstock Mainnet - Native symbol: RBTC
'0x3dc': '0x779ded0c9e1022225f8e0630b35a9b54be713736', // Stable - Native symbol: USDT0
'0x1079': '0x20c0000000000000000000000000000000000000', // Tempo Mainnet - Pseudo-Native symbol: pathUSD
'0xa5bf': '0x20c0000000000000000000000000000000000000', // Tempo Moderato Testnet - Pseudo-Native symbol: pathUSD
};

/**
Expand Down Expand Up @@ -289,7 +287,7 @@ export const SPOT_PRICES_SUPPORT_INFO = {
'0x74c': 'eip155:1868/erc20:0x0000000000000000000000000000000000000000', // Soneium - Native symbol: ETH
'0xa729': 'eip155:42793/erc20:0x0000000000000000000000000000000000000000', // Etherlink - Native symbol: XTZ (Tezos L2)
'0xab5': 'eip155:2741/erc20:0x0000000000000000000000000000000000000000', // Abstract - Native symbol: ETH
'0x1079': 'eip155:4217/erc20:0x20c0000000000000000000000000000000000000', // Tempo Mainnet - Pseudo-Native symbol: pathUSD
'0x1079': 'eip155:4217/slip44:60', // Tempo Mainnet - No native asset
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.

Note: Setting this to erc20:0x0000000000000000000000000000000000000000 led the various errors, such as disappearance of pathUSD (ERC20 token) or disappearance of all tokens from assets list when trying to fix through the state.
Setting eip155:4217/slip44:60 works. Since there is not actual native asset, eip155:4217/slip44:60 is no less correct than erc20:0x0000000000000000000000000000000000000000 and is consistent with the default pre-population of nativeAssetIdentifiers.

'0x10e6': 'eip155:4326/erc20:0x0000000000000000000000000000000000000000', // MegaETH Mainnet - Native symbol: ETH
'0x1388': 'eip155:5000/erc20:0xdeaddeaddeaddeaddeaddeaddeaddeaddead0000', // Mantle - Native symbol: MNT
'0x2105': 'eip155:8453/slip44:60', // Base - Native symbol: ETH
Expand All @@ -302,7 +300,7 @@ export const SPOT_PRICES_SUPPORT_INFO = {
'0xa516': 'eip155:42262/slip44:474', // Oasis Emerald - Native symbol: ROSE
'0xa867': 'eip155:43111/erc20:0x0000000000000000000000000000000000000000', // Hemi - Native symbol: ETH
'0xa86a': 'eip155:43114/slip44:9005', // Avalanche C-Chain - Native symbol: AVAX
'0xa5bf': 'eip155:42431/erc20:0x20c0000000000000000000000000000000000000', // Tempo Testnet Moderato - Pseudo-Native symbol: pathUSD
'0xa5bf': 'eip155:42431/slip44:60', // Tempo Testnet Moderato - No native asset
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.

Note: Setting this to erc20:0x0000000000000000000000000000000000000000 led the various errors, such as disappearance of pathUSD (ERC20 token) or disappearance of all tokens from assets list when trying to fix through the state.
Setting eip155:4217/slip44:60 works. Since there is not actual native asset, eip155:4217/slip44:60 is no less correct than erc20:0x0000000000000000000000000000000000000000 and is consistent with the default pre-population of nativeAssetIdentifiers.

'0xe708': 'eip155:59144/slip44:60', // Linea Mainnet - Native symbol: ETH
'0xed88': 'eip155:60808/erc20:0x0000000000000000000000000000000000000000', // BOB - Native symbol: ETH
'0x138de': 'eip155:80094/erc20:0x0000000000000000000000000000000000000000', // Berachain - Native symbol: Bera',
Expand Down
Loading