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": "c2248a8a9e83e02f1fcb1b95ec33c23f5cde2a5d",
"lastSyncedMobileBranch": "sync-perps-core",
"lastSyncedCoreCommit": "e30c0b11e0808242382f62283b1643cc6723cf4a",
"lastSyncedCoreBranch": "latest-perps-sync",
"lastSyncedDate": "2026-04-17T16:42:46Z",
"sourceChecksum": "8cd74a6ee57a4ead596f0eea26176aa9d386da905b6306928b347aafbf28a0c0"
"lastSyncedMobileCommit": "d4c68052ccec878922a9909ee95306252a959ff8",
"lastSyncedMobileBranch": "fix/core-sync-bugbot",
"lastSyncedCoreCommit": "5ea3b550b9e52744443da89da4a929ba2a7b0df7",
"lastSyncedCoreBranch": "feat/perps/controller-apr-23rd",
"lastSyncedDate": "2026-04-23T10:22:26Z",
"sourceChecksum": "9afe08e6639a1705e1ac7b51f47d7617e1195504ec05928783e8bf0fcda44f9d"
}
25 changes: 25 additions & 0 deletions packages/perps-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `coalescePerpsRestRequest` utility for deduplicating concurrent REST requests with account-scoped cache keys ([#8560](https://github.com/MetaMask/core/pull/8560))
- Add `accountUtils` helpers for resolving the active perps account id and pinning it to forwarded provider params ([#8560](https://github.com/MetaMask/core/pull/8560))

### Changed

- Account-scope the REST cache and guard cache writes so mount load stays cacheable without cross-account bleed ([#8560](https://github.com/MetaMask/core/pull/8560))
- Make `forceRefresh` provider-agnostic and align rate-limit handling with the extension ([#8560](https://github.com/MetaMask/core/pull/8560))
- Regenerate `PerpsController` method action types; shrink rate-limit diff and drop verbose history logs ([#8560](https://github.com/MetaMask/core/pull/8560))

### Removed

- Drop the dead `spotState` parameter from `adaptAccountStateFromSDK`. Spot balances are layered on by `addSpotBalanceToAccountState`, which enforces the USDC-only policy via `SPOT_COLLATERAL_COINS`; removing the dormant branch keeps one source of truth and prevents a future caller from silently getting ALL-coins behavior ([#8560](https://github.com/MetaMask/core/pull/8560))

### Fixed

- HyperLiquid Unified-mode live balance: subscribe to `spotState` WS and compute tradeable/total balance from on-chain math ([#8560](https://github.com/MetaMask/core/pull/8560))
- Complete spot-balance parity with the extension consumer ([#8560](https://github.com/MetaMask/core/pull/8560))
- Preserve integer trailing zeros when `szDecimals=0` in `perpsFormatters` ([#8560](https://github.com/MetaMask/core/pull/8560))
- Preserve candle pagination cancellation and skip coalesce for explicit-`endTime` candle paging to avoid stale pages ([#8560](https://github.com/MetaMask/core/pull/8560))
- Defer account resolution on the non-paginated cache path to prevent race conditions ([#8560](https://github.com/MetaMask/core/pull/8560))
- Force-refresh on activity mount and evict expired coalesce entries so stale promises cannot resolve to cache ([#8560](https://github.com/MetaMask/core/pull/8560))
- Normalize `event.user` to lowercase when caching the spot-state WS address so `#ensureSpotState` hits the cache instead of triggering a redundant REST `spotClearinghouseState` refetch when HyperLiquid returns a checksummed address ([#8560](https://github.com/MetaMask/core/pull/8560))

## [3.2.0]

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,9 @@ export type PerpsControllerGetPositionsAction = {
* Thin delegation to MarketDataService
*
* @param params - The operation parameters.
* @param options - Optional call modifiers.
* @param options.forceRefresh - Bypass the request-coalesce cache
* end-to-end (user-initiated refresh).
* @returns Array of historical trade executions (fills).
*/
export type PerpsControllerGetOrderFillsAction = {
Expand All @@ -318,6 +321,9 @@ export type PerpsControllerGetOrderFillsAction = {
* Thin delegation to MarketDataService
*
* @param params - The operation parameters.
* @param options - Optional call modifiers.
* @param options.forceRefresh - Bypass the request-coalesce cache
* end-to-end (user-initiated refresh).
* @returns Array of historical orders.
*/
export type PerpsControllerGetOrdersAction = {
Expand Down Expand Up @@ -345,6 +351,9 @@ export type PerpsControllerGetOpenOrdersAction = {
* Thin delegation to MarketDataService
*
* @param params - The operation parameters.
* @param options - Optional call modifiers.
* @param options.forceRefresh - Bypass the request-coalesce cache
* end-to-end (user-initiated refresh).
* @returns Array of historical funding payments.
*/
export type PerpsControllerGetFundingAction = {
Expand Down
27 changes: 24 additions & 3 deletions packages/perps-controller/src/PerpsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2761,14 +2761,21 @@ export class PerpsController extends BaseController<
* Thin delegation to MarketDataService
*
* @param params - The operation parameters.
* @param options - Optional call modifiers.
* @param options.forceRefresh - Bypass the request-coalesce cache
* end-to-end (user-initiated refresh).
* @returns Array of historical trade executions (fills).
*/
async getOrderFills(params?: GetOrderFillsParams): Promise<OrderFill[]> {
async getOrderFills(
params?: GetOrderFillsParams,
options?: { forceRefresh?: boolean },
): Promise<OrderFill[]> {
const provider = this.getActiveProvider();
return this.#marketDataService.getOrderFills({
provider,
params,
context: this.#createServiceContext('getOrderFills'),
forceRefresh: options?.forceRefresh,
});
}

Expand All @@ -2777,14 +2784,21 @@ export class PerpsController extends BaseController<
* Thin delegation to MarketDataService
*
* @param params - The operation parameters.
* @param options - Optional call modifiers.
* @param options.forceRefresh - Bypass the request-coalesce cache
* end-to-end (user-initiated refresh).
* @returns Array of historical orders.
*/
async getOrders(params?: GetOrdersParams): Promise<Order[]> {
async getOrders(
params?: GetOrdersParams,
options?: { forceRefresh?: boolean },
): Promise<Order[]> {
const provider = this.getActiveProvider();
return this.#marketDataService.getOrders({
provider,
params,
context: this.#createServiceContext('getOrders'),
forceRefresh: options?.forceRefresh,
});
}

Expand Down Expand Up @@ -2819,14 +2833,21 @@ export class PerpsController extends BaseController<
* Thin delegation to MarketDataService
*
* @param params - The operation parameters.
* @param options - Optional call modifiers.
* @param options.forceRefresh - Bypass the request-coalesce cache
* end-to-end (user-initiated refresh).
* @returns Array of historical funding payments.
*/
async getFunding(params?: GetFundingParams): Promise<Funding[]> {
async getFunding(
params?: GetFundingParams,
options?: { forceRefresh?: boolean },
): Promise<Funding[]> {
const provider = this.getActiveProvider();
return this.#marketDataService.getFunding({
provider,
params,
context: this.#createServiceContext('getFunding'),
forceRefresh: options?.forceRefresh,
});
}

Expand Down
31 changes: 31 additions & 0 deletions packages/perps-controller/src/constants/perpsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,37 @@ export const PERFORMANCE_CONFIG = {
// Prevents WS subscription churn during rapid market switching (#28141)
CandleConnectDebounceMs: 500,

// 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
// teardown so rapid back-and-forth switches do not churn the connection.
CandleTeardownDelayMs: 150,

// Perps REST coalesce TTL (milliseconds)
//
// Window in which identical GET-style REST calls (getOrderFills, getOrders,
// getFunding, historicalOrders) share a single in-flight promise / cached
// result. `forceRefresh` still bypasses the cache end-to-end (hooks →
// controller → MarketDataService → provider → HyperLiquidClientService), so
// pull-to-refresh always hits the network.
//
// Why 60 s: HyperLiquid's documented rate limit is 1200 weight / IP /
// rolling 60 s window. Sizing TTL = window length caps each endpoint-per-
// account at ≤1 REST hit per window under any UI activity pattern — rapid
// market switching, re-mounts (usePerpsMarketFills, usePerpsTransactionHistory),
// and multi-tab scans all share a single request. Live fills/orders/prices
// still flow via WS subscriptions, so REST is seed/backfill only — cache
// staleness inside the 60 s window is never user-visible.
PerpsRestCoalesceTtlMs: 60_000,

// Candle snapshot REST coalesce TTL (milliseconds).
// Longer than PerpsRestCoalesceTtlMs because WS stream keeps live candles
// fresh — the REST snapshot only seeds the chart on initial subscribe. A
// 30 s window lets rapid market switching (pass 1 → pass 2 of a stress
// loop) share the same snapshot per (symbol, interval), cutting
// candleSnapshot REST weight roughly in half.
PerpsCandleCoalesceTtlMs: 30_000,

// Navigation params delay (milliseconds)
// Required for React Navigation to complete state transitions before setting params
// This ensures navigation context is available when programmatically selecting tabs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import type {
WithdrawParams,
WithdrawResult,
RawLedgerUpdate,
PerpsReadOptions,
} from '../types';

/**
Expand Down Expand Up @@ -297,10 +298,13 @@ export class AggregatedPerpsProvider implements PerpsProvider {
).flat();
}

async getOrderFills(params?: GetOrderFillsParams): Promise<OrderFill[]> {
async getOrderFills(
params?: GetOrderFillsParams,
options?: PerpsReadOptions,
): Promise<OrderFill[]> {
const results = await Promise.allSettled(
this.#getActiveProviders().map(async ([id, provider]) => {
const fills = await provider.getOrderFills(params);
const fills = await provider.getOrderFills(params, options);
return fills.map((fill) => ({ ...fill, providerId: id }));
}),
);
Expand All @@ -319,10 +323,13 @@ export class AggregatedPerpsProvider implements PerpsProvider {
return this.#extractSuccessfulResults(results, 'getOrFetchFills').flat();
}

async getOrders(params?: GetOrdersParams): Promise<Order[]> {
async getOrders(
params?: GetOrdersParams,
options?: PerpsReadOptions,
): Promise<Order[]> {
const results = await Promise.allSettled(
this.#getActiveProviders().map(async ([id, provider]) => {
const orders = await provider.getOrders(params);
const orders = await provider.getOrders(params, options);
return orders.map((order) => ({ ...order, providerId: id }));
}),
);
Expand All @@ -341,10 +348,13 @@ export class AggregatedPerpsProvider implements PerpsProvider {
return this.#extractSuccessfulResults(results, 'getOpenOrders').flat();
}

async getFunding(params?: GetFundingParams): Promise<Funding[]> {
async getFunding(
params?: GetFundingParams,
options?: PerpsReadOptions,
): Promise<Funding[]> {
const results = await Promise.allSettled(
this.#getActiveProviders().map(async ([_providerId, provider]) => {
const funding = await provider.getFunding(params);
const funding = await provider.getFunding(params, options);
// Funding type doesn't have providerId - we could add it if needed
return funding;
}),
Expand Down Expand Up @@ -378,6 +388,17 @@ export class AggregatedPerpsProvider implements PerpsProvider {
return this.#getDefaultProvider().getUserNonFundingLedgerUpdates(params);
}

/**
* Resolve the currently selected CAIP account identifier. Accounts are
* shared across sub-providers (same InternalAccountController), so the
* default provider's view is authoritative.
*
* @returns Resolved CAIP account id from the default sub-provider.
*/
async getCurrentAccountId(): Promise<CaipAccountId> {
return this.#getDefaultProvider().getCurrentAccountId();
}

/**
* Get user history from all providers.
*
Expand Down
Loading
Loading