Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
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
30 changes: 30 additions & 0 deletions packages/core-backend/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- `BackendWebSocketService` - Added optional `traceFn` parameter to constructor for performance tracing integration (e.g., Sentry)
- Enables tracing of WebSocket operations including connect, disconnect methods
- Trace function receives operation metadata and callback to wrap for performance monitoring
- **BREAKING**: `BackendWebSocketService` - Simplified connection management and added KeyringController event integration ([#6819](https://github.com/MetaMask/core/pull/6819))
- Added `KeyringController:lock` and `KeyringController:unlock` event subscriptions to automatically manage WebSocket connections based on wallet lock state
- Renamed internal method `setupAuthentication()` to `subscribeEvents()` to reflect broader event handling responsibilities
- Simplified reconnection logic: auto-reconnect on any unexpected disconnect, stay disconnected on manual disconnects (tracked via `#manualDisconnect` flag)
- Updated `connect()` to reset manual disconnect flag, allowing reconnection after previous manual disconnects
- Updated `disconnect()` to set manual disconnect flag, preventing automatic reconnection
- Improved error handling in `connect()` to properly rethrow errors to callers
- **BREAKING**: `AccountActivityService` - Replaced API-based chain support detection with system notification-driven chain tracking ([#6819](https://github.com/MetaMask/core/pull/6819))
- Added internal `#chainsUp` Set to track chains reported as 'up' via system notifications
- Updated system notification handler to dynamically track chain status (add to set when 'up', remove when 'down')
- Updated WebSocket state change handler to flush all tracked chains as 'down' on disconnect/error (instead of using hardcoded list)
- Chain status is now entirely driven by backend system notifications rather than proactive API calls
- **BREAKING**: Updated `Transaction` type definition - renamed `hash` field to `id` for consistency with backend API ([#6819](https://github.com/MetaMask/core/pull/6819))
- **BREAKING**: Updated `Asset` type definition - added required `decimals` field for proper token amount formatting ([#6819](https://github.com/MetaMask/core/pull/6819))
- Updated documentation (README.md) to reflect new connection management model and chain tracking behavior ([#6819](https://github.com/MetaMask/core/pull/6819))
- Added "WebSocket Connection Management" section explaining connection requirements and behavior
- Updated sequence diagram to show system notification-driven chain status flow
- Updated key flow characteristics to reflect internal chain tracking mechanism

### Removed

- Removed `shouldReconnectOnClose()` method - reconnection decisions now based solely on manual disconnect flag rather than close codes
- Removed `getSupportedChains()` public method and all related API fetching logic
- Removed hardcoded `DEFAULT_SUPPORTED_CHAINS` fallback list and cache expiration mechanism

## [1.0.1]

### Changed
Expand Down
84 changes: 62 additions & 22 deletions packages/core-backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ Core backend services for MetaMask, serving as the data layer between Backend se
- [Data Flow](#data-flow)
- [Sequence Diagram: Real-time Account Activity Flow](#sequence-diagram-real-time-account-activity-flow)
- [Key Flow Characteristics](#key-flow-characteristics)
- [WebSocket Connection Management](#websocket-connection-management)
- [Connection Requirements](#connection-requirements)
- [Connection Behavior](#connection-behavior)
- [API Reference](#api-reference)
- [BackendWebSocketService](#backendwebsocketservice)
- [Constructor Options](#constructor-options)
Expand Down Expand Up @@ -227,16 +230,17 @@ sequenceDiagram
Backend->>WS: Connection established
WS->>AA: WebSocket connection status notification<br/>(BackendWebSocketService:connectionStateChanged)<br/>{state: 'CONNECTED'}
par StatusChanged Event
AA->>TBC: Chain availability notification<br/>(AccountActivityService:statusChanged)<br/>{chainIds: ['0x1', '0x89', ...], status: 'up'}
TBC->>TBC: Increase polling interval from 20s to 10min<br/>(.updateChainPollingConfigs({0x89: 600000}))
and Account Subscription
AA->>AA: call('AccountsController:getSelectedAccount')
AA->>WS: subscribe({channels, callback})
WS->>Backend: {event: 'subscribe', channels: ['account-activity.v1.eip155:0:0x123...']}
Backend->>WS: {event: 'subscribe-response', subscriptionId: 'sub-456'}
WS->>AA: Subscription sucessful
end
AA->>AA: call('AccountsController:getSelectedAccount')
AA->>WS: subscribe({channels, callback})
WS->>Backend: {event: 'subscribe', channels: ['account-activity.v1.eip155:0:0x123...']}
Backend->>WS: {event: 'subscribe-response', subscriptionId: 'sub-456'}
Note over WS,Backend: System notification sent automatically upon subscription
Backend->>WS: {event: 'system-notification', data: {chainIds: ['eip155:1', 'eip155:137', ...], status: 'up'}}
WS->>AA: System notification received
AA->>AA: Track chains as 'up' internally
AA->>TBC: Chain availability notification<br/>(AccountActivityService:statusChanged)<br/>{chainIds: ['0x1', '0x89', ...], status: 'up'}
TBC->>TBC: Increase polling interval from 20s to 10min<br/>(.updateChainPollingConfigs({0x89: 600000}))
Note over TBC,Backend: User Account Change
Expand Down Expand Up @@ -285,24 +289,60 @@ sequenceDiagram
Note over TBC,Backend: Connection Health Management
Backend-->>WS: Connection lost
WS->>TBC: WebSocket connection status notification<br/>(BackendWebSocketService:connectionStateChanged)<br/>{state: 'DISCONNECTED'}
TBC->>TBC: Decrease polling interval from 10min to 20s(.updateChainPollingConfigs({0x89: 20000}))
WS->>AA: WebSocket connection status notification<br/>(BackendWebSocketService:connectionStateChanged)<br/>{state: 'DISCONNECTED'}
AA->>AA: Mark all tracked chains as 'down'<br/>(flush internal tracking set)
AA->>TBC: Chain status notification for all tracked chains<br/>(AccountActivityService:statusChanged)<br/>{chainIds: ['0x1', '0x89', ...], status: 'down'}
TBC->>TBC: Decrease polling interval from 10min to 20s<br/>(.updateChainPollingConfigs({0x89: 20000}))
TBC->>HTTP: Fetch balances immediately
WS->>WS: Automatic reconnection<br/>with exponential backoff
WS->>Backend: Reconnection successful - Restart initial setup
WS->>Backend: Reconnection successful
Note over AA,Backend: Restart initial setup - resubscribe and get fresh chain status
AA->>WS: subscribe (same account, new subscription)
WS->>Backend: {event: 'subscribe', channels: ['account-activity.v1.eip155:0:0x123...']}
Backend->>WS: {event: 'subscribe-response', subscriptionId: 'sub-999'}
Backend->>WS: {event: 'system-notification', data: {chainIds: [...], status: 'up'}}
WS->>AA: System notification received
AA->>AA: Track chains as 'up' again
AA->>TBC: Chain availability notification<br/>(AccountActivityService:statusChanged)<br/>{chainIds: [...], status: 'up'}
TBC->>TBC: Increase polling interval back to 10min
```

#### Key Flow Characteristics

1. **Initial Setup**: BackendWebSocketService establishes connection, then AccountActivityService simultaneously notifies all chains are up AND subscribes to selected account, TokenBalancesController increases polling interval to 10 min, then makes initial HTTP request for current balance state
2. **User Account Changes**: When users switch accounts, AccountActivityService unsubscribes from old account, TokenBalancesController makes HTTP calls to fill data gaps, then AccountActivityService subscribes to new account
3. **Real-time Updates**: Backend pushes data through: Backend → BackendWebSocketService → AccountActivityService → TokenBalancesController (+ future TransactionController integration)
4. **System Notifications**: Backend sends chain status updates (up/down) through WebSocket, AccountActivityService processes and forwards to TokenBalancesController which adjusts polling intervals and fetches balances immediately on chain down (chain down: 10min→20s + immediate fetch, chain up: 20s→10min)
5. **Parallel Processing**: Transaction and balance updates processed simultaneously - AccountActivityService publishes both transactionUpdated (future) and balanceUpdated events in parallel
6. **Dynamic Polling**: TokenBalancesController adjusts HTTP polling intervals based on WebSocket connection health (10 min when connected, 20s when disconnected)
7. **Direct Balance Processing**: Real-time balance updates bypass HTTP polling and update TokenBalancesController state directly
8. **Connection Resilience**: Automatic reconnection with resubscription to selected account
9. **Ultra-Simple Error Handling**: Any error anywhere → force reconnection (no nested try-catch)
1. **Initial Setup**: BackendWebSocketService establishes connection, then AccountActivityService subscribes to selected account. Backend automatically sends a system notification with all chains that are currently up. AccountActivityService tracks these chains internally and notifies TokenBalancesController, which increases polling interval to 5 min
2. **Chain Status Tracking**: AccountActivityService maintains an internal set of chains that are 'up' based on system notifications. On disconnect/error, it marks all tracked chains as 'down' before clearing the set
3. **System Notifications**: Backend automatically sends chain status updates (up/down) upon subscription and when status changes. AccountActivityService forwards these to TokenBalancesController, which adjusts polling intervals (up: 5min, down: 30s + immediate fetch)
4. **User Account Changes**: When users switch accounts, AccountActivityService unsubscribes from old account and subscribes to new account. Backend sends fresh system notification with current chain status for the new account
5. **Connection Resilience**: On reconnection, AccountActivityService resubscribes to selected account and receives fresh chain status via system notification. Automatic reconnection with exponential backoff
6. **Real-time Updates**: Backend pushes data through: Backend → BackendWebSocketService → AccountActivityService → TokenBalancesController (+ future TransactionController integration)
7. **Parallel Processing**: Transaction and balance updates processed simultaneously - AccountActivityService publishes both transactionUpdated (future) and balanceUpdated events in parallel
8. **Direct Balance Processing**: Real-time balance updates bypass HTTP polling and update TokenBalancesController state directly

## WebSocket Connection Management

### Connection Requirements

The WebSocket connects when **ALL 3 conditions are true**:

1.**Feature enabled** - `isEnabled()` callback returns `true` (feature flag)
2.**User signed in** - `AuthenticationController.isSignedIn = true`
3.**Wallet unlocked** - `KeyringController.isUnlocked = true`

**Plus:** Platform code must call `connect()` when app opens/foregrounds and `disconnect()` when app closes/backgrounds.

### Connection Behavior

**Idempotent `connect()`:**

- Safe to call multiple times - validates conditions and returns early if already connected
- Multiple rapid calls reuse the same connection promise (no duplicate connections)
- No debouncing needed - handled automatically

**Auto-Reconnect:**

-**Unexpected disconnects** (network issues, server restart) → Auto-reconnect
-**Manual disconnects** (app backgrounds, wallet locks, user signs out) → Stay disconnected

## API Reference

Expand Down
5 changes: 3 additions & 2 deletions packages/core-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,20 @@
"devDependencies": {
"@metamask/accounts-controller": "^33.1.1",
"@metamask/auto-changelog": "^3.4.4",
"@metamask/keyring-controller": "^23.1.1",
"@ts-bridge/cli": "^0.6.1",
"@types/jest": "^27.4.1",
"deepmerge": "^4.2.2",
"jest": "^27.5.1",
"nock": "^13.3.1",
"sinon": "^9.2.4",
"ts-jest": "^27.1.4",
"typedoc": "^0.24.8",
"typedoc-plugin-missing-exports": "^2.0.0",
"typescript": "~5.2.2"
},
"peerDependencies": {
"@metamask/accounts-controller": "^33.1.0"
"@metamask/accounts-controller": "^33.1.0",
"@metamask/keyring-controller": "^23.0.0"
},
"engines": {
"node": "^18.18 || >=20"
Expand Down
Loading
Loading