Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions packages/perps-controller/.sync-state.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"lastSyncedMobileCommit": "35953448cf3c32b2867de8fe0599a356925913ef",
"lastSyncedMobileBranch": "main",
"lastSyncedCoreCommit": "3e549deb97d362c6798a0062dd8b01ac481615c4",
"lastSyncedCoreBranch": "main",
"lastSyncedDate": "2026-05-13T21:35:30Z",
"sourceChecksum": "79a9acc7ad058802b357c6f54774799229b44e9418e802f2f7958e345e16cf59"
"lastSyncedMobileCommit": "cc154d351581605282f5a70f8749565956d42b36",
"lastSyncedMobileBranch": "TAT-3187-perps-controller-removal",
"lastSyncedCoreCommit": "fbe58b4cca248101d12df709c0092cd87f15956f",
"lastSyncedCoreBranch": "feat/perps/controller-in-core",
"lastSyncedDate": "2026-05-21T08:44:09Z",
"sourceChecksum": "b05070da67baeb718f1e926ad167863c47efb5240b19e3ac99c31766070d0f91"
}
12 changes: 12 additions & 0 deletions packages/perps-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add slippage controls so users can configure per-order slippage tolerance for market trades ([#8871](https://github.com/MetaMask/core/pull/8871))
- Track `vip_tier` and `vip_discount` properties on perps trading events for fee analytics ([#8871](https://github.com/MetaMask/core/pull/8871))
- Surface an in-app banner during an ongoing HyperLiquid outage so users see degraded trading status ([#8871](https://github.com/MetaMask/core/pull/8871))

### Fixed

- 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))
- 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))
- Approve the HyperLiquid builder fee when missing so order submission succeeds after fresh wallet setup ([#8871](https://github.com/MetaMask/core/pull/8871))

## [6.2.0]

### Changed
Expand Down
2 changes: 1 addition & 1 deletion packages/perps-controller/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module.exports = {
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 70,
branches: 69,
functions: 78,
lines: 80,
statements: 80,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,26 @@ export type PerpsControllerSaveMarketFilterPreferencesAction = {
handler: PerpsController['saveMarketFilterPreferences'];
};

/**
* Get the user's max slippage tolerance in basis points.
*
* @returns The configured max slippage bps, or undefined if never set (callers should default to 300 bps / 3%).
*/
export type PerpsControllerGetMaxSlippageAction = {
type: `PerpsController:getMaxSlippage`;
handler: PerpsController['getMaxSlippage'];
};

/**
* Set the user's max slippage tolerance in basis points.
*
* @param bps - Max slippage in basis points (e.g. 300 = 3%). Clamped to 10–1000, snapped to step of 10.
*/
export type PerpsControllerSetMaxSlippageAction = {
type: `PerpsController:setMaxSlippage`;
handler: PerpsController['setMaxSlippage'];
};

/**
* Set the selected payment token for the Perps order/deposit flow.
* Pass null or a token with description PERPS_CONSTANTS.PerpsBalanceTokenDescription to select Perps balance.
Expand Down Expand Up @@ -1060,6 +1080,8 @@ export type PerpsControllerMethodActions =
| PerpsControllerClearPendingTradeConfigurationAction
| PerpsControllerGetMarketFilterPreferencesAction
| PerpsControllerSaveMarketFilterPreferencesAction
| PerpsControllerGetMaxSlippageAction
| PerpsControllerSetMaxSlippageAction
| PerpsControllerSetSelectedPaymentTokenAction
| PerpsControllerResetSelectedPaymentTokenAction
| PerpsControllerGetOrderBookGroupingAction
Expand Down
84 changes: 61 additions & 23 deletions packages/perps-controller/src/PerpsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ import {
} from './constants/eventNames';
import { USDC_SYMBOL } from './constants/hyperLiquidConfig';
import { PerpsMeasurementName } from './constants/performanceMetrics';
import type { SortOptionId } from './constants/perpsConfig';
import {
PERPS_CONSTANTS,
MARKET_SORTING_CONFIG,
PROVIDER_CONFIG,
PERPS_DISK_CACHE_MARKETS,
PERPS_DISK_CACHE_USER_DATA,
buildProviderCacheKey,
MAX_SLIPPAGE_BOUNDS,
} from './constants/perpsConfig';
import type { SortOptionId } from './constants/perpsConfig';
import type { PerpsControllerMethodActions } from './PerpsController-method-action-types';
import { PERPS_ERROR_CODES } from './perpsErrorCodes';
import { AggregatedPerpsProvider } from './providers/AggregatedPerpsProvider';
Expand Down Expand Up @@ -120,7 +121,7 @@ import {
LastTransactionResult,
TransactionStatus,
} from './types/transactionTypes';
import { getSelectedEvmAccount } from './utils/accountUtils';
import { getSelectedEvmAccountFromMessenger } from './utils/accountUtils';
import { ensureError } from './utils/errorUtils';
import {
hydrateFromDiskSync,
Expand Down Expand Up @@ -343,6 +344,9 @@ export type PerpsControllerState = {
};
};

// Max slippage tolerance in basis points (e.g. 300 = 3%). Global user preference.
maxSlippageBps?: number;

// Market filter preferences (network-independent) - includes both sorting and filtering options
marketFilterPreferences: {
optionId: SortOptionId;
Expand Down Expand Up @@ -590,6 +594,12 @@ const metadata: StateMetadata<PerpsControllerState> = {
includeInDebugSnapshot: false,
usedInUi: true,
},
maxSlippageBps: {
includeInStateLogs: true,
persist: true,
includeInDebugSnapshot: false,
usedInUi: true,
},
marketFilterPreferences: {
includeInStateLogs: true,
persist: true,
Expand Down Expand Up @@ -739,6 +749,8 @@ const MESSENGER_EXPOSED_METHODS = [
'refreshEligibility',
'resetFirstTimeUserState',
'resetSelectedPaymentToken',
'getMaxSlippage',
'setMaxSlippage',
'saveMarketFilterPreferences',
'saveOrderBookGrouping',
'savePendingTradeConfiguration',
Expand Down Expand Up @@ -1155,11 +1167,7 @@ export class PerpsController extends BaseController<
// Get current user address for validation
let currentAddress: string | null = null;
try {
const evmAccount = getSelectedEvmAccount(
this.messenger.call(
'AccountTreeController:getAccountsFromSelectedAccountGroup',
),
);
const evmAccount = getSelectedEvmAccountFromMessenger(this.messenger);
currentAddress = evmAccount?.address ?? null;
} catch {
// Can't determine current account — trust the cache
Expand Down Expand Up @@ -2181,6 +2189,7 @@ export class PerpsController extends BaseController<
return this.#tradingService.flipPosition({
provider,
position: params.position,
trackingData: params.trackingData,
context: this.#createServiceContext('flipPosition'),
});
}
Expand Down Expand Up @@ -2215,11 +2224,7 @@ export class PerpsController extends BaseController<
currentDepositId = depositId;

// Get current account address via messenger (outside of update() for proper typing)
const evmAccount = getSelectedEvmAccount(
this.messenger.call(
'AccountTreeController:getAccountsFromSelectedAccountGroup',
),
);
const evmAccount = getSelectedEvmAccountFromMessenger(this.messenger);
const accountAddress = evmAccount?.address ?? 'unknown';

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

// Watch for account changes via AccountTreeController
// Watch for selected account changes and selected account group changes.
const accountChangeHandler = (): void => {
const evmAccount = getSelectedEvmAccount(
this.messenger.call(
'AccountTreeController:getAccountsFromSelectedAccountGroup',
),
);
const evmAccount = getSelectedEvmAccountFromMessenger(this.messenger);
const currentAddress = evmAccount?.address ?? null;

// If any cached entry belongs to a different account, clear all entries.
Expand Down Expand Up @@ -3134,11 +3135,19 @@ export class PerpsController extends BaseController<
}
}
};
this.messenger.subscribe(
'AccountsController:selectedAccountChange',
accountChangeHandler,
);
this.messenger.subscribe(
'AccountTreeController:selectedAccountGroupChange',
accountChangeHandler,
);
this.#accountChangeUnsubscribe = (): void => {
this.messenger.unsubscribe(
'AccountsController:selectedAccountChange',
accountChangeHandler,
);
this.messenger.unsubscribe(
'AccountTreeController:selectedAccountGroupChange',
accountChangeHandler,
Expand Down Expand Up @@ -3339,11 +3348,7 @@ export class PerpsController extends BaseController<
}

// Get current user address
const evmAccount = getSelectedEvmAccount(
this.messenger.call(
'AccountTreeController:getAccountsFromSelectedAccountGroup',
),
);
const evmAccount = getSelectedEvmAccountFromMessenger(this.messenger);
if (!evmAccount?.address) {
return;
}
Expand Down Expand Up @@ -4821,6 +4826,39 @@ export class PerpsController extends BaseController<
});
}

/**
* Get the user's max slippage tolerance in basis points.
*
* @returns The configured max slippage bps, or undefined if never set (callers should default to 300 bps / 3%).
*/
getMaxSlippage(): number | undefined {
return this.state.maxSlippageBps;
}

/**
* Set the user's max slippage tolerance in basis points.
*
* @param bps - Max slippage in basis points (e.g. 300 = 3%). Clamped to 10–1000, snapped to step of 10.
*/
setMaxSlippage(bps: number): void {
// Reject non-finite input (NaN/Infinity) so it cannot reach the order
// path, where it would poison `getMaxSlippage` and produce a NaN limit
// price. `Math.max(..., NaN)` returns NaN and `??` does not catch it.
if (!Number.isFinite(bps)) {
return;
}
const clamped = Math.min(
MAX_SLIPPAGE_BOUNDS.MaxBps,
Math.max(MAX_SLIPPAGE_BOUNDS.MinBps, bps),
);
const snapped =
Math.round(clamped / MAX_SLIPPAGE_BOUNDS.StepBps) *
MAX_SLIPPAGE_BOUNDS.StepBps;
this.update((state) => {
state.maxSlippageBps = snapped;
});
}

/**
* Set the selected payment token for the Perps order/deposit flow.
* Pass null or a token with description PERPS_CONSTANTS.PerpsBalanceTokenDescription to select Perps balance.
Expand Down
25 changes: 25 additions & 0 deletions packages/perps-controller/src/constants/eventNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ export const PERPS_EVENT_PROPERTY = {
IMAGE_SELECTED: 'image_selected',
TAB_NUMBER: 'tab_number',

// VIP rewards properties
VIP_TIER: 'vip_tier',
VIP_DISCOUNT: 'vip_discount',

// A/B testing properties (flat per test for multiple concurrent tests)
// Only include AB test properties when test is enabled (event not sent when disabled)
// Button color test (TAT-1937)
Expand All @@ -123,6 +127,9 @@ export const PERPS_EVENT_PROPERTY = {
// Balance properties
HAS_PERP_BALANCE: 'has_perp_balance',

// Service interruption banner
OUTAGE_BANNER_SHOWN: 'outage_banner_shown',

// Geo-blocking properties (TAT-2337: track geo-blocked withdrawals for monitoring)
IS_GEO_BLOCKED: 'is_geo_blocked',

Expand Down Expand Up @@ -152,6 +159,11 @@ export const PERPS_EVENT_PROPERTY = {
INITIAL_PAYMENT_METHOD: 'initial_payment_method',
NEW_PAYMENT_METHOD: 'new_payment_method',

// Slippage properties
MAX_SLIPPAGE_PCT: 'max_slippage_pct',
MAX_SLIPPAGE_SOURCE: 'max_slippage_source',
ESTIMATED_SLIPPAGE_PCT: 'estimated_slippage_pct',

// Account setup / abstraction mode (PERPS_ACCOUNT_SETUP)
ABSTRACTION_MODE: 'abstraction_mode',
PREVIOUS_ABSTRACTION_MODE: 'previous_abstraction_mode',
Expand Down Expand Up @@ -322,6 +334,14 @@ export const PERPS_EVENT_VALUE = {
PAYMENT_METHOD_CHANGED: 'payment_method_changed',
// Deposit + order (pay-with token) cancel
CANCEL_TRADE_WITH_TOKEN: 'cancel_trade_with_token',
// Slippage interactions
SLIPPAGE_CONFIG_OPENED: 'slippage_config_opened',
SLIPPAGE_CONFIG_CHANGED: 'slippage_config_changed',
SLIPPAGE_LIMIT_BLOCKED_ORDER: 'slippage_limit_blocked_order',
},
MAX_SLIPPAGE_SOURCE: {
DEFAULT: 'default',
USER_CONFIGURED: 'user_configured',
},
ACTION_TYPE: {
START_TRADING: 'start_trading',
Expand Down Expand Up @@ -360,6 +380,10 @@ export const PERPS_EVENT_VALUE = {
SUCCESS: 'success',
ALREADY_ENABLED: 'already_enabled',
MIGRATION_REQUIRED: 'migration_required',
// Emitted when a migration attempt is skipped because it is not applicable
// (e.g. the user has no Hyperliquid account yet — nothing to migrate).
// Distinguishes expected no-ops from real failures in dashboards.
NOT_APPLICABLE: 'not_applicable',
},
SCREEN_TYPE: {
MARKETS: 'markets',
Expand Down Expand Up @@ -401,6 +425,7 @@ export const PERPS_EVENT_VALUE = {
},
SETTING_TYPE: {
LEVERAGE: 'leverage',
SLIPPAGE: 'slippage',
},
SCREEN_NAME: {
CONNECTION_ERROR: 'connection_error',
Expand Down
24 changes: 24 additions & 0 deletions packages/perps-controller/src/constants/perpsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,16 @@ export const ORDER_SLIPPAGE_CONFIG = {
DefaultLimitSlippageBps: 100,
} as const;

/**
* Bounds and step for the user-configurable max slippage preference (basis points).
* Shared by the controller (`setMaxSlippage`) and UI (`slippageConfig.ts`).
*/
export const MAX_SLIPPAGE_BOUNDS = {
MinBps: 10,
MaxBps: 1000,
StepBps: 10,
} as const;

/**
* Max order amount buffer to reduce "Insufficient margin" rejections from the exchange.
* When the user selects 100% (slider or Max), we cap the order at (1 - this) of the
Expand Down Expand Up @@ -135,6 +145,20 @@ export const PERFORMANCE_CONFIG = {
// Prevents WS subscription churn during rapid market switching (#28141)
CandleConnectDebounceMs: 500,

// Order-form slippage estimate throttle (milliseconds)
// Updates the estimated-slippage value derived from the live L2 order book
// no more than once per window. Aggressive enough to keep the row reactive
// while the user edits the amount, conservative enough to avoid re-render
// pressure on every book tick.
SlippageEstimateThrottleMs: 250,

// Order-book levels sampled when estimating slippage
// Number of price levels (per side) walked by `calculateEstimatedSlippageBps`
// to fill the requested USD notional. Matches the L2 sample size used by the
// order-book panel and is enough depth for the typical order sizes we
// surface in the order form.
SlippageEstimateBookLevels: 10,

// Candle WS teardown delay (milliseconds)
// When the last subscriber for a cacheKey unsubscribes, wait this long before
// tearing down the WS. A subsequent subscribe inside the window cancels the
Expand Down
1 change: 1 addition & 0 deletions packages/perps-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ export {
WITHDRAWAL_CONSTANTS,
VALIDATION_THRESHOLDS,
ORDER_SLIPPAGE_CONFIG,
MAX_SLIPPAGE_BOUNDS,
PERFORMANCE_CONFIG,
TP_SL_CONFIG,
HYPERLIQUID_ORDER_LIMITS,
Expand Down
Loading
Loading