Skip to content

feat(perps): sync controller code with extension#28509

Merged
abretonc7s merged 7 commits intomainfrom
feat/tat-2863-sync-controller-code-extension
Apr 8, 2026
Merged

feat(perps): sync controller code with extension#28509
abretonc7s merged 7 commits intomainfrom
feat/tat-2863-sync-controller-code-extension

Conversation

@abretonc7s
Copy link
Copy Markdown
Contributor

@abretonc7s abretonc7s commented 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 filesHyperLiquidProvider.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 land in a conflicting state with #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
Related: MetaMask/core#8374 (superseded by companion core PR #8398)

Manual testing steps

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

  • I've followed MetaMask Contributor Docs and MetaMask Mobile Coding Standards.
  • I've completed the PR template to the best of my ability
  • I've included tests if applicable (existing tests untouched; new location for moved test file is covered by the same suite)
  • I've documented my code using JSDoc format if applicable
  • I've applied the right labels on the PR (see labeling guidelines). 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.

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).

Reviewed by Cursor Bugbot for commit 1f90a9b. Bugbot is set up for automated code reviews on this repo. Configure here.

@abretonc7s abretonc7s added DO-NOT-MERGE Pull requests that should not be merged agentic labels Apr 8, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

Two changes enable @metamask/perps-controller to be consumed from
the extension without pulling @myx-trade/sdk into the static webpack
bundle or violating the LavaMoat policy:

1. Add /* webpackIgnore: true */ magic comment to the MYXProvider
   dynamic import so webpack skips static resolution of the
   intentionally-unshipped module. Runtime behavior on mobile's Metro
   bundler is unchanged — the magic comment is extension/webpack-only.

2. Remove the 8 MYX adapter functions (adaptMarketFromMYX,
   adaptPriceFromMYX, adaptMarketDataFromMYX, filterMYXExclusiveMarkets,
   isOverlappingMarket, buildPoolSymbolMap, buildSymbolPoolsMap,
   extractSymbolFromPoolId) from the public barrel and drop the
   ./myxAdapter re-export from utils/index.ts. These functions are
   still used internally by MYXProvider, which imports them via
   relative path (../utils/myxAdapter) — no runtime change.

Drift cleanup (required to unblock the core sync):

3. Move app/util/perpsConnectionAttemptContext.ts into
   app/controllers/perps/utils/ so HyperLiquidClientService can import
   it via a relative path that stays inside the synced perps package.
   The mobile-external consumer (PerpsConnectionManager) now imports
   withPerpsConnectionAttemptContext from @metamask/perps-controller
   instead of a deep relative path, satisfying the no-restricted-imports
   rule for code outside app/controllers/perps/.

Note: an earlier draft of this commit also declared
MetaMetricsController:trackEvent in PerpsControllerAllowedActions to
let the extension drop a type cast. That was reverted because mobile
has no MetaMetricsController (uses MetaMetrics singleton), so adding
it forced @metamask/messenger's parent type to narrow to never and
broke perps-controller-messenger/index.ts. Extension keeps its
existing cast workaround until both host apps share a real
MetaMetricsController.
@abretonc7s abretonc7s force-pushed the feat/tat-2863-sync-controller-code-extension branch from 2c400fa to 8b18a2b Compare April 8, 2026 09:13
@abretonc7s abretonc7s removed the DO-NOT-MERGE Pull requests that should not be merged label Apr 8, 2026
@abretonc7s abretonc7s marked this pull request as ready for review April 8, 2026 09:23
@abretonc7s abretonc7s requested a review from a team as a code owner April 8, 2026 09:23
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 8, 2026

Two fixes, both uncovered while getting core PR #8398 green:

1. Preflight freshness check (the main fix)

   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 the current branch was in review. That gap let
   #8398 land in a conflicting state with #8333.

   New check at the top of step_conflict_check:
   - Fetches origin/main quietly.
   - Runs `git rev-list --count HEAD..origin/main -- packages/perps-controller/`.
   - Hard-fails (not warns) with the offending commit list + a merge
     suggestion if any such commits exist.
   - Gracefully degrades to a WARN if offline or if origin/main is missing.

   This would have caught the #8333 collision before any file was
   copied, forcing a merge-then-sync workflow instead of a
   merge-after-sync cleanup.

2. Prettier pass on eslint-suppressions.json

   Core's lint:misc:check runs prettier on eslint-suppressions.json,
   but `yarn eslint --suppress-all` writes the file in a format
   that prettier disagrees with (trailing newline, key quoting).
   This caused PR #8398's lint:misc job to fail on a file that
   our own sync script had just written.

   Added a `yarn prettier --write eslint-suppressions.json` step
   at the end of the ESLint auto-fix stage so the sync output
   stays lint-clean.
Adds `BinaryExpression[operator='in']` (along with `WithStatement` and
`SequenceExpression`) to the mobile `.eslintrc.js` perps-controller
override so it mirrors core's `@metamask/eslint-config` base rules.
Without this, `'x' in y` type-guards in the perps package passed mobile
lint but landed in core as `no-restricted-syntax` suppressions, and the
extension team then saw them as hard errors in their editor.

Replaces 24 simple existence-check uses of `'x' in obj` with
`hasProperty(obj, 'x')` from `@metamask/utils` across:

- providers/HyperLiquidProvider.ts (9 replacements)
- services/HyperLiquidSubscriptionService.ts (4)
- services/TradingService.ts (1)
- types/index.ts (2)
- types/transactionTypes.ts (4)
- utils/errorUtils.ts (1)
- utils/hyperLiquidAdapter.ts (2)
- utils/marketDataTransform.ts (1)

Four uses where `in` provides discriminated-union narrowing that
`hasProperty` does not (the HyperLiquid SDK status-union branches in
`HyperLiquidProvider.ts` and the `HYPERLIQUID_ASSET_CONFIGS` keyof
narrowing in `hyperLiquidValidation.ts`) are kept with inline
`/* eslint-disable no-restricted-syntax */` comments and explanatory
notes.

Net effect in core: perps-controller suppression count drops from 30
to 6 on the next sync. All four remaining suppressions for
`no-restricted-syntax` are the intentional inline-disabled
discriminated-union narrowings above.
Extend `scripts/perps/validate-core-sync.sh` step 6 to snapshot
per-file/per-rule suppression counts from `eslint-suppressions.json`
BEFORE running `--fix` / `--suppress-all` / `--prune-suppressions`,
then diff against the post-run counts and hard-fail if any
(file, rule) pair's count increased.

Reducing counts (a mobile fix that removes a previously-suppressed
violation) is always allowed. Increases mean the current sync is
introducing NEW violations that would land in core as fresh
suppressions — e.g. a new `'x' in y` use, a new `@typescript-eslint/*`
violation. Those must be fixed at source in mobile before the sync
can proceed.

Replaces the old "WARN if count > 20" heuristic with a precise,
actionable per-entry report that lists every offending file+rule
pair and points at `hasProperty()` from `@metamask/utils` as the
canonical replacement for `'x' in y`.

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.
@abretonc7s abretonc7s force-pushed the feat/tat-2863-sync-controller-code-extension branch from 25763af to a41b258 Compare April 8, 2026 10:30
@github-actions github-actions bot added size-M risk-medium Moderate testing recommended · Possible bug introduction risk and removed size-S labels Apr 8, 2026
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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 a41b258. Configure here.

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 1527 (`if (provider.closePositions)`)
still works because normal property access traverses prototypes, so
runtime behavior is unchanged.

Simplify by dropping the bogus `hasBatchMethod` field from the debug
log entirely instead of working around it with `'in'` + an eslint
disable comment. The remaining `providerKeys` field already gives
enough signal for debugging.
@github-actions github-actions bot added risk-medium Moderate testing recommended · Possible bug introduction risk and removed risk-medium Moderate testing recommended · Possible bug introduction risk labels Apr 8, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokePerps, SmokeWalletPlatform, SmokeConfirmations
  • Selected Performance tags: @PerformancePreps
  • Risk Level: medium
  • AI Confidence: 82%
click to see 🤖 AI reasoning details

E2E Test Selection:
The changes in this PR are primarily code quality/refactoring changes to the Perps controller and related services:

  1. 'x' in objhasProperty(obj, 'x') replacements across HyperLiquidProvider, HyperLiquidSubscriptionService, HyperLiquidClientService, errorUtils, hyperLiquidAdapter, marketDataTransform, transactionTypes, and types/index.ts. These are functionally equivalent for runtime behavior but ensure ESLint compliance. The hasProperty utility from @metamask/utils performs the same check. Risk: low-medium (could theoretically affect type narrowing in edge cases).

  2. perpsConnectionAttemptContext module moved from app/util/perpsConnectionAttemptContext.ts to app/controllers/perps/utils/perpsConnectionAttemptContext.ts. Import paths updated in PerpsConnectionManager.ts and HyperLiquidClientService.ts. This is a file rename/move - if any import was missed, it could break the connection flow.

  3. PerpsController.ts - Added /* webpackIgnore: true */ to MYX provider dynamic import. This is a bundler hint, not a logic change.

  4. index.ts exports - Removed MYX adapter exports, added perpsConnectionAttemptContext exports. This changes the public API of the perps-controller package.

  5. .eslintrc.js - New ESLint rules (no runtime impact).

  6. scripts/perps/validate-core-sync.sh - CI tooling only (no runtime impact).

Tag Selection Rationale:

  • SmokePerps: Directly tests the Perps feature. The refactored code (HyperLiquidProvider, HyperLiquidSubscriptionService, TradingService, PerpsConnectionManager) is core to the Perps trading flow. Must run to verify the refactoring didn't break Add Funds, balance display, or connection flows.
  • SmokeWalletPlatform: Per tag description, Perps is a section inside the Trending tab. Changes to Perps views/controllers affect Trending. Also required when selecting SmokePerps.
  • SmokeConfirmations: Per tag description, when selecting SmokePerps, also select SmokeConfirmations since Add Funds deposits are on-chain transactions.

Not selected:

  • SmokeTrade: The changes don't touch swap/bridge flows directly, and the Perps entry point via TradeWalletActions is not modified.
  • Other tags: No changes to accounts, identity, network, ramps, or other features.

Performance Test Selection:
The changes touch core Perps controller services (HyperLiquidProvider, HyperLiquidSubscriptionService, TradingService, PerpsConnectionManager) which are involved in market data loading, position management, and add funds flows. While the changes are primarily refactoring (hasProperty replacements, import path moves), the PerpsConnectionManager connection flow and HyperLiquidSubscriptionService market data processing are performance-sensitive paths. Running @PerformancePreps validates that the refactoring didn't introduce any performance regressions in the perps market loading, position management, or add funds flow.

View GitHub Actions results

abretonc7s added a commit to MetaMask/core that referenced this pull request Apr 8, 2026
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.
@abretonc7s abretonc7s added skip-sonar-cloud Only used for bypassing sonar cloud when failures are not relevant to the changes. and removed agentic labels Apr 8, 2026
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 8, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
67.2% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

E2E Fixture Validation — Schema is up to date
15 value mismatches detected (expected — fixture represents an existing user).
View details

@abretonc7s abretonc7s added this pull request to the merge queue Apr 8, 2026
Merged via the queue into main with commit 93c5524 Apr 8, 2026
114 of 117 checks passed
@abretonc7s abretonc7s deleted the feat/tat-2863-sync-controller-code-extension branch April 8, 2026 12:07
@github-actions github-actions bot locked and limited conversation to collaborators Apr 8, 2026
@weitingsun weitingsun added release-7.74.0 Issue or pull request that will be included in release 7.74.0 and removed release-7.78.0 labels Apr 8, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-7.74.0 Issue or pull request that will be included in release 7.74.0 risk-medium Moderate testing recommended · Possible bug introduction risk size-M skip-sonar-cloud Only used for bypassing sonar cloud when failures are not relevant to the changes. team-perps Perps team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants