feat(perps-controller): make package safe for extension consumers (re-do of #8374)#8398
Merged
abretonc7s merged 8 commits intomainfrom Apr 8, 2026
Merged
feat(perps-controller): make package safe for extension consumers (re-do of #8374)#8398abretonc7s merged 8 commits intomainfrom
abretonc7s merged 8 commits intomainfrom
Conversation
Routine mobile→core sync via scripts/perps/validate-core-sync.sh from metamask-mobile branch feat/tat-2863-sync-controller-code-extension. Highlights (full diff generated by rsync from mobile source-of-truth): - Add /* webpackIgnore: true */ to the MYXProvider dynamic import so extension webpack builds skip static resolution of the unshipped module. Mobile Metro bundler ignores the magic comment. - Remove 8 MYX adapter functions from the public package barrel and drop the ./myxAdapter re-export from utils/index.ts. MYXProvider continues to use them via relative import. Prevents @myx-trade/sdk from entering the static webpack graph (addresses LavaMoat policy violation when consumed from the extension). - Declare MetaMetricsController:trackEvent in PerpsControllerAllowed Actions so host-app messengers (extension, mobile) that register a shared MetaMetrics handler assign cleanly to PerpsControllerMessenger without requiring a type cast. The action type is declared locally because MetaMetricsController is host-app specific. The controller itself does not call this action. - Move perpsConnectionAttemptContext into packages/perps-controller/src /utils/ (previously imported from a mobile-external path). Required to make the package self-contained for core builds. - Accumulated drift since the previous sync: withdrawal-timestamp state fields (lastCompletedWithdrawalTimestamp / lastCompletedWithdrawalTxHashes), HyperLiquid provider and service improvements, TradingService updates, and related action-type re-exports. Package metadata changes (move @myx-trade/sdk to optionalDependencies, CHANGELOG entry) are committed separately in a follow-up commit because they live outside the synced src/ tree.
Companion to the synced src/ changes in the previous commit. These files live outside the rsync path and must be maintained manually on the core side. - Move @myx-trade/sdk from dependencies to optionalDependencies in packages/perps-controller/package.json so consumers (extension, mobile) do not install it automatically. Combined with the public barrel removal in the previous commit, this prevents @myx-trade/sdk from entering the consumer's static webpack/metro import graph. MYXProvider continues to load it via dynamic import() when MM_PERPS_MYX_PROVIDER_ENABLED=true. - Regenerate yarn.lock so the @metamask/perps-controller workspace entry declares dependenciesMeta["@myx-trade/sdk"].optional = true. - Document the Unreleased changes in CHANGELOG.md (Changed and BREAKING Removed sections covering the public export removal, the webpackIgnore magic comment, the MetaMetrics action allow-list entry, and the dependency relocation). This is the core equivalent of #8374, re-created on top of the latest mobile sync.
Resolves conflicts after pulling in #8333 (competing perps sync) and 5 releases. Re-ran scripts/perps/validate-core-sync.sh after the merge so packages/perps-controller/src/ matches mobile @ a697bc90 — the source of truth — overwriting any auto-merged garbage from the two overlapping mobile snapshots. Also reverts the local declaration of MetaMetricsController:trackEvent in PerpsControllerAllowedActions: it broke mobile typecheck because mobile has no MetaMetricsController (uses MetaMetrics singleton) and @metamask/messenger's parent type narrows to never when the child declares actions the parent doesn't have. Extension keeps its existing cast workaround.
7 tasks
- Move Removed section before Fixed in CHANGELOG (Keep-a-Changelog canonical order: Added, Changed, Deprecated, Removed, Fixed, Security). - Add [#8398] PR links to the Changed and Removed entries so the monorepo changelog bot associates them with this PR. - Run prettier --write on eslint-suppressions.json to fix formatting drift introduced by the sync script's eslint auto-suppress step.
Output of `scripts/perps/validate-core-sync.sh` run from mobile branch `feat/tat-2863-sync-controller-code-extension` after the mobile fix to align mobile's `.eslintrc.js` perps-controller override with core's `@metamask/eslint-config` base rules. Mobile changes that produced this diff: 1. Added `BinaryExpression[operator='in']` (+ `WithStatement` and `SequenceExpression`) to `.eslintrc.js`'s perps-controller override so mobile lint mirrors what core enforces. This closes the gap where `'x' in y` violations passed mobile lint silently and then landed in core as new `no-restricted-syntax` suppressions at sync time. 2. Replaced 24 `'x' in y` uses with `hasProperty(y, 'x')` from `@metamask/utils` across 8 source files. Four cases were kept as `in` behind `/* eslint-disable no-restricted-syntax */` because `hasProperty` narrows the KEY but not the discriminated union branch — losing access to `.filled`, `.resting`, `.oid`, `.totalSz` on the HyperLiquid SDK types and losing `keyof typeof HYPERLIQUID_ASSET_CONFIGS` narrowing. 3. Hardened `scripts/perps/validate-core-sync.sh` step 6 with a per-file/per-rule suppression delta check that hard-fails the sync if any (file, rule) pair's suppression count INCREASED compared to the baseline. This is the canonical local detection point for "a new lint violation is about to land in core." Net effect on core: perps-controller suppression count drops from 30 to 6. `eslint-suppressions.json` loses 45 lines. No runtime change. Files touched by the sync: - `eslint-suppressions.json` — 24 entries removed - `packages/perps-controller/.sync-state.json` — new commit - `packages/perps-controller/src/providers/HyperLiquidProvider.ts` - `packages/perps-controller/src/services/HyperLiquidSubscriptionService.ts` - `packages/perps-controller/src/services/TradingService.ts` - `packages/perps-controller/src/types/index.ts` - `packages/perps-controller/src/types/transactionTypes.ts` - `packages/perps-controller/src/utils/errorUtils.ts` - `packages/perps-controller/src/utils/hyperLiquidAdapter.ts` - `packages/perps-controller/src/utils/hyperLiquidValidation.ts` - `packages/perps-controller/src/utils/marketDataTransform.ts`
Sync of mobile commit ec93364a98 (PR MetaMask/metamask-mobile#28509). Bugbot caught that `hasProperty(provider, 'closePositions')` from `@metamask/utils` is `Object.hasOwnProperty.call(...)` under the hood, which only checks own properties and never traverses the prototype chain. Since `provider` is a class instance and `closePositions` lives on the class prototype, the diagnostic log was always reporting `hasBatchMethod: false` even when the provider supports batch close. The actual feature gate at line 1525 (`if (provider.closePositions)`) still works because normal property access traverses prototypes, so runtime behavior is unchanged. Simplified by dropping the bogus `hasBatchMethod` field from the debug log entirely instead of working around it with `'in'` + an eslint disable comment.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 37fe686. Configure here.
The merge commit d85d463 (origin/main → feat/perps/another-sync) dropped main's new suppressions for recently-added custom rules (deprecated :stateChange, constructor messenger actions, restricted 'in' operator), causing CI lint to fail with ~550 pre-existing errors across unrelated packages. This restores eslint-suppressions.json from origin/main and re-runs the sync script's prune step, which cleans up the perps-controller entries we've already fixed. Net: main's suppressions for other packages are back, and perps-controller's reduced suppressions are preserved.
github-merge-queue bot
pushed a commit
to MetaMask/metamask-mobile
that referenced
this pull request
Apr 8, 2026
## **Description** Makes `@metamask/perps-controller` safe to consume from the MetaMask extension without pulling `@myx-trade/sdk` into the static webpack bundle or violating the LavaMoat policy. Mobile owns the source-of-truth for the perps controller package via `scripts/perps/validate-core-sync.sh`, so the fix is made in mobile and then rsync'd to core on a matching branch. Companion core PR: MetaMask/core#8398 — supersedes MetaMask/core#8374. ### What changed 1. **`app/controllers/perps/PerpsController.ts`** — add `/* webpackIgnore: true */` magic comment to the `MYXProvider` dynamic `import()` so webpack (extension) skips static resolution of the intentionally-unshipped module. Metro (mobile) ignores the magic comment, so runtime behavior on mobile is unchanged. 2. **`app/controllers/perps/index.ts`** — remove 8 MYX adapter functions from the public barrel (`adaptMarketFromMYX`, `adaptPriceFromMYX`, `adaptMarketDataFromMYX`, `filterMYXExclusiveMarkets`, `isOverlappingMarket`, `buildPoolSymbolMap`, `buildSymbolPoolsMap`, `extractSymbolFromPoolId`). These are still used internally by `MYXProvider`, which imports them via relative path `../utils/myxAdapter`. No runtime change in mobile — the functions are still available to MYXProvider. 3. **`app/controllers/perps/utils/index.ts`** — drop the `export * from './myxAdapter'` barrel re-export so the utils index no longer transitively re-exports MYX symbols. 4. **Drift fix — `perpsConnectionAttemptContext`** — moved from `app/util/perpsConnectionAttemptContext.ts` into `app/controllers/perps/utils/perpsConnectionAttemptContext.ts` and exported from `@metamask/perps-controller` so: - `HyperLiquidClientService` (inside the perps package) imports it via a relative path that stays inside the synced directory — required for the core sync to build. - `PerpsConnectionManager` (outside the perps package) imports it from `@metamask/perps-controller` — satisfies the `no-restricted-imports` rule for code outside `app/controllers/perps/`. 5. **`.eslintrc.js` — align mobile perps override with core's base rules.** Added `BinaryExpression[operator='in']`, `WithStatement`, and `SequenceExpression` selectors to the existing `no-restricted-syntax` rule in the `app/controllers/perps/**` override. Mobile's override was missing these, which meant `'x' in y` type-guards passed mobile lint silently and then landed in core as new `no-restricted-syntax` suppressions at sync time. The override now mirrors what `@metamask/eslint-config` enforces in core, catching the violations at source. 6. **Replace `'x' in y` with `hasProperty(y, 'x')` across 8 perps-controller files** — `HyperLiquidProvider.ts`, `HyperLiquidSubscriptionService.ts`, `TradingService.ts`, `marketDataTransform.ts`, `hyperLiquidAdapter.ts`, `types/index.ts`, `types/transactionTypes.ts`, `utils/errorUtils.ts`. Uses `hasProperty` from `@metamask/utils` (the idiomatic replacement documented in core's eslint config). 24 of the 28 occurrences were converted cleanly; the remaining 4 are kept as `'in'` behind `/* eslint-disable no-restricted-syntax */` blocks because `hasProperty` narrows the KEY but not the discriminated-union branch — losing access to `.filled`, `.resting`, `.oid`, `.totalSz` on the HyperLiquid SDK types and losing `keyof typeof HYPERLIQUID_ASSET_CONFIGS` narrowing. Each disable comment is documented with the narrowing rationale. 7. **`scripts/perps/validate-core-sync.sh` — hardened preflight + per-file suppression delta check.** - **Preflight freshness check** (`chore(perps-sync): hard-fail sync script when core main has drift`): the previous preflight only compared the core working tree against `.sync-state.json.lastSyncedCoreCommit`, so it could not detect that someone else had merged a competing perps-controller change into `origin/main` while this branch was in review. That gap let [#8398](MetaMask/core#8398) land in a conflicting state with [#8333](MetaMask/core#8333). The new check at the top of `step_conflict_check` fetches `origin/main`, runs `git rev-list --count HEAD..origin/main -- packages/perps-controller/`, and **hard-fails** (not warns) with the offending commit list + a merge suggestion if any such commits exist. Gracefully degrades to `WARN` if offline or if `origin/main` is missing. - **Per-file/per-rule suppression delta check** (`chore(perps-sync): hard-fail on per-file suppression delta increase`): step 6 now snapshots the per-file/per-rule suppression counts from `eslint-suppressions.json` BEFORE running `--fix` / `--suppress-all` / `--prune-suppressions`, then diffs against the post-run counts and hard-fails if any (file, rule) pair's count INCREASED. Reducing counts (mobile fixes removing previously-suppressed violations) is always allowed. Increases mean the current sync is introducing NEW violations that would land in core as fresh suppressions and must be fixed at source. The script prints every offending file+rule pair and points at `hasProperty()` from `@metamask/utils` as the canonical replacement. Replaces the old "WARN if count > 20" heuristic. This is the canonical local detection point for the problem that *"it should have been detected locally!"* — a violation now fails the sync script at step 6 BEFORE the core PR is opened. ### Not included (intentional) An earlier draft of this PR also declared `MetaMetricsController:trackEvent` in `PerpsControllerAllowedActions` to let the extension drop a type cast. That change was **reverted** because mobile has no `MetaMetricsController` — it uses a `MetaMetrics` singleton (`app/core/Analytics/MetaMetrics.ts`) — so adding the action to the allowed-actions union forced `@metamask/messenger`'s parent type to narrow to `never` and broke `app/core/Engine/messengers/perps-controller-messenger/index.ts`. The extension keeps its existing `as unknown as PackagePerpsControllerMessenger` cast workaround until both host apps share a real `MetaMetricsController`. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: [TAT-2863](https://consensyssoftware.atlassian.net/browse/TAT-2863) Related: MetaMask/core#8374 (superseded by companion core PR #8398) ## **Manual testing steps** ```gherkin Feature: Perps controller behavior unchanged after MYX export cleanup Scenario: User opens the Perps tab on testnet Given the wallet is unlocked and funded on HyperLiquid testnet When user navigates to the Perps tab Then the markets list loads And the HyperLiquid provider is selected by default Scenario: User opens and closes a BTC long position Given the wallet has testnet funds When user opens a $11 BTC long market order Then a position appears in the positions list When user closes the position at 100% Then the position is removed from the positions list ``` Automated verification (all passing locally): - `yarn lint` on all touched files — clean - `NODE_OPTIONS='--max-old-space-size=8192' npx tsc --noEmit` — clean (full, no incremental cache) - `yarn jest app/controllers/perps/PerpsController.test.ts app/controllers/perps/services/HyperLiquidSubscriptionService.test.ts app/controllers/perps/services/TradingService.test.ts` — 412 passing - `bash scripts/perps/validate-core-sync.sh --core-path …/core` — all 9 steps PASS. Suppression count drops from 30 → **6** after the lint cleanup. ## **Screenshots/Recordings** N/A — this PR is a bundler / import-graph + lint cleanup with zero UI impact. Mobile runtime behavior is unchanged because: - The `webpackIgnore` magic comment is extension/webpack-only; Metro ignores it. - The 8 removed exports are still used internally by `MYXProvider` via relative import. - `perpsConnectionAttemptContext` keeps its module-level state, just at a new file path. - `hasProperty(obj, key)` is runtime-equivalent to `key in obj` for plain objects (both delegate to `Object.prototype.hasOwnProperty` semantics via the prototype chain). ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable (existing tests untouched; new location for moved test file is covered by the same suite) - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it changes perps controller module boundaries/exports and touches trading/provider code paths (type-guard refactors and a few scoped lint suppressions), plus alters sync script behavior that can block releases if misconfigured. > > **Overview** > Improves extension-safe consumption of `@metamask/perps-controller` by ensuring the optional `MYXProvider` stays excluded from webpack’s static bundle and by removing MYX adapter re-exports from the public perps barrels. > > Aligns mobile’s perps linting with core by restricting the `in` operator (and a couple of other syntaxes) and refactors multiple perps files to use `hasProperty()` instead of `'key' in obj`, keeping a few documented `in` usages behind targeted `no-restricted-syntax` disables where TypeScript narrowing is required. > > Moves/exports `perpsConnectionAttemptContext` under the perps utils surface so in-package code imports remain sync-safe and out-of-package callers use the package export, and hardens `scripts/perps/validate-core-sync.sh` with an `origin/main` freshness check plus a per-file/per-rule eslint suppression delta hard-fail (including prettier formatting of `eslint-suppressions.json`). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 1f90a9b. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
michalconsensys
approved these changes
Apr 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Explanation
@metamask/perps-controllercurrently cannot be consumed from the MetaMask extension because:@myx-trade/sdkinto the static import graph even thoughMYXProvideris loaded via dynamicimport().import('./providers/MYXProvider')at build time, which breaks becauseMYXProvideris intentionally not published (files: [..., "!dist/providers/MYXProvider*"]).@myx-trade/sdkis independencies, so extensionyarn installstill pulls it even though it is unused outside the dynamic path.Solution
This PR is the functional equivalent of #8374, re-created on top of the latest mobile→core sync. The mobile repo owns the source-of-truth for
packages/perps-controller/src/**/*.tsviascripts/perps/validate-core-sync.sh, so thesrc/changes arrive here via the sync script run from mobile branchfeat/tat-2863-sync-controller-code-extension. The package metadata (package.json,CHANGELOG.md,yarn.lock) has to be maintained manually on the core side because it lives outside the rsync path.Commit layout
Commit 1 (
feat(perps): sync controller code from mobile) — initial sync output. The three core fixes (webpackIgnoremagic comment, MYX barrel removal,perpsConnectionAttemptContextrelocation) plus accumulated mobile drift from the previous sync baseline (withdrawal-timestamp state fields, HyperLiquid service improvements, etc.).Commit 2 (
feat(perps): move @myx-trade/sdk to optionalDependencies) — package metadata only:@myx-trade/sdkfromdependenciestooptionalDependenciesso extension/mobile consumers don't install it automatically.yarn.lockwithdependenciesMeta["@myx-trade/sdk"].optional = trueon the workspace entry.CHANGELOG.md(Changed + BREAKING Removed sections).Commit 3 (
Merge origin/main into feat/perps/another-sync) — merge commit that:mainwhile this PR was in review. Conflict resolution: take mobile as source of truth onsrc/(re-ran the sync script after merge to guaranteepackages/perps-controller/src/matches mobileHEADexactly), merged CHANGELOG entries, keptoptionalDependenciesblock.MetaMetricsController:trackEventtoPerpsControllerAllowedActions. An earlier draft added this to let the extension drop a type cast, but mobile has noMetaMetricsController(uses aMetaMetricssingleton), so@metamask/messenger's parent type narrowed toneverand broke mobile'sperps-controller-messengersetup. Extension keeps its existing cast workaround until both host apps share a realMetaMetricsController.Commit 4 (
chore(perps-controller): fix CI — changelog order + prettier) — small CI follow-up.Commit 5 (
chore(perps-controller): sync lint cleanup from mobile) — output of running the hardened mobile sync script after a corresponding mobile fix to align mobile's.eslintrc.jsperps-controller override with core's@metamask/eslint-configbase rules. Mobile changes that produced this commit:BinaryExpression[operator='in'](+WithStatementandSequenceExpression) to the mobile.eslintrc.jsperps-controller override so mobile lint mirrors what core enforces. This closes the gap where'x' in yviolations passed mobile lint silently and then landed in core as newno-restricted-syntaxsuppressions at sync time.'x' in yuses withhasProperty(y, 'x')from@metamask/utilsacross 8 source files. Four cases were kept as'in'behind/* eslint-disable no-restricted-syntax */becausehasPropertynarrows the KEY but not the discriminated-union branch — losing access to.filled,.resting,.oid,.totalSzon the HyperLiquid SDK types and losingkeyof typeof HYPERLIQUID_ASSET_CONFIGSnarrowing.scripts/perps/validate-core-sync.shstep 6 with a per-file/per-rule suppression delta check that hard-fails the sync if any (file, rule) pair's suppression count INCREASED compared to the baseline. This is the canonical local detection point for "a new lint violation is about to land in core."Net effect on this PR: perps-controller suppression count drops from 30 → 6.
eslint-suppressions.jsonloses 45 entries. No runtime change.Splitting the commits this way makes it easy for a reviewer to see the "PR 8374 intent" (commits 1–2) isolated from the merge (commit 3), CI fix (commit 4), and the lint cleanup (commit 5).
Verification
yarn install— clean (only the expected optional-dep warning for@myx-trade/sdk).yarn workspace @metamask/perps-controller build— PASS.yarn workspace @metamask/perps-controller changelog:validate— PASS.tsc --noEmitwith no incremental cache): PASS — validates that the package type changes round-trip cleanly into mobile'sRootExtendedMessenger.PerpsController.test.ts,HyperLiquidSubscriptionService.test.ts,TradingService.test.ts): 412 passing, 4 skipped.References
hasPropertymigration that produced commit 5)controllerMessenger as PackagePerpsControllerMessengercast remains until MetaMetricsController becomes a shared concept)Checklist
Note
Medium Risk
Medium risk because it introduces a breaking change to public exports and adjusts dynamic import/bundling behavior, plus modifies WebSocket/REST candle subscription lifecycles which could affect chart/market data reliability under rapid navigation.
Overview
Makes
@metamask/perps-controllersafer for extension consumers by moving@myx-trade/sdktooptionalDependencies, removing MYX adapter utilities from the public barrel (breaking), and adding/* webpackIgnore: true */to the dynamicMYXProviderimport to avoid webpack static resolution.Hardens HyperLiquid connectivity and live-data flows: adds
perpsConnectionAttemptContextto optionally suppress noisy startup errors, adds candle subscription debounce andAbortControllercancellation for in-flight REST candle fetches, and prevents WS subscription leaks/races by tracking pending subscription promises.Includes a broad lint cleanup replacing many
'x' in ychecks withhasProperty(keeping a fewinchecks with explicit eslint disables for type narrowing), and updates the changelog, lockfile, and sync-state metadata accordingly.Reviewed by Cursor Bugbot for commit 5ec006a. Bugbot is set up for automated code reviews on this repo. Configure here.