feat(perps): perps controller migration from mobile#7749
feat(perps): perps controller migration from mobile#7749abretonc7s wants to merge 8 commits intomainfrom
Conversation
Complete migration of PerpsController implementation from MetaMask Mobile to enable cross-platform sharing of Perps (Perpetual Futures) functionality. This includes: - PerpsController (~3,000 lines) with full trading functionality - 8 state selectors for UI integration - Comprehensive TypeScript type definitions - 18 utility modules for calculations, formatting, validation - 8 service modules (Trading, MarketData, Eligibility, etc.) - HyperLiquidProvider with full protocol integration - AggregatedPerpsProvider for multi-provider support - Platform services for HyperLiquid client, subscriptions, wallet - Test infrastructure with mocks and 40 unit tests The package uses dependency injection via PerpsPlatformDependencies interface to remain platform-agnostic, allowing Mobile and Extension to provide their own implementations while sharing core business logic.
Add 17 test files migrated from metamask-mobile covering: - Amount conversion, margin, PnL, and position calculations - Order book grouping and order utilities - Market data transformation and sorting - HyperLiquid adapter and validation - TP/SL validation and string parsing utilities Add MIGRATION.md documenting migration status and coverage priorities. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
All alerts resolved. Learn more about Socket for GitHub. This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored. Ignoring alerts on:
|
|
@SocketSecurity ignore npm/@nktkas/hyperliquid@0.30.3 |
|
@SocketSecurity ignore npm/@noble/curves@2.0.1 |
|
@SocketSecurity ignore npm/@metamask/transaction-controller@62.10.0 |
|
I think we need to update the |
- Update @metamask/transaction-controller to ^62.12.0 - Add PR references to CHANGELOG.md - Regenerate yarn.lock Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| export type PerpsPlatformDependencies = { | ||
| // === Observability (stateless utilities) === | ||
| logger: PerpsLogger; | ||
| debugLogger: PerpsDebugLogger; | ||
| metrics: PerpsMetrics; | ||
| performance: PerpsPerformance; | ||
| tracer: PerpsTracer; | ||
|
|
||
| // === Platform Services (mobile/extension specific) === | ||
| streamManager: PerpsStreamManager; | ||
|
|
||
| // === Controller Access (ALL controllers consolidated) === | ||
| controllers: PerpsControllerAccess; | ||
| }; |
There was a problem hiding this comment.
We should use the messenger API for as many of these as possible.
| branches: 0, | ||
| functions: 0, | ||
| lines: 0, | ||
| statements: 0, |
There was a problem hiding this comment.
initially disabled to get coverage and run the POC in extension.
| 'includeInDebugSnapshot', | ||
| ); | ||
| // Debug snapshot should include controller state | ||
| expect(debugState).toBeDefined(); |
There was a problem hiding this comment.
This defeats the purpose of the test. The idea is to ensure we have the expected properties included in the debug snapshot. Same applies below
| // Note: ESLint rules below are disabled because this file was migrated from Mobile | ||
| // which has different ESLint configurations. The following rules are Core-specific | ||
| // and would require extensive refactoring to satisfy. The code is functionally correct. | ||
| /* eslint-disable jsdoc/require-param-description, jsdoc/require-returns */ | ||
| /* eslint-disable no-restricted-syntax */ | ||
| /* eslint-disable @typescript-eslint/naming-convention */ | ||
| /* eslint-disable id-denylist, id-length */ | ||
| /* eslint-disable consistent-return */ | ||
| /* eslint-disable @typescript-eslint/explicit-function-return-type */ | ||
| /* eslint-disable promise/always-return */ | ||
| /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ |
There was a problem hiding this comment.
Some of these certainly seem worth it to address
There was a problem hiding this comment.
The issue is that all those are good in mobile but we maintain the source of truth in mobile, this allow to minimize the diff when we migrate to core fully (currently we want to publish an intiial version of the perps controller for extension)
| // HIP-3 assets use format: hip3:dex_SYMBOL.svg (e.g., hip3:xyz_AAPL.svg) | ||
| // Regular assets use format: SYMBOL.svg (e.g., BTC.svg) | ||
| export const METAMASK_PERPS_ICONS_BASE_URL = | ||
| 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/icons/eip155:999/'; |
There was a problem hiding this comment.
Accessing files in production using this API is not allowed by GitHub
| // Type guard - this function only supports 'transfer' type | ||
| // The type parameter is kept for API compatibility with mobile's version | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| const _type: 'transfer' = type; |
There was a problem hiding this comment.
I don't follow why this is required, given the type of type
| const currentDepositId = generateDepositId(); | ||
|
|
||
| // Get deposit routes from provider | ||
| const depositRoutes = provider.getDepositRoutes({ isTestnet: false }); |
There was a problem hiding this comment.
Is this never supported in testnet mode?
| } | ||
| } | ||
| } | ||
| } No newline at end of file |
| debug(): { timestamp: string; source: string; message: string } { | ||
| const debugInfo = { | ||
| timestamp: new Date().toISOString(), | ||
| source: '@metamask/perps-controller', | ||
| message: 'DEBUG v5 - Hello from Core PerpsController!', | ||
| }; | ||
|
|
||
| this.debugLog('[PerpsController.debug]', debugInfo); | ||
|
|
||
| return debugInfo; | ||
| } |
There was a problem hiding this comment.
Is this meant to be included?
Mrtenz
left a comment
There was a problem hiding this comment.
Only got partially through reviewing this PR (due to its massive size), but here are some comments to look into. In addition:
- Look into using the messenger for communication between controllers and services.
- Look into reducing duplication. Extract types, utils, etc. into separate files, and reuse them between controllers and services.
| export function createMockOrderResult(): { | ||
| success: boolean; | ||
| orderId: string; | ||
| filledSize: string; | ||
| averagePrice: string; | ||
| } { |
| const errorObj = | ||
| caughtError instanceof Error | ||
| ? caughtError | ||
| : new Error(String(caughtError)); |
| /** Optional logger for error reporting (e.g., Sentry) */ | ||
| logger?: PerpsLogger; |
| caughtError instanceof Error | ||
| ? caughtError | ||
| : new Error(String(caughtError)); |
| caughtError instanceof Error | ||
| ? caughtError | ||
| : new Error(String(caughtError)); |
| const handleCandleEvent = (candleEvent: { | ||
| t: number; | ||
| o: string; | ||
| h: string; | ||
| l: string; | ||
| c: string; | ||
| v: string; | ||
| }): void => { |
| const intervalMap: Record<string, number> = { | ||
| '1m': 1 * 60 * 1000, | ||
| '3m': 3 * 60 * 1000, | ||
| '5m': 5 * 60 * 1000, | ||
| '15m': 15 * 60 * 1000, | ||
| '30m': 30 * 60 * 1000, | ||
| '1h': 60 * 60 * 1000, | ||
| '2h': 2 * 60 * 60 * 1000, | ||
| '4h': 4 * 60 * 60 * 1000, | ||
| '8h': 8 * 60 * 60 * 1000, | ||
| '12h': 12 * 60 * 60 * 1000, | ||
| '1d': 24 * 60 * 60 * 1000, | ||
| '3d': 3 * 24 * 60 * 60 * 1000, | ||
| '1w': 7 * 24 * 60 * 60 * 1000, | ||
| '1M': 30 * 24 * 60 * 60 * 1000, // Approximate | ||
| }; | ||
|
|
||
| return intervalMap[interval] ?? 60 * 60 * 1000; // Default to 1 hour |
| * @param error - The caught error (could be Error, string, or unknown) | ||
| * @returns A proper Error instance | ||
| */ | ||
| function ensureError(error: unknown): Error { |
| * @param options.isTestnet - Whether to use testnet mode | ||
| */ | ||
| constructor( | ||
| deps: PerpsPlatformDependencies, |
| * @param options.isTestnet - Whether to use testnet mode | ||
| */ | ||
| constructor( | ||
| deps: PerpsPlatformDependencies, |
There was a problem hiding this comment.
It looks like this file is describing the PR itself, the audience being the author of this PR. Was this added by mistake?
|
Thanks @FrederikBolding, @Mrtenz, @Gudahtt - really appreciate the thorough feedback. I should have first written a comment providing context for this PR before asking for reviews. Let me provide that context now. ContextThis PR was a rough first version to get something usable for extension integration. The goal was to have working code that could unblock extension development while we continue active development in mobile. A few things to note:
Commitment to Messenger PatternGood point about using the messenger API for controller communication. Interestingly, I originally had the messenger pattern implemented, but moved to dependency injection thinking it would be more generic/portable. I'll re-implement the messenger pattern accordingly. My plan: I'll refactor to use the messenger pattern in mobile first (since mobile is the source of truth), then bring those changes back to Core. This ensures we don't diverge and maintains a single source of truth. Mobile as Source of TruthThere's very active development happening in mobile right now (MYX integration, Hyperliquid improvements, provider aggregation). The current approach keeps mobile as the source of truth to minimize merge conflicts and divergence. I'm using yalc as a temporary workaround to unblock extension, but proper Core integration is definitely preferred. I recognize this isn't a perfect process, but I'm optimizing for outcome over process here. Keeping mobile as the source of truth allows us to move faster, which translates directly to additional revenue - a win for everyone. I'm committed to cleaning things up as we converge on the proper Core integration. Guidance RequestGiven the PR's size and the ongoing mobile development, I'd appreciate guidance on: Which items are blocking vs can be addressed in follow-up PRs? Here's how I'm thinking about prioritization: P0 - Will address now (blocking)
P1 - Follow-up PR
P2 - Can also address (via mobile-first workflow)
All P1/P2 items will follow the same workflow: fix in mobile first, then sync back to Core. Agreement NeededCan we align on this mobile-first process? I'll track all feedback, address items in mobile, then bring changes back to Core. This keeps a single source of truth while still addressing all the valid concerns raised. GitHub Raw API Note@FrederikBolding - regarding the GitHub raw file API concern: this has been discussed and accepted within our team as an intentional tradeoff for the fallback mechanism. Does this prioritization make sense? Are there items I've categorized as P1/P2 that you consider blocking? I want to make sure we're aligned on what's needed to move forward. |
|
@abretonc7s Sorry for the late reply here. Usually when our team is asked to review a PR, we like to be thorough. We want to make sure things are being done in a sensible way and that they follow the standards we've set. We're certainly happy that you reached out. However, this seems like an different situation, since the controller is already developed. So it doesn't seem that a review is what you're looking for. If I understand correctly, you want to make sure that engineers have all of the same functionality at the controller level they can use to develop Perps on extension? If this is true, then if speed is the motivation, you are certainly free to approve the PR and merge it (you are codeowners after all). That said, I feel compelled to provide some advice based on your last comment:
Anyway, I know that @gambinish has also published a preview build for these changes in #7841, and there was some talk I overheard about perhaps using this in production. I also want to advise that preview builds are really for testing, and we should treat them as ephemeral. So from a best practices perspective, once we want to release Perps on Extension, I think it would be better to cut a "production" release of |
Explanation
This PR migrates the complete
PerpsControllerimplementation from MetaMask Mobile to the Core monorepo as@metamask/perps-controller.Background
The Perps (Perpetual Futures) feature was initially developed in MetaMask Mobile. To enable sharing this functionality across platforms (Mobile, Extension) and maintain a single source of truth, we are migrating the controller logic to the Core monorepo.
What's Included
Core Controller (~3,000 lines)
PerpsController.ts- Main controller extendingBaseControllerwith full trading functionalityselectors.ts- 8 state selectors for UI integrationTypes & Constants
types/- Comprehensive TypeScript type definitions for all Perps operationsconstants/- Configuration (perpsConfig, hyperLiquidConfig, errorCodes, eventNames)Services (8 modules)
TradingService- Order placement, cancellation, position managementMarketDataService- Market info, prices, order bookAccountService- Account state managementDepositService- Deposit flow handlingEligibilityService- User eligibility checksFeatureFlagConfigurationService- Remote feature flag integrationRewardsIntegrationService- MetaMask rewards integrationDataLakeService- Analytics data collectionProviders
HyperLiquidProvider- Full HyperLiquid protocol integration (~7,000 lines)AggregatedPerpsProvider- Multi-provider aggregation layerProviderRouter- Routing logic for multi-provider supportPlatform Services
HyperLiquidClientService- HTTP client for HyperLiquid APIHyperLiquidSubscriptionService- WebSocket subscription managementHyperLiquidWalletService- Wallet signing operationsUtilities (18 modules)
Architecture
The package uses dependency injection via
PerpsPlatformDependenciesinterface to remain platform-agnostic:This allows Mobile and Extension to provide their own implementations of these dependencies while sharing the core business logic.
ESLint Compliance
Most files pass Core ESLint rules. The following files have ESLint rules temporarily disabled due to the volume of code migrated:
PerpsController.tsHyperLiquidProvider.tsHyperLiquidSubscriptionService.tsThese suppressions allow the migration to proceed while maintaining functionality. They can be addressed incrementally in follow-up PRs.
Test Coverage
@nktkas/hyperliquidSDK (ESM module)serviceMocks.ts)providerMocks.ts)Coverage thresholds are currently set to 0% to allow incremental test migration. Full test coverage will be added in follow-up PRs.
References
Checklist
Note: This is a new package, so there are no breaking changes for existing consumers.
Note
High Risk
High risk due to introducing a large new controller that orchestrates trading, signing, and transaction submission via injected platform controllers, plus new external dependencies and reduced coverage enforcement during migration.
Overview
Adds the migrated, full-featured
PerpsControllerto@metamask/perps-controller, including a rich persisted state model, messenger actions/events, dependency-injected platform infrastructure, provider initialization/reinit logic (testnet toggle, aggregated routing), and delegation to new services for trading, market data, deposits/withdrawals, eligibility, and analytics.Updates package wiring for consumption and testing: re-exports selectors/types/constants from
src/index.ts, introduces HyperLiquid SDK/Jest ESM mocking (moduleNameMapper+src/__mocks__/hyperliquidMock.tsand shared test mocks), adds new runtime/peer dependencies, and temporarily drops Jest coverage thresholds to 0 during the migration (with new migration guide + changelog entry).Written by Cursor Bugbot for commit b40b1fc. This will update automatically on new commits. Configure here.