Skip to content

Conversation

Kriys94
Copy link
Contributor

@Kriys94 Kriys94 commented Sep 8, 2025

Description

This PR implements the @metamask/backend-platform package, providing a unified real-time data layer for MetaMask applications through WebSocket services and event-driven architecture.

What Changed

  • WebSocketService Class: Core WebSocket connection management with automatic reconnection, message queuing, and health monitoring
  • AccountActivityService Class: Real-time account activity monitoring with balance updates, transaction tracking, and account switching
  • RestrictedMessenger Integration: Event-driven communication system for loose coupling between core services and consuming controllers
  • TypeScript Interface Definitions: Comprehensive type safety for WebSocket events, service configurations, and cross-package data contracts
  • Error Recovery Architecture: Robust handling of connection failures, network interruptions, and service degradation scenarios
  • Controller Integration Pattern: Standardized approach for existing controllers to leverage real-time capabilities

Why These Changes

Current Core packages, i.e. TokenBalancesController or RatesController, lacks real-time data capabilities, forcing all consuming applications to implement polling-based architectures:

Performance Problems: 20-30 second polling intervals create poor user experience for balance updates and transaction status
Resource Waste: Continuous HTTP polling consumes unnecessary bandwidth and server resources

Technical Implementation

  • Connection Management: WebSocketService handles connection lifecycle, reconnection logic, and message buffering
  • Event-Driven Architecture: Services communicate through RestrictedMessenger events, enabling loose coupling
  • Service Discovery Pattern: Controllers can dynamically discover and integrate with backend platform services
  • Graceful Degradation: Services continue functioning when WebSocket is unavailable, falling back to HTTP polling

Related issues

Integration PRs:

Manual testing steps

Scenario: WebSocket service initialization and connection
  Given I have imported the @metamask/backend-platform package
  And I create a WebSocketService with valid configuration
  When I call webSocketService.connect()
  Then the service should establish WebSocket connection successfully
  And connection state should change to 'connected'
  And onConnectionStateChanged event should be emitted

Scenario: AccountActivityService integration with controllers
  Given I have initialized AccountActivityService with messenger
  And I have subscribed to account activity for address 0x123...
  When a balance change occurs for that address
  Then the service should emit AccountActivity:balanceChanged event
  And consuming controllers should receive the event via messenger
  And event payload should contain updated balance data

Scenario: Error recovery and reconnection
  Given I have an active WebSocket connection
  When the connection is forcibly closed due to network issue
  Then the WebSocketService should automatically attempt reconnection
  And reconnection should succeed within 10 seconds
  And any buffered messages should be sent after reconnection

Scenario: Graceful degradation without WebSocket
  Given the WebSocket endpoint is unavailable
  When I initialize the backend platform services
  Then services should start successfully without throwing errors
  And consumers should be able to fallback to HTTP polling
  And application functionality should remain intact

Screenshots/Recordings

Service Architecture:

  • Code walkthrough showing WebSocketService class structure and key methods
  • Demonstration of AccountActivityService event emission and handling
  • RestrictedMessenger integration showing event flow between services

Integration Examples:

  • TokenBalancesController integration with backend platform services
  • Error handling and recovery scenarios in development environment
  • TypeScript compiler validation showing type safety across packages

Performance Metrics:

  • Memory usage comparison before/after WebSocket service initialization
  • Network request reduction when WebSocket is active vs HTTP polling
  • Service startup time measurements

Pre-merge checklist

  • I have followed MetaMask Core Coding Standards
  • I have completed the template fully
  • I have self-reviewed the PR and addressed any linting errors
  • I have added comprehensive unit tests for WebSocketService and AccountActivityService
  • I have added integration tests for RestrictedMessenger event handling
  • I have tested error recovery and reconnection scenarios
  • I have verified TypeScript compilation across dependent packages
  • I have tested graceful degradation when WebSocket is unavailable
  • I have validated memory management and connection cleanup
  • I have updated relevant JSDoc comments and type definitions
  • I have added changelog entry (above)
  • All existing Core package tests pass with new services
  • Performance benchmarking completed for service initialization
  • Cross-package type references resolve correctly

Additional Notes

Documentation Reference

📖 Complete Technical Documentation: https://raw.githubusercontent.com/MetaMask/core/3919440aff6718247715b0add0afa52774d8f6ad/packages/backend-platform/README.md
The README contains comprehensive architecture diagrams, integration guides, and detailed API documentation for implementing real-time data services.

API Interfaces

WebSocketService:

export class WebSocketService {
  constructor(config: WebSocketServiceConfig)
  connect(): Promise<void>
  disconnect(): void
  send(message: WebSocketMessage): void
  on(event: 'connectionStateChanged', handler: (state: ConnectionState) => void): void
}

AccountActivityService:

export class AccountActivityService {
  constructor(messenger: AccountActivityServiceMessenger)
  subscribeToAccount(address: string): Promise<void>
  unsubscribeFromAccount(address: string): void
  getAccountActivity(address: string): Promise<AccountActivity>
}

Integration Pattern for Controllers

// Example integration in existing controller
class TokenBalancesController {
  constructor(messenger: RestrictedControllerMessenger) {
    this.messagingSystem = messenger;
    
    // Subscribe to real-time balance updates
    this.messagingSystem.subscribe(
      'BackendAccountActivityService:balanceChanged',
      this.handleBalanceUpdate.bind(this)
    );
  }
  
  private handleBalanceUpdate(payload: BalanceChangedPayload) {
    // Update controller state with real-time data
    this.update((state) => ({
      ...state,
      balances: { ...state.balances, [payload.address]: payload.balance }
    }));
  }
}

Files

  • packages/backend-platform/src/WebSocketService.ts - Core WebSocket connection management
  • packages/backend-platform/src/AccountActivityService.ts - Real-time account monitoring
  • packages/backend-platform/src/types.ts - TypeScript interface definitions
  • packages/backend-platform/src/index.ts - Package exports and public API
  • packages/backend-platform/package.json - Package configuration and dependencies
  • packages/backend-platform/tsconfig.json - TypeScript compilation settings
  • packages/assets-controllers/src/TokenBalancesController.ts - Integration example
  • packages/assets-controllers/package.json - Added backend-platform dependency
  • packages/assets-controllers/tsconfig.json - Added workspace reference

The backend platform package is now implemented with robust WebSocket services and ready for Extension and Mobile integration through standardized controller patterns.

@Kriys94 Kriys94 force-pushed the feature/AccountActivity2 branch from 490eef2 to 2855aa0 Compare September 9, 2025 14:41
@Kriys94
Copy link
Contributor Author

Kriys94 commented Sep 9, 2025

@metamaskbot publish-preview

Copy link
Contributor

github-actions bot commented Sep 9, 2025

Preview builds have been published. See these instructions for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/account-tree-controller": "0.13.1-preview-2855aa0",
  "@metamask-previews/accounts-controller": "33.0.0-preview-2855aa0",
  "@metamask-previews/address-book-controller": "6.1.1-preview-2855aa0",
  "@metamask-previews/announcement-controller": "7.0.3-preview-2855aa0",
  "@metamask-previews/app-metadata-controller": "1.0.0-preview-2855aa0",
  "@metamask-previews/approval-controller": "7.1.3-preview-2855aa0",
  "@metamask-previews/assets-controllers": "74.3.3-preview-2855aa0",
  "@metamask-previews/backend-platform": "0.0.0-preview-2855aa0",
  "@metamask-previews/base-controller": "8.3.0-preview-2855aa0",
  "@metamask-previews/bridge-controller": "42.0.0-preview-2855aa0",
  "@metamask-previews/bridge-status-controller": "42.0.0-preview-2855aa0",
  "@metamask-previews/build-utils": "3.0.3-preview-2855aa0",
  "@metamask-previews/chain-agnostic-permission": "1.1.1-preview-2855aa0",
  "@metamask-previews/composable-controller": "11.0.0-preview-2855aa0",
  "@metamask-previews/controller-utils": "11.12.0-preview-2855aa0",
  "@metamask-previews/delegation-controller": "0.7.0-preview-2855aa0",
  "@metamask-previews/earn-controller": "7.0.0-preview-2855aa0",
  "@metamask-previews/eip-5792-middleware": "1.0.0-preview-2855aa0",
  "@metamask-previews/eip1193-permission-middleware": "1.0.0-preview-2855aa0",
  "@metamask-previews/ens-controller": "17.0.1-preview-2855aa0",
  "@metamask-previews/error-reporting-service": "2.0.0-preview-2855aa0",
  "@metamask-previews/eth-json-rpc-provider": "4.1.8-preview-2855aa0",
  "@metamask-previews/foundryup": "1.0.1-preview-2855aa0",
  "@metamask-previews/gas-fee-controller": "24.0.0-preview-2855aa0",
  "@metamask-previews/gator-permissions-controller": "0.1.0-preview-2855aa0",
  "@metamask-previews/json-rpc-engine": "10.0.3-preview-2855aa0",
  "@metamask-previews/json-rpc-middleware-stream": "8.0.7-preview-2855aa0",
  "@metamask-previews/keyring-controller": "23.0.0-preview-2855aa0",
  "@metamask-previews/logging-controller": "6.0.4-preview-2855aa0",
  "@metamask-previews/message-manager": "12.0.2-preview-2855aa0",
  "@metamask-previews/messenger": "0.2.0-preview-2855aa0",
  "@metamask-previews/multichain-account-service": "0.7.0-preview-2855aa0",
  "@metamask-previews/multichain-api-middleware": "1.0.0-preview-2855aa0",
  "@metamask-previews/multichain-network-controller": "0.12.0-preview-2855aa0",
  "@metamask-previews/multichain-transactions-controller": "5.0.0-preview-2855aa0",
  "@metamask-previews/name-controller": "8.0.3-preview-2855aa0",
  "@metamask-previews/network-controller": "24.1.0-preview-2855aa0",
  "@metamask-previews/network-enablement-controller": "0.4.0-preview-2855aa0",
  "@metamask-previews/notification-services-controller": "17.0.0-preview-2855aa0",
  "@metamask-previews/permission-controller": "11.0.6-preview-2855aa0",
  "@metamask-previews/permission-log-controller": "4.0.0-preview-2855aa0",
  "@metamask-previews/phishing-controller": "13.1.0-preview-2855aa0",
  "@metamask-previews/polling-controller": "14.0.0-preview-2855aa0",
  "@metamask-previews/preferences-controller": "19.0.0-preview-2855aa0",
  "@metamask-previews/profile-sync-controller": "24.0.0-preview-2855aa0",
  "@metamask-previews/rate-limit-controller": "6.0.3-preview-2855aa0",
  "@metamask-previews/remote-feature-flag-controller": "1.7.0-preview-2855aa0",
  "@metamask-previews/sample-controllers": "1.0.0-preview-2855aa0",
  "@metamask-previews/seedless-onboarding-controller": "4.0.0-preview-2855aa0",
  "@metamask-previews/selected-network-controller": "23.0.0-preview-2855aa0",
  "@metamask-previews/shield-controller": "0.1.2-preview-2855aa0",
  "@metamask-previews/signature-controller": "33.0.0-preview-2855aa0",
  "@metamask-previews/subscription-controller": "0.0.0-preview-2855aa0",
  "@metamask-previews/token-search-discovery-controller": "3.3.0-preview-2855aa0",
  "@metamask-previews/transaction-controller": "60.2.0-preview-2855aa0",
  "@metamask-previews/user-operation-controller": "39.0.0-preview-2855aa0"
}

@Kriys94 Kriys94 force-pushed the feature/AccountActivity2 branch from 2855aa0 to 5fcd7ee Compare September 10, 2025 15:24
@Kriys94
Copy link
Contributor Author

Kriys94 commented Sep 15, 2025

@metamaskbot publish-preview

@Kriys94 Kriys94 marked this pull request as ready for review September 15, 2025 08:25
@Kriys94 Kriys94 requested review from a team as code owners September 15, 2025 08:25
cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

"@ethersproject/contracts": "^5.7.0",
"@ethersproject/providers": "^5.7.0",
"@metamask/abi-utils": "^2.0.3",
"@metamask/backend-platform": "workspace:^",
Copy link
Contributor

Choose a reason for hiding this comment

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

i guess we will replace this with a published version ?

Copy link
Contributor

Choose a reason for hiding this comment

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

We can already just reference the local version instead of workspace

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I will create another PR for assets-controller changes

Copy link
Contributor

@mcmire mcmire left a comment

Choose a reason for hiding this comment

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

Hey, I noticed that a new package was being added to the monorepo. I haven't dived deeply into the implementation yet but I did leave some comments.

It looks like we are introducing a new package and then trying to use it right away. To make the changes easier to merge and queue up, what are your thoughts on extracting the changes to assets-controllers to a new PR?

};

// Action types for the messaging system
export type AccountActivityServiceSubscribeAccountsAction = {
Copy link
Contributor

@mcmire mcmire Sep 15, 2025

Choose a reason for hiding this comment

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

There is a pattern that we introduced a little while ago that DRYs up the code around "method actions", or actions that merely point to methods.

The way that this works is:

  • You define a MESSENGER_EXPOSED_METHOD constant that lists each method you'd like to expose through the messenger
  • You run yarn generate-method-action-types to generate a types file
  • You import AccountActivityServiceMethodActions from the generated file into this file, and you add this type to the AccountActivityServiceActions union.
  • Instead of registering each action manually in the constructor, you add:
    this.#messenger.registerMethodActionHandlers(
      this,
      MESSENGER_EXPOSED_METHODS,
    );
    
  • Now you can remove AccountActivityServiceSubscribeAccountsAction, AccountActivityServiceUnsubscribeAccountsAction, etc.

What do you think of using this pattern? You can see an example in SampleGasPricesService here:

const MESSENGER_EXPOSED_METHODS = ['fetchGasPrices'] as const;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, good tip. I made the change. I wonder why this does not exist for events?

Copy link
Contributor

Choose a reason for hiding this comment

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

Typically events do not correlate with methods on controllers or services, so they need to be listed by hand anyway.

AccountActivityServiceMessenger,
} from './AccountActivityService';
export {
ACCOUNT_ACTIVITY_SERVICE_ALLOWED_ACTIONS,
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason why these constants are being exported? We don't typically do this in packages. If we need to use an action or event elsewhere we repeat it.

| NetworkControllerStateChangeEvent
| KeyringControllerAccountRemovedEvent;
| KeyringControllerAccountRemovedEvent
| AccountActivityServiceBalanceUpdatedEvent
Copy link
Contributor

Choose a reason for hiding this comment

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

Heads up that updates to AllowedEvents in a package are breaking changes (because the allowlist needs to be updated on the client side). Can you mention this in the changelog?

TokenBalancesControllerState
>;

export type TokenBalancesControllerUpdateChainPollingConfigsAction = {
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like we are removing these actions, should we mention this in the changelog?

*
* @returns The default polling interval in milliseconds
*/
getDefaultPollingInterval(): number {
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like we are adding a new method to the controller, should we mention this in the changelog?

* Processes balance updates and updates the token balance state
* If any balance update has an error, triggers fallback polling for the chain
*/
readonly #onAccountActivityBalanceUpdate = async ({
Copy link
Contributor

Choose a reason for hiding this comment

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

Anything to mention in the changelog about these changes? Any differences in behavior?

cursor[bot]

This comment was marked as outdated.

@mcmire
Copy link
Contributor

mcmire commented Sep 15, 2025

@Kriys94 I realize that your team is named Backend Platform, but I'm wondering whether this is the best package name to use.

Is the plan to place all services that talk to internal APIs in this package from now on? Do you plan on moving any code from other controllers to this package in the future? It seems a bit broad in scope and I am concerned about it becoming a sort of catch-all similar to the assets-controllers package (the bigger a package is, the more impact it could have on other packages if there is a major version).

If having a backend-platform package still makes sense, what are your thoughts on at least extracting WebSocketService to its own package? It seems like it could be useful in a more general sense, to talk to any WebSocket endpoint. Or do we have specific logic that suits our APIs?

Copy link
Contributor

@mcmire mcmire left a comment

Choose a reason for hiding this comment

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

Some more minor suggestions. I haven't done a full pass of the implementation so I may come back and make some more comments later but this is what I noticed for now.

},
) {
this.#messenger = options.messenger;
this.#webSocketService = options.webSocketService;
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of taking the WebSocketService as an argument, what are your thoughts on having it use it through the messenger?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is more a habit from someone not used to code in MM codebase, I find this more straightforward to inject WebSocketService as the dependency is easier to understand for IDEs (like understanding where functions are implemented). Here I know that the AccountActivityService logic fully depend on WebsocketService from the BackendPlatform package.

But I can switch to messenger if necessary


readonly #options: Required<WebSocketServiceOptions>;

#ws!: WebSocket;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why can we assume this is set?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because #ws is set only when we successfully established the connection. In case of error #ws is undefined.

try {
return JSON.parse(data);
} catch {
// Fail fast on parse errors (mobile optimization)
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we want to print a message here saying that JSON parsing failed?

// Publish connection state change event
try {
this.#messenger.publish(
'BackendWebSocketService:connectionStateChanged',
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we use SERVICE_NAME here?

Suggested change
'BackendWebSocketService:connectionStateChanged',
`${SERVICE_NAME}:connectionStateChanged`,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've heard that ${SERVICE_NAME}:connectionStateChanged is not recommended because it makes the search of the event or action more difficult to find

@Kriys94
Copy link
Contributor Author

Kriys94 commented Sep 17, 2025

@metamaskbot publish-preview

cursor[bot]

This comment was marked as outdated.

@Kriys94
Copy link
Contributor Author

Kriys94 commented Sep 17, 2025

Note: Move Debouncer in the pooling directly

@Kriys94
Copy link
Contributor Author

Kriys94 commented Sep 17, 2025

For new Token we need to call "addToken" from TokenController. First check if this is part of allToken (imported tokens). As a second PR.

@Kriys94
Copy link
Contributor Author

Kriys94 commented Sep 17, 2025

Exclude updates if a token is not part of allTokens or allIgnoredTokens

cursor[bot]

This comment was marked as outdated.

@Kriys94
Copy link
Contributor Author

Kriys94 commented Sep 18, 2025

@Kriys94 I realize that your team is named Backend Platform, but I'm wondering whether this is the best package name to use.

Is the plan to place all services that talk to internal APIs in this package from now on? Do you plan on moving any code from other controllers to this package in the future? It seems a bit broad in scope and I am concerned about it becoming a sort of catch-all similar to the assets-controllers package (the bigger a package is, the more impact it could have on other packages if there is a major version).

If having a backend-platform package still makes sense, what are your thoughts on at least extracting WebSocketService to its own package? It seems like it could be useful in a more general sense, to talk to any WebSocket endpoint. Or do we have specific logic that suits our APIs?

> Is the plan to place all services that talk to internal APIs in this package from now on?

Yes, that's the plan even if that will progressive

Do you plan on moving any code from other controllers to this package in the future?

Not moving existing code, but mainly creating new code to interact with any interface that the backend-platform offers. When that will be the case, we would have to modify the Controllers codebase.

It seems a bit broad in scope and I am concerned about it becoming a sort of catch-all similar to the `assets-controllers` package (the bigger a package is, the more impact it could have on other packages if there is a major version).

The backend-platform package is really just interfacing our backend-platform APIs with the Controller logic.

what are your thoughts on at least extracting `WebSocketService` to its own package? It seems like it could be useful in a more general sense, to talk to _any_ WebSocket endpoint. Or do we have specific logic that suits our APIs?`

WebSocketService is really specific to the WebsocketInterface and behavior that the backend platform offers. Unfortunately, I think that a Websocket package that works with any websocket endpoint is not useful, mainly because the other use-cases (Hyperliquid and Solana for now) connect to their respective websocket via their respective SDKs. As such we could not have a generic Websocket interface that we would inject to Solana to Hyperliquid.

cursor[bot]

This comment was marked as outdated.

@Kriys94 Kriys94 force-pushed the feature/AccountActivity2 branch from b222eb5 to 4b89590 Compare September 19, 2025 13:08
cursor[bot]

This comment was marked as outdated.

@Kriys94 Kriys94 force-pushed the feature/AccountActivity2 branch from 4e5692c to 80cde4c Compare September 24, 2025 12:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants