From c4b2e169197fe006f52751760e57a6200c4d2823 Mon Sep 17 00:00:00 2001 From: Arthur Breton Date: Fri, 19 Jun 2026 14:40:49 +0200 Subject: [PATCH 1/3] feat(perps): expose market tradability for unavailable HIP-3 markets From 62995b2b7f4576ac3c4e2f5f80e581e68cbd49dc Mon Sep 17 00:00:00 2001 From: Arthur Breton Date: Fri, 19 Jun 2026 15:33:31 +0200 Subject: [PATCH 2/3] feat(perps): expose market tradability for unavailable HIP-3 markets Add PriceUpdate.isTradable, computed per provider from the market's mid-vs-oracle price deviation, so clients can warn before placing an order the protocol would reject (HyperLiquid rejects orders >95% from the reference price, which most often affects HIP-3 markets). - Export pure isMarketTradable helper; missing/non-positive prices default to tradable so transient no-data ticks never raise a banner. - Add HYPERLIQUID_CONFIG.OraclePriceDeviationLimit (0.95) HL default. - Add protocol-agnostic PerpsControllerConfig.fallbackPriceDeviationLimit client override, threaded controller -> provider -> subscription service. --- packages/perps-controller/CHANGELOG.md | 4 + .../perps-controller/src/PerpsController.ts | 8 ++ .../src/constants/hyperLiquidConfig.ts | 7 + packages/perps-controller/src/index.ts | 1 + .../src/providers/HyperLiquidProvider.ts | 7 + .../HyperLiquidSubscriptionService.ts | 21 ++- packages/perps-controller/src/types/index.ts | 24 ++++ .../src/utils/marketDataTransform.ts | 52 ++++++++ ...uidSubscriptionService.market-data.test.ts | 121 ++++++++++++++++++ .../src/utils/marketDataTransform.test.ts | 77 +++++++++++ 10 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 packages/perps-controller/tests/src/utils/marketDataTransform.test.ts diff --git a/packages/perps-controller/CHANGELOG.md b/packages/perps-controller/CHANGELOG.md index 176e1e7ebe..f5c409f3cf 100644 --- a/packages/perps-controller/CHANGELOG.md +++ b/packages/perps-controller/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Surface per-market trading availability so clients can warn before placing an order that would be rejected ([#9205](https://github.com/MetaMask/core/pull/9205)) + - Add an optional `isTradable` boolean to `PriceUpdate`. It is `false` when a market's mid price has drifted past the protocol's oracle-deviation limit (HyperLiquid rejects orders more than 95% away from the reference price, which most often affects HIP-3 markets); `undefined` means tradability is unknown and the market should be treated as tradable. + - Add an optional, protocol-agnostic `fallbackPriceDeviationLimit` to `PerpsControllerConfig` so clients can tune the deviation threshold; each provider applies its own default when omitted. + - Export the pure `isMarketTradable` helper and add `HYPERLIQUID_CONFIG.OraclePriceDeviationLimit` (`0.95`, the HyperLiquid default). - Add observational hard timeout for order submission: tag the `Perps Order Submission` trace and emit a breadcrumb when a provider round-trip exceeds `PlaceOrderTimeoutMs` (60s), without cancelling the in-flight order ([#21217](https://github.com/MetaMask/core/pull/21217)) ### Changed diff --git a/packages/perps-controller/src/PerpsController.ts b/packages/perps-controller/src/PerpsController.ts index 8e57403634..577a6b8d28 100644 --- a/packages/perps-controller/src/PerpsController.ts +++ b/packages/perps-controller/src/PerpsController.ts @@ -832,6 +832,11 @@ export class PerpsController extends BaseController< #hip3ConfigSource: 'remote' | 'fallback' = 'fallback'; + // Optional client override for the max market-vs-oracle price deviation before a + // market is reported untradable (PriceUpdate.isTradable). Protocol-agnostic: passed + // through to each provider, which applies its own default when this is undefined. + readonly #priceDeviationLimit?: number; + /** * Check if MYX provider is enabled via feature flag * Uses same pattern as other feature flags in FeatureFlagConfigurationService @@ -971,6 +976,7 @@ export class PerpsController extends BaseController< this.#hip3BlocklistMarkets = [ ...(clientConfig.fallbackHip3BlocklistMarkets ?? []), ]; + this.#priceDeviationLimit = clientConfig.fallbackPriceDeviationLimit; // Immediately set the fallback region list since RemoteFeatureFlagController is empty by default and takes a moment to populate. this.setBlockedRegionList( @@ -1277,6 +1283,7 @@ export class PerpsController extends BaseController< hip3Enabled: this.#hip3Enabled, allowlistMarkets: this.#hip3AllowlistMarkets, blocklistMarkets: this.#hip3BlocklistMarkets, + priceDeviationLimit: this.#priceDeviationLimit, platformDependencies: this.#options.infrastructure, messenger: this.messenger, builderAddressTestnet: @@ -1718,6 +1725,7 @@ export class PerpsController extends BaseController< hip3Enabled: this.#hip3Enabled, allowlistMarkets: this.#hip3AllowlistMarkets, blocklistMarkets: this.#hip3BlocklistMarkets, + priceDeviationLimit: this.#priceDeviationLimit, platformDependencies: this.#options.infrastructure, messenger: this.messenger, builderAddressTestnet: diff --git a/packages/perps-controller/src/constants/hyperLiquidConfig.ts b/packages/perps-controller/src/constants/hyperLiquidConfig.ts index 2421e6ee08..ea72ff055e 100644 --- a/packages/perps-controller/src/constants/hyperLiquidConfig.ts +++ b/packages/perps-controller/src/constants/hyperLiquidConfig.ts @@ -246,6 +246,13 @@ export const HYPERLIQUID_CONFIG = { // Exchange name used in predicted funding data // HyperLiquid uses 'HlPerp' as their perps exchange identifier ExchangeName: 'HlPerp', + // Maximum allowed deviation of the market (mid) price from the oracle (reference) + // price before HyperLiquid rejects orders. HyperLiquid enforces "Order price cannot + // be more than 95% away from the reference price", which makes markets — most often + // HIP-3 builder-deployed ones — temporarily untradable when the mid price drifts past + // this limit. Expressed as a decimal fraction (0.95 = 95%). + // Protocol rule, not a UI warning threshold (see VALIDATION_THRESHOLDS.PriceDeviation). + OraclePriceDeviationLimit: 0.95, } as const; /** diff --git a/packages/perps-controller/src/index.ts b/packages/perps-controller/src/index.ts index 040ff121dd..41a267cbc9 100644 --- a/packages/perps-controller/src/index.ts +++ b/packages/perps-controller/src/index.ts @@ -470,6 +470,7 @@ export { } from './utils'; export { calculateOpenInterestUSD, + isMarketTradable, transformMarketData, formatChange, } from './utils'; diff --git a/packages/perps-controller/src/providers/HyperLiquidProvider.ts b/packages/perps-controller/src/providers/HyperLiquidProvider.ts index 95954e852c..89baaad63c 100644 --- a/packages/perps-controller/src/providers/HyperLiquidProvider.ts +++ b/packages/perps-controller/src/providers/HyperLiquidProvider.ts @@ -20,6 +20,7 @@ import { HIP3_ASSET_MARKET_TYPES, HIP3_FEE_CONFIG, HIP3_MARGIN_CONFIG, + HYPERLIQUID_CONFIG, HYPERLIQUID_WITHDRAWAL_MINUTES, REFERRAL_CONFIG, SPOT_ASSET_ID_OFFSET, @@ -407,11 +408,14 @@ export class HyperLiquidProvider implements PerpsProvider { readonly #builderAddressMainnet?: string; + readonly #priceDeviationLimit: number; + constructor(options: { isTestnet?: boolean; hip3Enabled?: boolean; allowlistMarkets?: string[]; blocklistMarkets?: string[]; + priceDeviationLimit?: number; useUnifiedAccount?: boolean; platformDependencies: PerpsPlatformDependencies; messenger: PerpsControllerMessengerBase; @@ -423,6 +427,8 @@ export class HyperLiquidProvider implements PerpsProvider { this.#messenger = options.messenger; this.#builderAddressTestnet = options.builderAddressTestnet; this.#builderAddressMainnet = options.builderAddressMainnet; + this.#priceDeviationLimit = + options.priceDeviationLimit ?? HYPERLIQUID_CONFIG.OraclePriceDeviationLimit; const isTestnet = options.isTestnet ?? false; // Dev-friendly defaults: Enable all markets by default for easier testing (discovery mode) @@ -457,6 +463,7 @@ export class HyperLiquidProvider implements PerpsProvider { [], // enabledDexs - will be populated after DEX discovery in buildAssetMapping this.#allowlistMarkets, this.#blocklistMarkets, + this.#priceDeviationLimit, ); // NOTE: Clients are NOT initialized here - they'll be initialized lazily diff --git a/packages/perps-controller/src/services/HyperLiquidSubscriptionService.ts b/packages/perps-controller/src/services/HyperLiquidSubscriptionService.ts index f0cd24a171..a7a613720b 100644 --- a/packages/perps-controller/src/services/HyperLiquidSubscriptionService.ts +++ b/packages/perps-controller/src/services/HyperLiquidSubscriptionService.ts @@ -17,6 +17,7 @@ import type { SpotStateWsEvent, } from '@nktkas/hyperliquid'; +import { HYPERLIQUID_CONFIG } from '../constants/hyperLiquidConfig'; import { TP_SL_CONFIG, PERPS_CONSTANTS, @@ -60,7 +61,10 @@ import { parseAssetName, } from '../utils/hyperLiquidAdapter'; import { processBboData } from '../utils/hyperLiquidOrderBookProcessor'; -import { calculateOpenInterestUSD } from '../utils/marketDataTransform'; +import { + calculateOpenInterestUSD, + isMarketTradable, +} from '../utils/marketDataTransform'; import type { HyperLiquidClientService } from './HyperLiquidClientService'; import type { HyperLiquidWalletService } from './HyperLiquidWalletService'; @@ -83,6 +87,9 @@ export class HyperLiquidSubscriptionService { #blocklistMarkets: string[]; // Market filtering (blocklist) + // Max market-vs-oracle price deviation before a market is reported untradable + readonly #priceDeviationLimit: number; + #discoveredDexNames: string[] = []; // DEX order for mapping webData3 perpDexStates indices // DEX discovery synchronization - allows subscriptions to wait for HIP-3 DEX discovery @@ -327,6 +334,7 @@ export class HyperLiquidSubscriptionService { enabledDexs?: string[], allowlistMarkets?: string[], blocklistMarkets?: string[], + priceDeviationLimit?: number, ) { this.#clientService = clientService; this.#walletService = walletService; @@ -336,6 +344,8 @@ export class HyperLiquidSubscriptionService { this.#discoveredDexNames = enabledDexs ?? []; this.#allowlistMarkets = allowlistMarkets ?? []; this.#blocklistMarkets = blocklistMarkets ?? []; + this.#priceDeviationLimit = + priceDeviationLimit ?? HYPERLIQUID_CONFIG.OraclePriceDeviationLimit; } /** @@ -2992,6 +3002,15 @@ export class HyperLiquidSubscriptionService { ? marketData?.openInterest : undefined, volume24h: hasMarketDataSubscribers ? marketData?.volume24h : undefined, + // Flag markets that are currently untradable because the mid price has drifted + // too far from the oracle price (HyperLiquid rejects such orders). Lets clients + // warn the user before they attempt an order that would fail. Defaults to tradable + // when the oracle price isn't yet cached. + isTradable: isMarketTradable({ + midPrice: currentPrice, + oraclePrice: marketData?.oraclePrice, + deviationLimit: this.#priceDeviationLimit, + }), }; return priceUpdate; diff --git a/packages/perps-controller/src/types/index.ts b/packages/perps-controller/src/types/index.ts index 24f8cbbdd4..a4c9fa33a5 100644 --- a/packages/perps-controller/src/types/index.ts +++ b/packages/perps-controller/src/types/index.ts @@ -664,6 +664,16 @@ export type PerpsControllerConfig = { */ fallbackHip3BlocklistMarkets?: string[]; + /** + * Override for the maximum allowed deviation of a market's price from its oracle + * (reference) price before it is reported as untradable (`PriceUpdate.isTradable`), + * as a decimal fraction (e.g. `0.95` = 95%). Protocol-agnostic: each provider applies + * its own default when omitted (HyperLiquid uses + * `HYPERLIQUID_CONFIG.OraclePriceDeviationLimit`, `0.95`). Lets a client tune the + * threshold without a package release. + */ + fallbackPriceDeviationLimit?: number; + /** * Per-provider credentials and configuration. * Nested by provider name so each provider's settings are self-contained @@ -710,6 +720,20 @@ export type PriceUpdate = { funding?: number; // Current funding rate openInterest?: number; // Open interest in USD volume24h?: number; // 24h trading volume in USD + /** + * Whether the market is currently tradable. + * + * Some markets — most often HIP-3 builder-deployed ones — become temporarily + * untradable when their market price drifts too far from the oracle price, in which + * case the protocol rejects orders (HyperLiquid: "Order price cannot be more than 95% + * away from the reference price"). Clients use this to proactively show a "trading + * unavailable" warning instead of letting the order fail on submission. + * + * This is computed per provider/protocol from that protocol's own rules; absence + * (`undefined`) means tradability is unknown and the market should be treated as + * tradable. + */ + isTradable?: boolean; providerId?: PerpsProviderType; // Multi-provider: price source (injected by aggregator) }; diff --git a/packages/perps-controller/src/utils/marketDataTransform.ts b/packages/perps-controller/src/utils/marketDataTransform.ts index 47bcffadfb..65dcb28dad 100644 --- a/packages/perps-controller/src/utils/marketDataTransform.ts +++ b/packages/perps-controller/src/utils/marketDataTransform.ts @@ -50,6 +50,58 @@ export function calculateOpenInterestUSD( return openInterestNum * priceNum; } +/** + * Determine whether a market is currently tradable based on how far its market + * (mid) price has drifted from the oracle (reference) price. + * + * HyperLiquid rejects orders when the order price is more than 95% away from the + * reference price ("Order price cannot be more than 95% away from the reference + * price"). This most often affects HIP-3 builder-deployed markets, which can become + * temporarily untradable when their mid price diverges far from the oracle price. + * Clients use this signal to proactively warn the user (e.g. a "trading unavailable" + * banner) instead of letting the order fail on submission. + * + * Note: the deviation limit is a HyperLiquid protocol rule. Other providers may have + * different rules and should compute tradability accordingly. + * + * @param params - The parameters for the tradability check. + * @param params.midPrice - Current market/mid price. + * @param params.oraclePrice - Current oracle/reference price. + * @param params.deviationLimit - Max allowed deviation as a decimal fraction + * (defaults to HyperLiquid's 0.95). A market is untradable when + * `abs(midPrice - oraclePrice) / oraclePrice > deviationLimit`. + * @returns `true` when the market is tradable (or when prices are unavailable, so the + * absence of data never blocks trading); `false` when the deviation exceeds the limit. + */ +export function isMarketTradable(params: { + midPrice: number | undefined; + oraclePrice: number | undefined; + deviationLimit?: number; +}): boolean { + const { + midPrice, + oraclePrice, + deviationLimit = HYPERLIQUID_CONFIG.OraclePriceDeviationLimit, + } = params; + + // Without usable prices we cannot assess deviation — default to tradable so missing + // data never blocks the user. A non-positive price means "no data" (e.g. the transient + // zero price emitted before the first real tick), not an untradable market. + if ( + midPrice === undefined || + oraclePrice === undefined || + isNaN(midPrice) || + isNaN(oraclePrice) || + midPrice <= 0 || + oraclePrice <= 0 + ) { + return true; + } + + const deviation = Math.abs(midPrice - oraclePrice) / oraclePrice; + return deviation <= deviationLimit; +} + /** * HyperLiquid-specific market data structure */ diff --git a/packages/perps-controller/tests/src/services/HyperLiquidSubscriptionService.market-data.test.ts b/packages/perps-controller/tests/src/services/HyperLiquidSubscriptionService.market-data.test.ts index d87a1380ce..7d86783bac 100644 --- a/packages/perps-controller/tests/src/services/HyperLiquidSubscriptionService.market-data.test.ts +++ b/packages/perps-controller/tests/src/services/HyperLiquidSubscriptionService.market-data.test.ts @@ -12,6 +12,7 @@ import type { HyperLiquidClientService } from '../../../src/services/HyperLiquid import { HyperLiquidSubscriptionService } from '../../../src/services/HyperLiquidSubscriptionService'; import type { HyperLiquidWalletService } from '../../../src/services/HyperLiquidWalletService'; import type { + PriceUpdate, SubscribeOrderBookParams, SubscribeOrderFillsParams, SubscribePositionsParams, @@ -2559,4 +2560,124 @@ describe('HyperLiquidSubscriptionService', () => { expect(result2).toBeNull(); }); }); + + describe('Market tradability (isTradable)', () => { + const getLastBtcUpdate = (mockCallback: jest.Mock) => { + const { calls } = mockCallback.mock; + const lastCall = calls[calls.length - 1][0]; + return lastCall.find((update: PriceUpdate) => update.symbol === 'BTC'); + }; + + it('marks a market tradable when the mid price is close to the oracle price', async () => { + // Default mock: mid (allMids) BTC = 50000, oraclePx = 50100 -> ~0.2% deviation + const mockCallback = jest.fn(); + + const unsubscribe = await service.subscribeToPrices({ + symbols: ['BTC'], + callback: mockCallback, + includeMarketData: true, + }); + + await jest.runAllTimersAsync(); + + expect(getLastBtcUpdate(mockCallback)).toEqual( + expect.objectContaining({ symbol: 'BTC', isTradable: true }), + ); + + unsubscribe(); + }); + + it('marks a market untradable when the mid price deviates more than 95% from the oracle price', async () => { + // mid (allMids) BTC = 50000, oraclePx = 100 -> deviation far beyond the 95% limit + mockSubscriptionClient.activeAssetCtx = jest.fn( + (params: any, callback: any) => { + setTimeout(() => { + callback({ + coin: params.coin, + ctx: { + prevDayPx: '49000', + funding: '0.01', + openInterest: '1000000', + dayNtlVlm: '50000000', + oraclePx: '100', + midPx: '50000', + }, + }); + }, 0); + return Promise.resolve({ + unsubscribe: jest.fn().mockResolvedValue(undefined), + }); + }, + ); + + const mockCallback = jest.fn(); + + const unsubscribe = await service.subscribeToPrices({ + symbols: ['BTC'], + callback: mockCallback, + includeMarketData: true, + }); + + await jest.runAllTimersAsync(); + + expect(getLastBtcUpdate(mockCallback)).toEqual( + expect.objectContaining({ symbol: 'BTC', isTradable: false }), + ); + + unsubscribe(); + }); + + it('honors an injected price deviation limit', async () => { + // mid (allMids) BTC = 50000, oraclePx = 40000 -> 25% deviation: tradable under the + // default 0.95 limit, but untradable under an injected 0.1 (10%) limit. + mockSubscriptionClient.activeAssetCtx = jest.fn( + (params: any, callback: any) => { + setTimeout(() => { + callback({ + coin: params.coin, + ctx: { + prevDayPx: '49000', + funding: '0.01', + openInterest: '1000000', + dayNtlVlm: '50000000', + oraclePx: '40000', + midPx: '50000', + }, + }); + }, 0); + return Promise.resolve({ + unsubscribe: jest.fn().mockResolvedValue(undefined), + }); + }, + ); + + const customService = new HyperLiquidSubscriptionService( + mockClientService, + mockWalletService, + mockDeps, + true, // hip3Enabled + [], // enabledDexs + [], // allowlistMarkets + [], // blocklistMarkets + 0.1, // priceDeviationLimit (10%) + ); + + const mockCallback = jest.fn(); + + const unsubscribe = await customService.subscribeToPrices({ + symbols: ['BTC'], + callback: mockCallback, + includeMarketData: true, + }); + + await jest.runAllTimersAsync(); + + expect(getLastBtcUpdate(mockCallback)).toEqual( + expect.objectContaining({ symbol: 'BTC', isTradable: false }), + ); + + unsubscribe(); + customService.clearAll(); + }); + }); }); diff --git a/packages/perps-controller/tests/src/utils/marketDataTransform.test.ts b/packages/perps-controller/tests/src/utils/marketDataTransform.test.ts new file mode 100644 index 0000000000..5299646d99 --- /dev/null +++ b/packages/perps-controller/tests/src/utils/marketDataTransform.test.ts @@ -0,0 +1,77 @@ +import { HYPERLIQUID_CONFIG } from '../../../src/constants/hyperLiquidConfig'; +import { isMarketTradable } from '../../../src/utils/marketDataTransform'; + +describe('marketDataTransform', () => { + describe('isMarketTradable', () => { + it('is tradable when mid and oracle prices are equal', () => { + expect( + isMarketTradable({ midPrice: 50000, oraclePrice: 50000 }), + ).toBe(true); + }); + + it('is tradable for small deviations well within the limit', () => { + // 0.2% deviation + expect( + isMarketTradable({ midPrice: 50100, oraclePrice: 50000 }), + ).toBe(true); + }); + + it('is tradable exactly at the deviation limit (inclusive boundary)', () => { + // 95% above the oracle price -> deviation === limit + expect( + isMarketTradable({ midPrice: 50000 * 1.95, oraclePrice: 50000 }), + ).toBe(true); + }); + + it('is untradable when the mid price is more than 95% above the oracle price', () => { + // 96% above the oracle price -> deviation > limit + expect( + isMarketTradable({ midPrice: 50000 * 1.96, oraclePrice: 50000 }), + ).toBe(false); + }); + + it('is untradable when the mid price is more than 95% below the oracle price', () => { + // Mid price near zero relative to oracle -> ~100% deviation + expect(isMarketTradable({ midPrice: 1, oraclePrice: 50000 })).toBe(false); + }); + + it('respects a custom deviation limit', () => { + // 20% deviation with a 10% limit -> untradable + expect( + isMarketTradable({ + midPrice: 120, + oraclePrice: 100, + deviationLimit: 0.1, + }), + ).toBe(false); + // Same deviation with a 50% limit -> tradable + expect( + isMarketTradable({ + midPrice: 120, + oraclePrice: 100, + deviationLimit: 0.5, + }), + ).toBe(true); + }); + + it('uses the HyperLiquid 0.95 default when no limit is provided', () => { + expect(HYPERLIQUID_CONFIG.OraclePriceDeviationLimit).toBe(0.95); + // Just over 95% -> untradable with the default limit + expect( + isMarketTradable({ midPrice: 1.96, oraclePrice: 1 }), + ).toBe(false); + }); + + it.each([ + ['mid price undefined', { midPrice: undefined, oraclePrice: 50000 }], + ['oracle price undefined', { midPrice: 50000, oraclePrice: undefined }], + ['mid price NaN', { midPrice: NaN, oraclePrice: 50000 }], + ['oracle price NaN', { midPrice: 50000, oraclePrice: NaN }], + ['mid price zero', { midPrice: 0, oraclePrice: 50000 }], + ['oracle price zero', { midPrice: 50000, oraclePrice: 0 }], + ['oracle price negative', { midPrice: 50000, oraclePrice: -1 }], + ])('defaults to tradable when %s', (_label, params) => { + expect(isMarketTradable(params)).toBe(true); + }); + }); +}); From ce0c39bf010432e3ee18b14ab199a8961c27e9a5 Mon Sep 17 00:00:00 2001 From: Arthur Breton Date: Fri, 19 Jun 2026 18:15:17 +0200 Subject: [PATCH 3/3] fix(perps): move #9205 changelog entry to Unreleased and apply oxfmt --- packages/perps-controller/CHANGELOG.md | 7 +++++-- .../src/providers/HyperLiquidProvider.ts | 3 ++- .../tests/src/utils/marketDataTransform.test.ts | 16 +++++++--------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/perps-controller/CHANGELOG.md b/packages/perps-controller/CHANGELOG.md index f7c0250e5b..81079a67b6 100644 --- a/packages/perps-controller/CHANGELOG.md +++ b/packages/perps-controller/CHANGELOG.md @@ -7,14 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [8.2.0] - ### Added - Surface per-market trading availability so clients can warn before placing an order that would be rejected ([#9205](https://github.com/MetaMask/core/pull/9205)) - Add an optional `isTradable` boolean to `PriceUpdate`. It is `false` when a market's mid price has drifted past the protocol's oracle-deviation limit (HyperLiquid rejects orders more than 95% away from the reference price, which most often affects HIP-3 markets); `undefined` means tradability is unknown and the market should be treated as tradable. - Add an optional, protocol-agnostic `fallbackPriceDeviationLimit` to `PerpsControllerConfig` so clients can tune the deviation threshold; each provider applies its own default when omitted. - Export the pure `isMarketTradable` helper and add `HYPERLIQUID_CONFIG.OraclePriceDeviationLimit` (`0.95`, the HyperLiquid default). + +## [8.2.0] + +### Added + - Add Perps Discovery analytics constants to `PERPS_EVENT_PROPERTY` and `PERPS_EVENT_VALUE` so mobile can import them from `@metamask/perps-controller` instead of maintaining a local mirror ([#9178](https://github.com/MetaMask/core/pull/9178)) - New `PERPS_EVENT_PROPERTY` keys: `SOURCE_SECTION`, `RESULT_COUNT`, `SECTION_NAME`, `SECTION_INDEX`, `SECTIONS_DISPLAYED`, `WATCHLIST_COUNT`, `WATCHLIST_MARKETS` - New `PERPS_EVENT_VALUE.SOURCE_SECTION` group: values for home sections (`positions`, `orders`, `watchlist`, `whats_happening`, `products`, `top_gainers`, `top_losers`, `crypto`, `commodity`, `stock`, `forex`), explore sections (`perps_movers`, `perps_crypto`, `perps_stocks_commodities`, `perps_markets`), and market-list sections (`all_markets`, `new`, `active_search`) diff --git a/packages/perps-controller/src/providers/HyperLiquidProvider.ts b/packages/perps-controller/src/providers/HyperLiquidProvider.ts index 283b23f0f0..6aee702685 100644 --- a/packages/perps-controller/src/providers/HyperLiquidProvider.ts +++ b/packages/perps-controller/src/providers/HyperLiquidProvider.ts @@ -429,7 +429,8 @@ export class HyperLiquidProvider implements PerpsProvider { this.#builderAddressTestnet = options.builderAddressTestnet; this.#builderAddressMainnet = options.builderAddressMainnet; this.#priceDeviationLimit = - options.priceDeviationLimit ?? HYPERLIQUID_CONFIG.OraclePriceDeviationLimit; + options.priceDeviationLimit ?? + HYPERLIQUID_CONFIG.OraclePriceDeviationLimit; const isTestnet = options.isTestnet ?? false; // Dev-friendly defaults: Enable all markets by default for easier testing (discovery mode) diff --git a/packages/perps-controller/tests/src/utils/marketDataTransform.test.ts b/packages/perps-controller/tests/src/utils/marketDataTransform.test.ts index 0cdcb4d6e6..cf5a8fea6e 100644 --- a/packages/perps-controller/tests/src/utils/marketDataTransform.test.ts +++ b/packages/perps-controller/tests/src/utils/marketDataTransform.test.ts @@ -36,16 +36,16 @@ function makeUniverseEntry(name: string): PerpsUniverse { describe('marketDataTransform', () => { describe('isMarketTradable', () => { it('is tradable when mid and oracle prices are equal', () => { - expect( - isMarketTradable({ midPrice: 50000, oraclePrice: 50000 }), - ).toBe(true); + expect(isMarketTradable({ midPrice: 50000, oraclePrice: 50000 })).toBe( + true, + ); }); it('is tradable for small deviations well within the limit', () => { // 0.2% deviation - expect( - isMarketTradable({ midPrice: 50100, oraclePrice: 50000 }), - ).toBe(true); + expect(isMarketTradable({ midPrice: 50100, oraclePrice: 50000 })).toBe( + true, + ); }); it('is tradable exactly at the deviation limit (inclusive boundary)', () => { @@ -89,9 +89,7 @@ describe('marketDataTransform', () => { it('uses the HyperLiquid 0.95 default when no limit is provided', () => { expect(HYPERLIQUID_CONFIG.OraclePriceDeviationLimit).toBe(0.95); // Just over 95% -> untradable with the default limit - expect( - isMarketTradable({ midPrice: 1.96, oraclePrice: 1 }), - ).toBe(false); + expect(isMarketTradable({ midPrice: 1.96, oraclePrice: 1 })).toBe(false); }); it.each([