Skip to content

Commit 7c30d1d

Browse files
authored
feat(perps): sync perps controller may 21 2026 (final) (#8871)
## Explanation Final source sync of the perps controller from mobile (`app/controllers/perps/`) into `packages/perps-controller/src/`. After this PR merges, mobile drops its in-repo controller copy and depends on `@metamask/perps-controller` from this repo directly. The core package becomes the source of truth. Mobile changes carried over since the previous sync (mobile commit `35953448`): - `feat(perps)`: add slippage controls for market orders (mobile #30125) - `feat(perps)`: track `vip_tier` / `vip_discount` properties on trading events (mobile #30385) - `feat(perps)`: in-app banner during an ongoing HyperLiquid outage (mobile #30081) - `fix`: prefer the selected EVM account when resolving the trading account (mobile #30253) - `fix(perps)`: suppress `User or API Wallet does not exist` Sentry noise from unfunded wallets (mobile #29972) - `fix(perps)`: approve the HyperLiquid builder fee when missing (mobile #30095) Validation: - `scripts/perps/validate-core-sync.sh` driven from mobile (rsync, ESLint --fix + suppress, oxfmt, build, lint, tests, changelog, sync-state). - `yarn build` at core root produces `packages/perps-controller/dist/PerpsController.{mjs,cjs}` with the `webpackIgnore: true` safeguard intact for the MYX entry that extension consumers exclude via `package.json` `files`. ## References - Mobile follow-up PR (controller removal): coming next on `TAT-3187-perps-controller-removal`. ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes touch order pricing/slippage handling and HyperLiquid trading-readiness flows (migration, builder-fee approval, referral), which can affect order submission behavior and analytics; most changes are additive with guards and fallbacks. > > **Overview** > Adds a persisted, user-configurable **max slippage** preference (`maxSlippageBps`) exposed via new controller actions (`getMaxSlippage`/`setMaxSlippage`), shared bounds (`MAX_SLIPPAGE_BOUNDS`), and order-price calculations updated to use bps (with a temporary deprecated decimal `slippage` fallback). > > Improves account resolution by preferring `AccountsController:getSelectedAccount` (and subscribing to `AccountsController:selectedAccountChange`) so perps state/cache and signing always follow the actively selected EVM account. > > Hardens HyperLiquid setup to reduce noise and unblock trading: introduces a session cache/probe for whether a wallet is registered on HyperLiquid and skips/refuses to Sentry-log the benign "User or API Wallet does not exist" case; builder-fee approval is now retried after prior failures; trading analytics gains `vip_tier`/`vip_discount` properties (including flip-position tracking) plus new event constants (slippage/outage/status). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 558a874. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 0573a8c commit 7c30d1d

24 files changed

Lines changed: 664 additions & 204 deletions
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
2-
"lastSyncedMobileCommit": "35953448cf3c32b2867de8fe0599a356925913ef",
3-
"lastSyncedMobileBranch": "main",
4-
"lastSyncedCoreCommit": "3e549deb97d362c6798a0062dd8b01ac481615c4",
5-
"lastSyncedCoreBranch": "main",
6-
"lastSyncedDate": "2026-05-13T21:35:30Z",
7-
"sourceChecksum": "79a9acc7ad058802b357c6f54774799229b44e9418e802f2f7958e345e16cf59"
2+
"lastSyncedMobileCommit": "cc154d351581605282f5a70f8749565956d42b36",
3+
"lastSyncedMobileBranch": "TAT-3187-perps-controller-removal",
4+
"lastSyncedCoreCommit": "fbe58b4cca248101d12df709c0092cd87f15956f",
5+
"lastSyncedCoreBranch": "feat/perps/controller-in-core",
6+
"lastSyncedDate": "2026-05-21T08:44:09Z",
7+
"sourceChecksum": "b05070da67baeb718f1e926ad167863c47efb5240b19e3ac99c31766070d0f91"
88
}

packages/perps-controller/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add slippage controls so users can configure per-order slippage tolerance for market trades ([#8871](https://github.com/MetaMask/core/pull/8871))
13+
- Track `vip_tier` and `vip_discount` properties on perps trading events for fee analytics ([#8871](https://github.com/MetaMask/core/pull/8871))
14+
- Surface an in-app banner during an ongoing HyperLiquid outage so users see degraded trading status ([#8871](https://github.com/MetaMask/core/pull/8871))
15+
16+
### Fixed
17+
18+
- Prefer the currently selected EVM account when resolving the trading account so account switching is honored across providers ([#8871](https://github.com/MetaMask/core/pull/8871))
19+
- Suppress `User or API Wallet does not exist` Sentry noise from unfunded wallets that have not interacted with HyperLiquid ([#8871](https://github.com/MetaMask/core/pull/8871))
20+
- Approve the HyperLiquid builder fee when missing so order submission succeeds after fresh wallet setup ([#8871](https://github.com/MetaMask/core/pull/8871))
21+
1022
## [6.2.0]
1123

1224
### Changed

packages/perps-controller/jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ module.exports = {
1818
// An object that configures minimum threshold enforcement for coverage results
1919
coverageThreshold: {
2020
global: {
21-
branches: 70,
21+
branches: 69,
2222
functions: 78,
2323
lines: 80,
2424
statements: 80,

packages/perps-controller/src/PerpsController-method-action-types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,26 @@ export type PerpsControllerSaveMarketFilterPreferencesAction = {
895895
handler: PerpsController['saveMarketFilterPreferences'];
896896
};
897897

898+
/**
899+
* Get the user's max slippage tolerance in basis points.
900+
*
901+
* @returns The configured max slippage bps, or undefined if never set (callers should default to 300 bps / 3%).
902+
*/
903+
export type PerpsControllerGetMaxSlippageAction = {
904+
type: `PerpsController:getMaxSlippage`;
905+
handler: PerpsController['getMaxSlippage'];
906+
};
907+
908+
/**
909+
* Set the user's max slippage tolerance in basis points.
910+
*
911+
* @param bps - Max slippage in basis points (e.g. 300 = 3%). Clamped to 10–1000, snapped to step of 10.
912+
*/
913+
export type PerpsControllerSetMaxSlippageAction = {
914+
type: `PerpsController:setMaxSlippage`;
915+
handler: PerpsController['setMaxSlippage'];
916+
};
917+
898918
/**
899919
* Set the selected payment token for the Perps order/deposit flow.
900920
* Pass null or a token with description PERPS_CONSTANTS.PerpsBalanceTokenDescription to select Perps balance.
@@ -1060,6 +1080,8 @@ export type PerpsControllerMethodActions =
10601080
| PerpsControllerClearPendingTradeConfigurationAction
10611081
| PerpsControllerGetMarketFilterPreferencesAction
10621082
| PerpsControllerSaveMarketFilterPreferencesAction
1083+
| PerpsControllerGetMaxSlippageAction
1084+
| PerpsControllerSetMaxSlippageAction
10631085
| PerpsControllerSetSelectedPaymentTokenAction
10641086
| PerpsControllerResetSelectedPaymentTokenAction
10651087
| PerpsControllerGetOrderBookGroupingAction

packages/perps-controller/src/PerpsController.ts

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,16 @@ import {
1717
} from './constants/eventNames';
1818
import { USDC_SYMBOL } from './constants/hyperLiquidConfig';
1919
import { PerpsMeasurementName } from './constants/performanceMetrics';
20+
import type { SortOptionId } from './constants/perpsConfig';
2021
import {
2122
PERPS_CONSTANTS,
2223
MARKET_SORTING_CONFIG,
2324
PROVIDER_CONFIG,
2425
PERPS_DISK_CACHE_MARKETS,
2526
PERPS_DISK_CACHE_USER_DATA,
2627
buildProviderCacheKey,
28+
MAX_SLIPPAGE_BOUNDS,
2729
} from './constants/perpsConfig';
28-
import type { SortOptionId } from './constants/perpsConfig';
2930
import type { PerpsControllerMethodActions } from './PerpsController-method-action-types';
3031
import { PERPS_ERROR_CODES } from './perpsErrorCodes';
3132
import { AggregatedPerpsProvider } from './providers/AggregatedPerpsProvider';
@@ -120,7 +121,7 @@ import {
120121
LastTransactionResult,
121122
TransactionStatus,
122123
} from './types/transactionTypes';
123-
import { getSelectedEvmAccount } from './utils/accountUtils';
124+
import { getSelectedEvmAccountFromMessenger } from './utils/accountUtils';
124125
import { ensureError } from './utils/errorUtils';
125126
import {
126127
hydrateFromDiskSync,
@@ -343,6 +344,9 @@ export type PerpsControllerState = {
343344
};
344345
};
345346

347+
// Max slippage tolerance in basis points (e.g. 300 = 3%). Global user preference.
348+
maxSlippageBps?: number;
349+
346350
// Market filter preferences (network-independent) - includes both sorting and filtering options
347351
marketFilterPreferences: {
348352
optionId: SortOptionId;
@@ -590,6 +594,12 @@ const metadata: StateMetadata<PerpsControllerState> = {
590594
includeInDebugSnapshot: false,
591595
usedInUi: true,
592596
},
597+
maxSlippageBps: {
598+
includeInStateLogs: true,
599+
persist: true,
600+
includeInDebugSnapshot: false,
601+
usedInUi: true,
602+
},
593603
marketFilterPreferences: {
594604
includeInStateLogs: true,
595605
persist: true,
@@ -739,6 +749,8 @@ const MESSENGER_EXPOSED_METHODS = [
739749
'refreshEligibility',
740750
'resetFirstTimeUserState',
741751
'resetSelectedPaymentToken',
752+
'getMaxSlippage',
753+
'setMaxSlippage',
742754
'saveMarketFilterPreferences',
743755
'saveOrderBookGrouping',
744756
'savePendingTradeConfiguration',
@@ -1155,11 +1167,7 @@ export class PerpsController extends BaseController<
11551167
// Get current user address for validation
11561168
let currentAddress: string | null = null;
11571169
try {
1158-
const evmAccount = getSelectedEvmAccount(
1159-
this.messenger.call(
1160-
'AccountTreeController:getAccountsFromSelectedAccountGroup',
1161-
),
1162-
);
1170+
const evmAccount = getSelectedEvmAccountFromMessenger(this.messenger);
11631171
currentAddress = evmAccount?.address ?? null;
11641172
} catch {
11651173
// Can't determine current account — trust the cache
@@ -2181,6 +2189,7 @@ export class PerpsController extends BaseController<
21812189
return this.#tradingService.flipPosition({
21822190
provider,
21832191
position: params.position,
2192+
trackingData: params.trackingData,
21842193
context: this.#createServiceContext('flipPosition'),
21852194
});
21862195
}
@@ -2215,11 +2224,7 @@ export class PerpsController extends BaseController<
22152224
currentDepositId = depositId;
22162225

22172226
// Get current account address via messenger (outside of update() for proper typing)
2218-
const evmAccount = getSelectedEvmAccount(
2219-
this.messenger.call(
2220-
'AccountTreeController:getAccountsFromSelectedAccountGroup',
2221-
),
2222-
);
2227+
const evmAccount = getSelectedEvmAccountFromMessenger(this.messenger);
22232228
const accountAddress = evmAccount?.address ?? 'unknown';
22242229

22252230
this.update((state) => {
@@ -3096,13 +3101,9 @@ export class PerpsController extends BaseController<
30963101
this.messenger.unsubscribe('PerpsController:stateChange', handler);
30973102
};
30983103

3099-
// Watch for account changes via AccountTreeController
3104+
// Watch for selected account changes and selected account group changes.
31003105
const accountChangeHandler = (): void => {
3101-
const evmAccount = getSelectedEvmAccount(
3102-
this.messenger.call(
3103-
'AccountTreeController:getAccountsFromSelectedAccountGroup',
3104-
),
3105-
);
3106+
const evmAccount = getSelectedEvmAccountFromMessenger(this.messenger);
31063107
const currentAddress = evmAccount?.address ?? null;
31073108

31083109
// If any cached entry belongs to a different account, clear all entries.
@@ -3134,11 +3135,19 @@ export class PerpsController extends BaseController<
31343135
}
31353136
}
31363137
};
3138+
this.messenger.subscribe(
3139+
'AccountsController:selectedAccountChange',
3140+
accountChangeHandler,
3141+
);
31373142
this.messenger.subscribe(
31383143
'AccountTreeController:selectedAccountGroupChange',
31393144
accountChangeHandler,
31403145
);
31413146
this.#accountChangeUnsubscribe = (): void => {
3147+
this.messenger.unsubscribe(
3148+
'AccountsController:selectedAccountChange',
3149+
accountChangeHandler,
3150+
);
31423151
this.messenger.unsubscribe(
31433152
'AccountTreeController:selectedAccountGroupChange',
31443153
accountChangeHandler,
@@ -3339,11 +3348,7 @@ export class PerpsController extends BaseController<
33393348
}
33403349

33413350
// Get current user address
3342-
const evmAccount = getSelectedEvmAccount(
3343-
this.messenger.call(
3344-
'AccountTreeController:getAccountsFromSelectedAccountGroup',
3345-
),
3346-
);
3351+
const evmAccount = getSelectedEvmAccountFromMessenger(this.messenger);
33473352
if (!evmAccount?.address) {
33483353
return;
33493354
}
@@ -4821,6 +4826,39 @@ export class PerpsController extends BaseController<
48214826
});
48224827
}
48234828

4829+
/**
4830+
* Get the user's max slippage tolerance in basis points.
4831+
*
4832+
* @returns The configured max slippage bps, or undefined if never set (callers should default to 300 bps / 3%).
4833+
*/
4834+
getMaxSlippage(): number | undefined {
4835+
return this.state.maxSlippageBps;
4836+
}
4837+
4838+
/**
4839+
* Set the user's max slippage tolerance in basis points.
4840+
*
4841+
* @param bps - Max slippage in basis points (e.g. 300 = 3%). Clamped to 10–1000, snapped to step of 10.
4842+
*/
4843+
setMaxSlippage(bps: number): void {
4844+
// Reject non-finite input (NaN/Infinity) so it cannot reach the order
4845+
// path, where it would poison `getMaxSlippage` and produce a NaN limit
4846+
// price. `Math.max(..., NaN)` returns NaN and `??` does not catch it.
4847+
if (!Number.isFinite(bps)) {
4848+
return;
4849+
}
4850+
const clamped = Math.min(
4851+
MAX_SLIPPAGE_BOUNDS.MaxBps,
4852+
Math.max(MAX_SLIPPAGE_BOUNDS.MinBps, bps),
4853+
);
4854+
const snapped =
4855+
Math.round(clamped / MAX_SLIPPAGE_BOUNDS.StepBps) *
4856+
MAX_SLIPPAGE_BOUNDS.StepBps;
4857+
this.update((state) => {
4858+
state.maxSlippageBps = snapped;
4859+
});
4860+
}
4861+
48244862
/**
48254863
* Set the selected payment token for the Perps order/deposit flow.
48264864
* Pass null or a token with description PERPS_CONSTANTS.PerpsBalanceTokenDescription to select Perps balance.

packages/perps-controller/src/constants/eventNames.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ export const PERPS_EVENT_PROPERTY = {
110110
IMAGE_SELECTED: 'image_selected',
111111
TAB_NUMBER: 'tab_number',
112112

113+
// VIP rewards properties
114+
VIP_TIER: 'vip_tier',
115+
VIP_DISCOUNT: 'vip_discount',
116+
113117
// A/B testing properties (flat per test for multiple concurrent tests)
114118
// Only include AB test properties when test is enabled (event not sent when disabled)
115119
// Button color test (TAT-1937)
@@ -123,6 +127,9 @@ export const PERPS_EVENT_PROPERTY = {
123127
// Balance properties
124128
HAS_PERP_BALANCE: 'has_perp_balance',
125129

130+
// Service interruption banner
131+
OUTAGE_BANNER_SHOWN: 'outage_banner_shown',
132+
126133
// Geo-blocking properties (TAT-2337: track geo-blocked withdrawals for monitoring)
127134
IS_GEO_BLOCKED: 'is_geo_blocked',
128135

@@ -152,6 +159,11 @@ export const PERPS_EVENT_PROPERTY = {
152159
INITIAL_PAYMENT_METHOD: 'initial_payment_method',
153160
NEW_PAYMENT_METHOD: 'new_payment_method',
154161

162+
// Slippage properties
163+
MAX_SLIPPAGE_PCT: 'max_slippage_pct',
164+
MAX_SLIPPAGE_SOURCE: 'max_slippage_source',
165+
ESTIMATED_SLIPPAGE_PCT: 'estimated_slippage_pct',
166+
155167
// Account setup / abstraction mode (PERPS_ACCOUNT_SETUP)
156168
ABSTRACTION_MODE: 'abstraction_mode',
157169
PREVIOUS_ABSTRACTION_MODE: 'previous_abstraction_mode',
@@ -322,6 +334,14 @@ export const PERPS_EVENT_VALUE = {
322334
PAYMENT_METHOD_CHANGED: 'payment_method_changed',
323335
// Deposit + order (pay-with token) cancel
324336
CANCEL_TRADE_WITH_TOKEN: 'cancel_trade_with_token',
337+
// Slippage interactions
338+
SLIPPAGE_CONFIG_OPENED: 'slippage_config_opened',
339+
SLIPPAGE_CONFIG_CHANGED: 'slippage_config_changed',
340+
SLIPPAGE_LIMIT_BLOCKED_ORDER: 'slippage_limit_blocked_order',
341+
},
342+
MAX_SLIPPAGE_SOURCE: {
343+
DEFAULT: 'default',
344+
USER_CONFIGURED: 'user_configured',
325345
},
326346
ACTION_TYPE: {
327347
START_TRADING: 'start_trading',
@@ -360,6 +380,10 @@ export const PERPS_EVENT_VALUE = {
360380
SUCCESS: 'success',
361381
ALREADY_ENABLED: 'already_enabled',
362382
MIGRATION_REQUIRED: 'migration_required',
383+
// Emitted when a migration attempt is skipped because it is not applicable
384+
// (e.g. the user has no Hyperliquid account yet — nothing to migrate).
385+
// Distinguishes expected no-ops from real failures in dashboards.
386+
NOT_APPLICABLE: 'not_applicable',
363387
},
364388
SCREEN_TYPE: {
365389
MARKETS: 'markets',
@@ -401,6 +425,7 @@ export const PERPS_EVENT_VALUE = {
401425
},
402426
SETTING_TYPE: {
403427
LEVERAGE: 'leverage',
428+
SLIPPAGE: 'slippage',
404429
},
405430
SCREEN_NAME: {
406431
CONNECTION_ERROR: 'connection_error',

packages/perps-controller/src/constants/perpsConfig.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,16 @@ export const ORDER_SLIPPAGE_CONFIG = {
106106
DefaultLimitSlippageBps: 100,
107107
} as const;
108108

109+
/**
110+
* Bounds and step for the user-configurable max slippage preference (basis points).
111+
* Shared by the controller (`setMaxSlippage`) and UI (`slippageConfig.ts`).
112+
*/
113+
export const MAX_SLIPPAGE_BOUNDS = {
114+
MinBps: 10,
115+
MaxBps: 1000,
116+
StepBps: 10,
117+
} as const;
118+
109119
/**
110120
* Max order amount buffer to reduce "Insufficient margin" rejections from the exchange.
111121
* When the user selects 100% (slider or Max), we cap the order at (1 - this) of the
@@ -135,6 +145,20 @@ export const PERFORMANCE_CONFIG = {
135145
// Prevents WS subscription churn during rapid market switching (#28141)
136146
CandleConnectDebounceMs: 500,
137147

148+
// Order-form slippage estimate throttle (milliseconds)
149+
// Updates the estimated-slippage value derived from the live L2 order book
150+
// no more than once per window. Aggressive enough to keep the row reactive
151+
// while the user edits the amount, conservative enough to avoid re-render
152+
// pressure on every book tick.
153+
SlippageEstimateThrottleMs: 250,
154+
155+
// Order-book levels sampled when estimating slippage
156+
// Number of price levels (per side) walked by `calculateEstimatedSlippageBps`
157+
// to fill the requested USD notional. Matches the L2 sample size used by the
158+
// order-book panel and is enough depth for the typical order sizes we
159+
// surface in the order form.
160+
SlippageEstimateBookLevels: 10,
161+
138162
// Candle WS teardown delay (milliseconds)
139163
// When the last subscriber for a cacheKey unsubscribes, wait this long before
140164
// tearing down the WS. A subsequent subscribe inside the window cancels the

packages/perps-controller/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,7 @@ export {
409409
WITHDRAWAL_CONSTANTS,
410410
VALIDATION_THRESHOLDS,
411411
ORDER_SLIPPAGE_CONFIG,
412+
MAX_SLIPPAGE_BOUNDS,
412413
PERFORMANCE_CONFIG,
413414
TP_SL_CONFIG,
414415
HYPERLIQUID_ORDER_LIMITS,

0 commit comments

Comments
 (0)