Skip to content
Merged
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Each package in this repository has its own README where you can find installati
- [`@metamask/chain-agnostic-permission`](packages/chain-agnostic-permission)
- [`@metamask/composable-controller`](packages/composable-controller)
- [`@metamask/controller-utils`](packages/controller-utils)
- [`@metamask/core-backend`](packages/core-backend)
- [`@metamask/delegation-controller`](packages/delegation-controller)
- [`@metamask/earn-controller`](packages/earn-controller)
- [`@metamask/eip-5792-middleware`](packages/eip-5792-middleware)
Expand Down Expand Up @@ -98,6 +99,7 @@ linkStyle default opacity:0.5
chain_agnostic_permission(["@metamask/chain-agnostic-permission"]);
composable_controller(["@metamask/composable-controller"]);
controller_utils(["@metamask/controller-utils"]);
core_backend(["@metamask/core-backend"]);
delegation_controller(["@metamask/delegation-controller"]);
earn_controller(["@metamask/earn-controller"]);
eip_5792_middleware(["@metamask/eip-5792-middleware"]);
Expand Down Expand Up @@ -159,6 +161,7 @@ linkStyle default opacity:0.5
assets_controllers --> account_tree_controller;
assets_controllers --> accounts_controller;
assets_controllers --> approval_controller;
assets_controllers --> core_backend;
assets_controllers --> keyring_controller;
assets_controllers --> multichain_account_service;
assets_controllers --> network_controller;
Expand Down Expand Up @@ -192,6 +195,11 @@ linkStyle default opacity:0.5
chain_agnostic_permission --> permission_controller;
composable_controller --> base_controller;
composable_controller --> json_rpc_engine;
core_backend --> base_controller;
core_backend --> controller_utils;
core_backend --> profile_sync_controller;
core_backend --> accounts_controller;
core_backend --> keyring_controller;
delegation_controller --> base_controller;
delegation_controller --> accounts_controller;
delegation_controller --> keyring_controller;
Expand Down Expand Up @@ -264,6 +272,7 @@ linkStyle default opacity:0.5
permission_log_controller --> json_rpc_engine;
phishing_controller --> base_controller;
phishing_controller --> controller_utils;
phishing_controller --> transaction_controller;
polling_controller --> base_controller;
polling_controller --> controller_utils;
polling_controller --> network_controller;
Expand Down
4 changes: 4 additions & 0 deletions packages/assets-controllers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Fix address casing in WebSocket-based token balance updates to ensure consistency ([#6819](https://github.com/MetaMask/core/pull/6819))

## [80.0.0]

### Added
Expand Down
14 changes: 14 additions & 0 deletions packages/assets-controllers/src/TokenBalancesController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4328,6 +4328,7 @@ describe('TokenBalancesController', () => {
type: `eip155:1/erc20:${tokenAddress}`,
unit: 'USDC',
fungible: true,
decimals: 6,
},
postBalance: {
amount: '0xf4240', // 1000000 in hex (1 USDC with 6 decimals)
Expand Down Expand Up @@ -4378,6 +4379,7 @@ describe('TokenBalancesController', () => {
type: 'eip155:1/slip44:60',
unit: 'ETH',
fungible: true,
decimals: 18,
},
postBalance: {
amount: '0xde0b6b3a7640000', // 1 ETH in wei
Expand Down Expand Up @@ -4431,6 +4433,7 @@ describe('TokenBalancesController', () => {
type: 'eip155:1/slip44:60',
unit: 'ETH',
fungible: true,
decimals: 18,
},
postBalance: {
amount: '0',
Expand Down Expand Up @@ -4468,6 +4471,7 @@ describe('TokenBalancesController', () => {
type: 'eip155:1/unknown:0x123',
unit: 'UNKNOWN',
fungible: true,
decimals: 18,
},
postBalance: {
amount: '1000',
Expand Down Expand Up @@ -4650,6 +4654,7 @@ describe('TokenBalancesController', () => {
type: `eip155:1/erc20:${token1}`,
unit: 'USDC',
fungible: true,
decimals: 6,
},
postBalance: {
amount: '0xf4240', // 1000000 in hex
Expand All @@ -4661,6 +4666,7 @@ describe('TokenBalancesController', () => {
type: `eip155:1/erc20:${token2}`,
unit: 'USDT',
fungible: true,
decimals: 6,
},
postBalance: {
amount: '0x1e8480', // 2000000 in hex
Expand Down Expand Up @@ -4706,6 +4712,7 @@ describe('TokenBalancesController', () => {
type: 'eip155:1/erc20:invalid-address', // Not a valid hex address
unit: 'INVALID',
fungible: true,
decimals: 18,
},
postBalance: { amount: '1000000' },
transfers: [],
Expand Down Expand Up @@ -4780,6 +4787,7 @@ describe('TokenBalancesController', () => {
type: `eip155:1/erc20:${newTokenAddress}`,
unit: 'USDC',
fungible: true,
decimals: 6,
},
postBalance: {
amount: '0xf4240', // 1000000 in hex
Expand Down Expand Up @@ -4853,6 +4861,7 @@ describe('TokenBalancesController', () => {
type: `eip155:1/erc20:${trackedTokenAddress}`,
unit: 'USDC',
fungible: true,
decimals: 6,
},
postBalance: {
amount: '0xf4240', // 1000000 in hex
Expand Down Expand Up @@ -4916,6 +4925,7 @@ describe('TokenBalancesController', () => {
type: `eip155:1/erc20:${ignoredTokenAddress}`,
unit: 'USDC',
fungible: true,
decimals: 6,
},
postBalance: {
amount: '0xf4240', // 1000000 in hex
Expand Down Expand Up @@ -4973,6 +4983,7 @@ describe('TokenBalancesController', () => {
type: 'eip155:1/slip44:60',
unit: 'ETH',
fungible: true,
decimals: 18,
},
postBalance: {
amount: '0xde0b6b3a7640000', // 1 ETH in wei
Expand Down Expand Up @@ -5039,6 +5050,7 @@ describe('TokenBalancesController', () => {
type: `eip155:1/erc20:${newTokenAddress}`,
unit: 'USDC',
fungible: true,
decimals: 6,
},
postBalance: {
amount: '0xf4240', // 1000000 in hex
Expand Down Expand Up @@ -5109,6 +5121,7 @@ describe('TokenBalancesController', () => {
type: `eip155:1/erc20:${trackedToken}`,
unit: 'USDC',
fungible: true,
decimals: 6,
},
postBalance: {
amount: '0xf4240', // 1000000 in hex
Expand All @@ -5120,6 +5133,7 @@ describe('TokenBalancesController', () => {
type: `eip155:1/erc20:${untrackedToken}`,
unit: 'USDT',
fungible: true,
decimals: 6,
},
postBalance: {
amount: '0x1e8480', // 2000000 in hex
Expand Down
21 changes: 11 additions & 10 deletions packages/assets-controllers/src/TokenBalancesController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -844,12 +844,10 @@ export class TokenBalancesController extends StaticIntervalPollingController<{
account: ChecksumAddress,
chainId: ChainIdHex,
): boolean {
const normalizedAddress = tokenAddress.toLowerCase();

// Check if token exists in allTokens
if (
this.#allTokens?.[chainId]?.[account.toLowerCase()]?.some(
(token) => token.address.toLowerCase() === normalizedAddress,
(token) => token.address === tokenAddress,
)
) {
return true;
Expand All @@ -858,7 +856,7 @@ export class TokenBalancesController extends StaticIntervalPollingController<{
// Check if token exists in allIgnoredTokens
if (
this.#allIgnoredTokens?.[chainId]?.[account.toLowerCase()]?.some(
(addr) => addr.toLowerCase() === normalizedAddress,
(token) => token === tokenAddress,
Copy link

Choose a reason for hiding this comment

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

Bug: Token Tracking Case Sensitivity Issue

The #isTokenTracked method now uses case-sensitive address comparison, which can cause tokens to be incorrectly identified as untracked or not ignored. This happens because stored addresses in allTokens and allIgnoredTokens may not consistently match the casing of the input tokenAddress.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

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

Bug: Token Address Matching Case Sensitivity Issue

The #isTokenTracked method changed from case-insensitive to case-sensitive address comparison. This causes it to incorrectly return false for tracked or ignored tokens when there's a casing mismatch between the input tokenAddress and addresses stored in allTokens or allIgnoredTokens.

Fix in Cursor Fix in Web

)
) {
return true;
Expand Down Expand Up @@ -1124,23 +1122,26 @@ export class TokenBalancesController extends StaticIntervalPollingController<{
updates: BalanceUpdate[];
}) => {
const chainId = caipChainIdToHex(chain);
const account = checksum(address);
const checksummedAccount = checksum(address);

try {
// Process all balance updates at once
const { tokenBalances, newTokens, nativeBalanceUpdates } =
this.#prepareBalanceUpdates(updates, account, chainId);
this.#prepareBalanceUpdates(updates, checksummedAccount, chainId);

// Update state once with all token balances
if (tokenBalances.length > 0) {
this.update((state) => {
// Initialize account and chain structure
state.tokenBalances[account] ??= {};
state.tokenBalances[account][chainId] ??= {};
// Temporary until ADR to normalize all keys - tokenBalances state requires: account in lowercase, token in checksum
const lowercaseAccount =
checksummedAccount.toLowerCase() as ChecksumAddress;
state.tokenBalances[lowercaseAccount] ??= {};
state.tokenBalances[lowercaseAccount][chainId] ??= {};

// Apply all token balance updates
for (const { tokenAddress, balance } of tokenBalances) {
state.tokenBalances[account][chainId][tokenAddress] = balance;
state.tokenBalances[lowercaseAccount][chainId][tokenAddress] =
balance;
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3535,6 +3535,8 @@ describe('TokenDetectionController', () => {
describe('addDetectedTokensViaWs', () => {
it('should add tokens detected from websocket with metadata from cache', async () => {
const mockTokenAddress = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48';
const checksummedTokenAddress =
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
const chainId = '0x1';

await withController(
Expand Down Expand Up @@ -3581,7 +3583,7 @@ describe('TokenDetectionController', () => {
'TokensController:addTokens',
[
{
address: mockTokenAddress,
address: checksummedTokenAddress,
decimals: 6,
symbol: 'USDC',
aggregators: [],
Expand Down Expand Up @@ -3652,7 +3654,11 @@ describe('TokenDetectionController', () => {

it('should add all tokens provided without filtering (filtering is caller responsibility)', async () => {
const mockTokenAddress = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48';
const checksummedTokenAddress =
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
const secondTokenAddress = '0x1f573d6fb3f13d689ff844b4ce37794d79a7ff1c';
const checksummedSecondTokenAddress =
'0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C';
const chainId = '0x1';
const selectedAccount = createMockInternalAccount({
address: '0x0000000000000000000000000000000000000001',
Expand Down Expand Up @@ -3718,7 +3724,7 @@ describe('TokenDetectionController', () => {
'TokensController:addTokens',
[
{
address: mockTokenAddress,
address: checksummedTokenAddress,
decimals: 6,
symbol: 'USDC',
aggregators: [],
Expand All @@ -3727,7 +3733,7 @@ describe('TokenDetectionController', () => {
name: 'USD Coin',
},
{
address: secondTokenAddress,
address: checksummedSecondTokenAddress,
decimals: 18,
symbol: 'BNT',
aggregators: [],
Expand All @@ -3744,6 +3750,8 @@ describe('TokenDetectionController', () => {

it('should track metrics when adding tokens from websocket', async () => {
const mockTokenAddress = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48';
const checksummedTokenAddress =
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
const chainId = '0x1';
const mockTrackMetricsEvent = jest.fn();

Expand Down Expand Up @@ -3793,7 +3801,7 @@ describe('TokenDetectionController', () => {
event: 'Token Detected',
category: 'Wallet',
properties: {
tokens: [`USDC - ${mockTokenAddress}`],
tokens: [`USDC - ${checksummedTokenAddress}`],
token_standard: 'ERC20',
asset_type: 'TOKEN',
},
Expand All @@ -3810,6 +3818,8 @@ describe('TokenDetectionController', () => {

it('should be callable directly as a public method on the controller instance', async () => {
const mockTokenAddress = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48';
const checksummedTokenAddress =
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
const chainId = '0x1';

await withController(
Expand Down Expand Up @@ -3857,7 +3867,7 @@ describe('TokenDetectionController', () => {
'TokensController:addTokens',
[
{
address: mockTokenAddress,
address: checksummedTokenAddress,
decimals: 6,
symbol: 'USDC',
aggregators: [],
Expand Down
24 changes: 13 additions & 11 deletions packages/assets-controllers/src/TokenDetectionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ERC20,
safelyExecute,
isEqualCaseInsensitive,
toChecksumHexAddress,
} from '@metamask/controller-utils';
import type {
KeyringControllerGetStateAction,
Expand Down Expand Up @@ -1042,27 +1043,28 @@ export class TokenDetectionController extends StaticIntervalPollingController<To
const tokensWithBalance: Token[] = [];
const eventTokensDetails: string[] = [];

for (const nonZeroTokenAddress of tokensSlice) {
// Check map of validated tokens
for (const tokenAddress of tokensSlice) {
// Normalize addresses explicitly (don't assume input format)
const lowercaseTokenAddress = tokenAddress.toLowerCase();
const checksummedTokenAddress = toChecksumHexAddress(tokenAddress);

// Check map of validated tokens (cache keys are lowercase)
const tokenData =
this.#tokensChainsCache[chainId]?.data?.[
nonZeroTokenAddress.toLowerCase()
];
this.#tokensChainsCache[chainId]?.data?.[lowercaseTokenAddress];

if (!tokenData) {
console.warn(
`Token metadata not found in cache for ${nonZeroTokenAddress} on chain ${chainId}`,
`Token metadata not found in cache for ${tokenAddress} on chain ${chainId}`,
);
continue;
}

const { decimals, symbol, aggregators, iconUrl, name, address } =
tokenData;
const { decimals, symbol, aggregators, iconUrl, name } = tokenData;

// Push to lists
eventTokensDetails.push(`${symbol} - ${address}`);
// Push to lists with checksummed address (for allTokens storage)
eventTokensDetails.push(`${symbol} - ${checksummedTokenAddress}`);
tokensWithBalance.push({
address,
address: checksummedTokenAddress,
decimals,
symbol,
aggregators,
Expand Down
33 changes: 33 additions & 0 deletions packages/core-backend/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **BREAKING:** Add required argument `channelType` to `BackendWebSocketService.subscribe` method ([#6819](https://github.com/MetaMask/core/pull/6819))
- Add `channelType` to argument of the `BackendWebSocketService:subscribe` messenger action
- Add `channelType` to `WebSocketSubscription` type
- **BREAKING**: Update `Asset` type definition: add required `decimals` field for proper token amount formatting ([#6819](https://github.com/MetaMask/core/pull/6819))
- Add optional `traceFn` parameter to `BackendWebSocketService` constructor for performance tracing integration (e.g., Sentry) ([#6819](https://github.com/MetaMask/core/pull/6819))
- Enables tracing of WebSocket operations including connect, disconnect methods
- Trace function receives operation metadata and callback to wrap for performance monitoring
- Add optional `timestamp` property to `ServerNotificationMessage` and `SystemNoticationData` types ([#6819](https://github.com/MetaMask/core/pull/6819))
- Add optional `timestamp` property to `AccountActivityService:statusChanged` event and corresponding event type ([#6819](https://github.com/MetaMask/core/pull/6819))

### Changed

- **BREAKING:** Update `BackendWebSocketService` to automatically manage WebSocket connections based on wallet lock state ([#6819](https://github.com/MetaMask/core/pull/6819))
- `KeyringController:lock` and `KeyringController:unlock` are now required events in the `BackendWebSocketService` messenger
- **BREAKING**: Update `Transaction` type definition: rename `hash` field to `id` for consistency with backend API ([#6819](https://github.com/MetaMask/core/pull/6819))
- **BREAKING:** Add peer dependency on `@metamask/keyring-controller` (^23.0.0) ([#6819](https://github.com/MetaMask/core/pull/6819))
- Update `BackendWebSocketService` to simplify reconnection logic: auto-reconnect on any unexpected disconnect (not just code 1000), stay disconnected when manually disconnecting via `disconnect` ([#6819](https://github.com/MetaMask/core/pull/6819))
- Improve error handling in `BackendWebSocketService.connect()` to properly rethrow errors to callers ([#6819](https://github.com/MetaMask/core/pull/6819))
- Update `AccountActivityService` to replace API-based chain support detection with system notification-driven chain tracking ([#6819](https://github.com/MetaMask/core/pull/6819))
- Instead of hardcoding a list of supported chains, assume that the backend has the list
- When receiving a system notification, capture the backend-tracked status of each chain instead of assuming it is up or down
- Flush all tracked chains as 'down' on disconnect/error (instead of using hardcoded list)
- Update documentation in `README.md` to reflect new connection management model and chain tracking behavior ([#6819](https://github.com/MetaMask/core/pull/6819))
- Add "WebSocket Connection Management" section explaining connection requirements and behavior
- Update sequence diagram to show system notification-driven chain status flow
- Update key flow characteristics to reflect internal chain tracking mechanism

### Removed

- **BREAKING**: Remove `getSupportedChains` method from `AccountActivityService` ([#6819](https://github.com/MetaMask/core/pull/6819))

## [1.0.1]

### Changed
Expand Down
Loading
Loading