Skip to content

Latest commit

 

History

History

README.md

@metamask/core-backend

Core backend services for MetaMask, serving as the data layer between Backend services (REST APIs, WebSocket services) and Frontend applications (Extension, Mobile). Provides authenticated real-time data delivery including account activity monitoring, price updates, and WebSocket connection management with type-safe controller integration.

Table of Contents

Installation

yarn add @metamask/core-backend

or

npm install @metamask/core-backend

Quick Start

Basic Usage

WebSocket for Real-time Updates:

import {
  BackendWebSocketService,
  AccountActivityService,
} from '@metamask/core-backend';

// Initialize Backend WebSocket service
const backendWebSocketService = new BackendWebSocketService({
  messenger: backendWebSocketServiceMessenger,
  url: 'wss://api.metamask.io/ws',
  timeout: 15000,
  requestTimeout: 20000,
});

// Initialize Account Activity service
const accountActivityService = new AccountActivityService({
  messenger: accountActivityMessenger,
});

// Connect and subscribe to account activity
await backendWebSocketService.connect();
await accountActivityService.subscribe({
  address: 'eip155:0:0x742d35cc6634c0532925a3b8d40c4e0e2c6e4e6',
});

// Listen for real-time updates
messenger.subscribe('AccountActivityService:transactionUpdated', (tx) => {
  console.log('New transaction:', tx);
});

messenger.subscribe(
  'AccountActivityService:balanceUpdated',
  ({ address, updates }) => {
    console.log(`Balance updated for ${address}:`, updates);
  },
);

HTTP API for REST Requests:

import { ApiPlatformClient } from '@metamask/core-backend';

// Create API client
const apiClient = new ApiPlatformClient({
  clientProduct: 'metamask-extension',
  getBearerToken: async () => authController.getBearerToken(),
});

// Fetch data with automatic caching and deduplication
const balances = await apiClient.accounts.fetchV5MultiAccountBalances([
  'eip155:1:0x742d35cc6634c0532925a3b8d40c4e0e2c6e4e6',
]);

const prices = await apiClient.prices.fetchV3SpotPrices([
  'eip155:1/slip44:60', // ETH
  'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
]);

Integration with Controllers

// Coordinate with TokenBalancesController for fallback polling
messenger.subscribe(
  'BackendWebSocketService:connectionStateChanged',
  (info) => {
    if (info.state === 'CONNECTED') {
      // Reduce polling when WebSocket is active
      messenger.call(
        'TokenBalancesController:updateChainPollingConfigs',
        { '0x1': { interval: 600000 } }, // 10 min backup polling
        { immediateUpdate: false },
      );
    } else {
      // Increase polling when WebSocket is down
      const defaultInterval = messenger.call(
        'TokenBalancesController:getDefaultPollingInterval',
      );
      messenger.call(
        'TokenBalancesController:updateChainPollingConfigs',
        { '0x1': { interval: defaultInterval } },
        { immediateUpdate: true },
      );
    }
  },
);

// Listen for account changes and manage subscriptions
messenger.subscribe(
  'AccountsController:selectedAccountChange',
  async (selectedAccount) => {
    if (selectedAccount) {
      await accountActivityService.subscribe({
        address: selectedAccount.address,
      });
    }
  },
);

Architecture & Design

Layered Architecture

graph TD
    subgraph "FRONTEND"
        subgraph "Presentation Layer"
            FE[Frontend Applications<br/>MetaMask Extension, Mobile, etc.]
        end

        subgraph "Integration Layer"
            IL[Controllers, State Management, UI]
        end

        subgraph "Data layer (core-backend)"
            subgraph "Domain Services"
                AAS[AccountActivityService]
                PUS[PriceUpdateService<br/>future]
                CS[Custom Services...]
            end

            subgraph "Transport Layer"
                WSS[WebSocketService<br/>• Connection management<br/>• Automatic reconnection<br/>• Message routing<br/>• Subscription management]
                HTTP[HTTP API Clients<br/>• REST API calls<br/>• Automatic caching<br/>• Request deduplication<br/>• Retry with backoff]
            end
        end
    end

    subgraph "BACKEND"
        BS[Backend Services<br/>REST APIs, WebSocket Services, etc.]
    end

    %% Flow connections
    FE --> IL
    IL --> AAS
    IL --> PUS
    IL --> CS
    AAS --> WSS
    AAS --> HTTP
    PUS --> WSS
    PUS --> HTTP
    CS --> WSS
    CS --> HTTP
    WSS <--> BS
    HTTP <--> BS

    %% Styling
    classDef frontend fill:#e1f5fe
    classDef backend fill:#f3e5f5
    classDef service fill:#e8f5e8
    classDef transport fill:#fff3e0

    class FE,IL frontend
    class BS backend
    class AAS,PUS,CS service
    class WSS,HTTP transport
Loading

Dependencies Structure

graph BT
    %% External Controllers
    AC["AccountsController<br/>(Auto-generated types)"]
    AuthC["AuthenticationController<br/>(Auto-generated types)"]
    TBC["TokenBalancesController<br/>(External Integration)"]

    %% Core Services
    AA["AccountActivityService"]
    WS["BackendWebSocketService"]

    %% Dependencies & Type Imports
    AC -.->|"Import types<br/>(DRY)" | AA
    AuthC -.->|"Import types<br/>(DRY)" | WS
    WS -->|"Messenger calls"| AA
    AA -.->|"Event publishing"| TBC

    %% Styling
    classDef core fill:#f3e5f5
    classDef integration fill:#fff3e0
    classDef controller fill:#e8f5e8

    class WS,AA core
    class TBC integration
    class AC,AuthC controller
Loading

Data Flow

Sequence Diagram: Real-time Account Activity Flow

sequenceDiagram
    participant TBC as TokenBalancesController
    participant AA as AccountActivityService
    participant WS as BackendWebSocketService
    participant HTTP as HTTP Services<br/>(APIs & RPC)
    participant Backend as WebSocket Endpoint<br/>(Backend)

    Note over TBC,Backend: Initial Setup
    TBC->>HTTP: Initial balance fetch via HTTP<br/>(first request for current state)

    WS->>Backend: WebSocket connection request
    Backend->>WS: Connection established
    WS->>AA: WebSocket connection status notification<br/>(BackendWebSocketService:connectionStateChanged)<br/>{state: 'CONNECTED'}

    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

    par StatusChanged Event
      TBC->>HTTP: Fetch balances for new account<br/>(fill transition gap)
    and Account Subscription
      AA->>AA: User switched to different account<br/>(AccountsController:selectedAccountChange)
      AA->>WS: subscribe (new account)
      WS->>Backend: {event: 'subscribe', channels: ['account-activity.v1.eip155:0:0x456...']}
      Backend->>WS: {event: 'subscribe-response', subscriptionId: 'sub-789'}
      AA->>WS: unsubscribe (previous account)
      WS->>Backend: {event: 'unsubscribe', subscriptionId: 'sub-456'}
      Backend->>WS: {event: 'unsubscribe-response'}
    end


    Note over TBC,Backend: Real-time Data Flow

    Backend->>WS: {event: 'notification', channel: 'account-activity.v1.eip155:0:0x123...',<br/>data: {address, tx, updates}}
    WS->>AA: Direct callback routing
    AA->>AA: Validate & process AccountActivityMessage

    par Balance Update
        AA->>TBC: Real-time balance change notification<br/>(AccountActivityService:balanceUpdated)<br/>{address, chain, updates}
        TBC->>TBC: Update balance state directly<br/>(or fallback poll if error)
    and Transaction and Activity Update (Not yet implemented)
        AA->>AA: Process transaction data<br/>(AccountActivityService:transactionUpdated)<br/>{tx: Transaction}
        Note right of AA: Future: Forward to TransactionController<br/>for transaction state management<br/>(pending → confirmed → finalized)
    end

    Note over TBC,Backend: System Notifications

    Backend->>WS: {event: 'system-notification', data: {chainIds: ['eip155:137'], status: 'down'}}
    WS->>AA: System notification received
    AA->>AA: Process chain status change
    AA->>TBC: Chain status notification<br/>(AccountActivityService:statusChanged)<br/>{chainIds: ['eip155:137'], status: 'down'}
    TBC->>TBC: Decrease polling interval from 10min to 20s<br/>(.updateChainPollingConfigs({0x89: 20000}))
    TBC->>HTTP: Fetch balances immediately

    Backend->>WS: {event: 'system-notification', data: {chainIds: ['eip155:137'], status: 'up'}}
    WS->>AA: System notification received
    AA->>AA: Process chain status change
    AA->>TBC: Chain status notification<br/>(AccountActivityService:statusChanged)<br/>{chainIds: ['eip155:137'], status: 'up'}
    TBC->>TBC: Increase polling interval from 20s to 10min<br/>(.updateChainPollingConfigs({0x89: 600000}))

    Note over TBC,Backend: Connection Health Management

    Backend-->>WS: Connection lost
    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

    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
Loading

Key Flow Characteristics

  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, 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

HTTP API

Overview

The HTTP API provides type-safe clients for accessing MetaMask backend REST APIs. It uses @tanstack/query-core for intelligent caching, request deduplication, and automatic retries.

Available APIs:

API Base URL Purpose
Accounts accounts.api.cx.metamask.io Balances, transactions, NFTs, token discovery
Prices price.api.cx.metamask.io Spot prices, exchange rates, historical prices
Token token.api.cx.metamask.io Token metadata, trending, top gainers
Tokens tokens.api.cx.metamask.io Bulk asset operations, supported networks

Features

  • Automatic request deduplication - Identical concurrent requests share a single network call
  • Intelligent caching - Configurable stale times per data type (prices: 30s, balances: 1min, networks: 30min)
  • Automatic retries - Exponential backoff with jitter, skips 4xx errors (except 429, 408)
  • Type safety - Full TypeScript support with response types
  • Bearer token caching - Auth tokens cached for 5 minutes
  • Unified client - Single entry point or individual API clients

Quick Start

import {
  ApiPlatformClient,
  createApiPlatformClient,
} from '@metamask/core-backend';

// Create unified client
const client = new ApiPlatformClient({
  clientProduct: 'metamask-extension',
  clientVersion: '12.0.0',
  getBearerToken: async () => authController.getBearerToken(),
});

// Access API methods through sub-clients
const networks = await client.accounts.fetchV2SupportedNetworks();
const balances = await client.accounts.fetchV5MultiAccountBalances([
  'eip155:1:0x742d35cc6634c0532925a3b8d40c4e0e2c6e4e6',
]);
const prices = await client.prices.fetchV3SpotPrices([
  'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
]);
const tokenList = await client.token.fetchTokenList(1);
const assets = await client.tokens.fetchV3Assets([
  'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
]);

Or use individual clients:

import { AccountsApiClient, PricesApiClient } from '@metamask/core-backend';

const accountsClient = new AccountsApiClient({
  clientProduct: 'metamask-extension',
});

const pricesClient = new PricesApiClient({
  clientProduct: 'metamask-extension',
  getBearerToken: async () => token,
});

API Clients

Optional parameters: options is FetchOptions (e.g. staleTime, gcTime). queryOptions are API-specific filters (e.g. networks, cursor). Each fetch* method has a matching get*QueryOptions that returns the TanStack Query options object for use with useQuery, useInfiniteQuery, useSuspenseQuery, etc.

AccountsApiClient

Handles account-related operations including balances, transactions, NFTs, and token discovery.

Method Description
fetchV1SupportedNetworks(options?) Get supported networks (v1)
fetchV2SupportedNetworks(options?) Get supported networks (v2)
fetchV2ActiveNetworks(accountIds, queryOptions?, options?) Get active networks by CAIP-10 account IDs
fetchV2Balances(address, queryOptions?, options?) Get balances for single address (supports networks, filterSupportedTokens, includeTokenAddresses, includeStakedAssets)
fetchV4MultiAccountBalances(addresses, queryOptions?, options?) Get balances for multiple addresses
fetchV5MultiAccountBalances(accountIds, queryOptions?, options?) Get balances using CAIP-10 IDs
fetchV1TransactionByHash(chainId, txHash, queryOptions?, options?) Get transaction by hash
fetchV1AccountTransactions(address, queryOptions?, options?) Get account transactions
fetchV4MultiAccountTransactions(accountAddresses, queryOptions?, options?) Get multi-account transactions
fetchV1AccountRelationship(chainId, from, to, options?) Get address relationship
fetchV2AccountNfts(address, queryOptions?, options?) Get account NFTs
fetchV2AccountTokens(address, queryOptions?, options?) Get detected ERC20 tokens
getV1SupportedNetworksQueryOptions(options?)getV2AccountTokensQueryOptions(...) Return TanStack Query options for each fetch (use with useQuery, useInfiniteQuery, etc.)
invalidateBalances() Invalidate all balance cache
invalidateAccounts() Invalidate all account cache

PricesApiClient

Handles price-related operations including spot prices, exchange rates, and historical data.

Method Description
fetchPriceV1SupportedNetworks(options?) Get price-supported networks (v1)
fetchPriceV2SupportedNetworks(options?) Get price-supported networks in CAIP format (v2)
fetchV1ExchangeRates(baseCurrency, options?) Get exchange rates for base currency
fetchV1FiatExchangeRates(options?) Get fiat exchange rates
fetchV1CryptoExchangeRates(options?) Get crypto exchange rates
fetchV1SpotPricesByCoinIds(coinIds, options?) Get spot prices by CoinGecko IDs
fetchV1SpotPriceByCoinId(coinId, currency?, options?) Get single coin spot price
fetchV1TokenPrices(chainId, addresses, queryOptions?, options?) Get token prices on chain
fetchV1TokenPrice(chainId, address, currency?, options?) Get single token price
fetchV2SpotPrices(chainId, addresses, queryOptions?, options?) Get spot prices with market data
fetchV3SpotPrices(assetIds, queryOptions?, options?) Get spot prices by CAIP-19 asset IDs
fetchV1HistoricalPricesByCoinId(coinId, queryOptions?, options?) Get historical prices by CoinGecko ID
fetchV1HistoricalPricesByTokenAddresses(chainId, addresses, queryOptions?, options?) Get historical prices for tokens
fetchV1HistoricalPrices(chainId, address, queryOptions?, options?) Get historical prices for single token
fetchV3HistoricalPrices(chainId, assetType, queryOptions?, options?) Get historical prices by CAIP-19
fetchV1HistoricalPriceGraphByCoinId(coinId, queryOptions?, options?) Get price graph by CoinGecko ID
fetchV1HistoricalPriceGraphByTokenAddress(chainId, address, queryOptions?, options?) Get price graph by token address
getPriceV1SupportedNetworksQueryOptions(options?)getV1HistoricalPriceGraphByTokenAddressQueryOptions(...) Return TanStack Query options for each fetch
invalidatePrices() Invalidate all price cache

TokenApiClient

Handles token metadata, lists, and trending/popular token discovery.

Method Description
fetchNetworks(options?) Get all networks
fetchNetworkByChainId(chainId, options?) Get network by chain ID
fetchTokenList(chainId, queryOptions?, options?) Get token list for chain
fetchV1TokenMetadata(chainId, address, queryOptions?, options?) Get token metadata
fetchTokenDescription(chainId, address, options?) Get token description
fetchV3TrendingTokens(chainIds, queryOptions?, options?) Get trending tokens
fetchV3TopGainers(chainIds, queryOptions?, options?) Get top gainers/losers
fetchV3PopularTokens(chainIds, queryOptions?, options?) Get popular tokens
fetchTopAssets(chainId, options?) Get top assets for chain
fetchV1SuggestedOccurrenceFloors(options?) Get suggested occurrence floors
getNetworksQueryOptions(options?)getV1SuggestedOccurrenceFloorsQueryOptions(...) Return TanStack Query options for each fetch

TokensApiClient

Handles bulk token operations and supported network queries.

Method Description
fetchTokenV1SupportedNetworks(options?) Get token-supported networks (v1)
fetchTokenV2SupportedNetworks(options?) Get token-supported networks with full/partial support (v2)
fetchV3Assets(assetIds, queryOptions?, fetchOptions?) Fetch assets by CAIP-19 IDs
getTokenV1SupportedNetworksQueryOptions(options?)getV3AssetsQueryOptions(...) Return TanStack Query options for each fetch
invalidateTokens() Invalidate all token cache

Configuration

type ApiPlatformClientOptions = {
  /** Client product identifier (e.g., 'metamask-extension', 'metamask-mobile') */
  clientProduct: string;
  /** Optional client version (default: '1.0.0') */
  clientVersion?: string;
  /** Function to get bearer token for authenticated requests */
  getBearerToken?: () => Promise<string | undefined>;
  /** Optional custom QueryClient instance for shared caching */
  queryClient?: QueryClient;
};

Default Stale Times:

Data Type Stale Time
Prices 30 seconds
Balances 1 minute
Transactions 30 seconds
Networks 10 minutes
Supported Networks 30 minutes
Token Metadata 5 minutes
Token List 10 minutes
Exchange Rates 5 minutes
Trending 2 minutes
Auth Token 5 minutes

Override Stale Time:

// Use custom stale time for specific request
const balances = await client.accounts.fetchV5MultiAccountBalances(
  accountIds,
  { networks: ['eip155:1'] },
  { staleTime: 10000 }, // 10 seconds
);

Cache Management

// Invalidate all caches
await client.invalidateAll();

// Invalidate auth token (on logout)
await client.invalidateAuthToken();

// Domain-specific invalidation
await client.accounts.invalidateBalances();
await client.prices.invalidatePrices();
await client.tokens.invalidateTokens();

// Clear all cached data
client.clear();

// Check if query is fetching
const isFetching = client.isFetching(['accounts', 'balances']);

// Access cached data directly
const cached = client.getCachedData(['accounts', 'balances', 'v5', { ... }]);

// Set cached data
client.setCachedData(queryKey, data);

// Access underlying QueryClient for advanced usage
const queryClient = client.queryClient;

API Reference

BackendWebSocketService

The core WebSocket client providing connection management, authentication, and message routing.

Constructor Options

interface BackendWebSocketServiceOptions {
  messenger: BackendWebSocketServiceMessenger;
  url: string;
  timeout?: number;
  reconnectDelay?: number;
  maxReconnectDelay?: number;
  requestTimeout?: number;
  enableAuthentication?: boolean;
  enabledCallback?: () => boolean;
}

Methods

  • connect(): Promise<void> - Establish authenticated WebSocket connection
  • disconnect(): Promise<void> - Close WebSocket connection
  • subscribe(options: SubscriptionOptions): Promise<SubscriptionResult> - Subscribe to channels
  • sendRequest(message: ClientRequestMessage): Promise<ServerResponseMessage> - Send request/response messages
  • channelHasSubscription(channel: string): boolean - Check subscription status
  • findSubscriptionsByChannelPrefix(prefix: string): SubscriptionInfo[] - Find subscriptions by prefix
  • getConnectionInfo(): WebSocketConnectionInfo - Get detailed connection state

AccountActivityService

High-level service for monitoring account activity using WebSocket data.

Constructor Options

interface AccountActivityServiceOptions {
  messenger: AccountActivityServiceMessenger;
  subscriptionNamespace?: string;
}

Methods

  • subscribe(subscription: SubscriptionOptions): Promise<void> - Subscribe to account activity
  • unsubscribe(subscription: SubscriptionOptions): Promise<void> - Unsubscribe from account activity

Events Published

  • AccountActivityService:balanceUpdated - Real-time balance changes
  • AccountActivityService:transactionUpdated - Transaction status updates
  • AccountActivityService:statusChanged - Chain/service status changes