-
-
Notifications
You must be signed in to change notification settings - Fork 250
feat(backend-platform): Add Websocket and AccountActivity Service #6490
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
490eef2
to
2855aa0
Compare
@metamaskbot publish-preview |
Preview builds have been published. See these instructions for more information about preview builds. Expand for full list of packages and versions.
|
2855aa0
to
5fcd7ee
Compare
@metamaskbot publish-preview |
3919440
to
dbd83fd
Compare
"@ethersproject/contracts": "^5.7.0", | ||
"@ethersproject/providers": "^5.7.0", | ||
"@metamask/abi-utils": "^2.0.3", | ||
"@metamask/backend-platform": "workspace:^", |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this 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 = { |
There was a problem hiding this comment.
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 theAccountActivityServiceActions
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:
core/packages/sample-controllers/src/sample-gas-prices-service/sample-gas-prices-service.ts
Line 25 in c45132e
const MESSENGER_EXPOSED_METHODS = ['fetchGasPrices'] as const; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 = { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 ({ |
There was a problem hiding this comment.
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?
@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 If having a |
There was a problem hiding this 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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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', |
There was a problem hiding this comment.
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?
'BackendWebSocketService:connectionStateChanged', | |
`${SERVICE_NAME}:connectionStateChanged`, |
There was a problem hiding this comment.
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
@metamaskbot publish-preview |
Note: Move Debouncer in the pooling directly |
For new Token we need to call "addToken" from TokenController. First check if this is part of allToken (imported tokens). As a second PR. |
Exclude updates if a token is not part of allTokens or allIgnoredTokens |
Yes, that's the plan even if that will progressive
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.
The backend-platform package is really just interfacing our backend-platform APIs with the Controller logic.
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. |
b222eb5
to
4b89590
Compare
4e5692c
to
80cde4c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've gone through quite a few reviews.
Approving, I'll share some notes on some test improvements we can add in a separate PR.
Follow-up of #6490 PR focusing on merging the core-backend package first ## Explanation ### What is the current state, and why does it need to change? Currently, MetaMask relies on polling-based HTTP requests to fetch blockchain data like account balances and transaction updates. This approach has several limitations: - **Performance inefficiency**: Constant polling creates unnecessary network requests and server load - **User experience delays**: Users must wait for the next polling cycle to see updates (typically 20 seconds to 10 minutes) - **Resource consumption**: Mobile devices waste battery on frequent HTTP requests - **Scalability concerns**: As MetaMask grows, polling doesn't scale well for real-time data needs ### What is the solution and how does it work? This PR introduces the **`@metamask/core-backend`** package - a new data layer platform that bridges backend services with MetaMask frontend applications through efficient WebSocket-based real-time communication. The solution provides: 1. **BackendWebSocketService**: Low-level WebSocket connection management with automatic reconnection, authentication integration, and intelligent message routing 2. **AccountActivityService**: High-level service for real-time account activity monitoring 3. **Intelligent Fallback Strategy**: Dynamic coordination with existing HTTP polling controllers - when WebSocket is connected, polling intervals increase from 20s to 10 minutes; when disconnected, polling returns to 20s with immediate balance fetches ### Key architectural benefits: - **Real-time Performance**: Instant delivery of transaction and balance updates via WebSocket push notifications - **Resource Efficiency**: Reduces HTTP requests by 95% when WebSocket is active (10min polling vs 20s polling) - **Multi-chain Support**: CAIP-10 address format support for blockchain interoperability - **Mobile-Optimized**: Lifecycle-aware connection management for background/foreground transitions - **Extensible Design**: Platform foundation for future services (price updates, etc.) ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - High test coverage across BackendWebSocketService and AccountActivityService - Tests cover WebSocket lifecycle, reconnection scenarios, subscription management, and integration patterns - Mobile-specific lifecycle testing included - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - 427-line comprehensive README with architecture diagrams, sequence diagrams, and integration examples - Full JSDoc coverage for all public APIs - TypeDoc configuration for API documentation generation - [x] I've communicated my changes to consumers by updating changelogs for packages I've changed, highlighting breaking changes as necessary - CHANGELOG.md created following Keep a Changelog format - Version 0.0.0 initial release - no breaking changes as this is a new package - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes - No breaking changes - this is an additive new package - Integration examples provided in README for Extension and Mobile consumers - Backward compatible - existing polling continues to work alongside WebSocket enhancement <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds the new @metamask/core-backend package providing a WebSocket client and account activity service, with types, tests, docs, and repo wiring. > > - **Packages**: > - **`packages/core-backend` (new)**: > - `BackendWebSocketService`: authenticated WS client with reconnection, request/response, subscriptions, channel callbacks, and connection-state events. > - `AccountActivityService`: real-time account activity (transactions/balances), system notifications, selected-account resubscription, and supported-chain fetching/caching. > - Public types (`transactions`, `balances`, messages), logger, exports, README, CHANGELOG, LICENSE, Jest config, TypeDoc, tsconfigs. > - Comprehensive tests for connection lifecycle, reconnection, subscriptions, message routing, and controller integration. > - **Repo config**: > - Add CODEOWNERS and teams mapping for `core-backend`. > - Wire into monorepo build/TypeScript refs (`tsconfig*.json`) and `yarn.lock`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 19edb84. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
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
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
Related issues
Integration PRs:
Manual testing steps
Screenshots/Recordings
Service Architecture:
Integration Examples:
Performance Metrics:
Pre-merge checklist
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:
AccountActivityService:
Integration Pattern for Controllers
Files
packages/backend-platform/src/WebSocketService.ts
- Core WebSocket connection managementpackages/backend-platform/src/AccountActivityService.ts
- Real-time account monitoringpackages/backend-platform/src/types.ts
- TypeScript interface definitionspackages/backend-platform/src/index.ts
- Package exports and public APIpackages/backend-platform/package.json
- Package configuration and dependenciespackages/backend-platform/tsconfig.json
- TypeScript compilation settingspackages/assets-controllers/src/TokenBalancesController.ts
- Integration examplepackages/assets-controllers/package.json
- Added backend-platform dependencypackages/assets-controllers/tsconfig.json
- Added workspace referenceThe backend platform package is now implemented with robust WebSocket services and ready for Extension and Mobile integration through standardized controller patterns.