From 13a834906a156953efc03202db0f9da5252768e0 Mon Sep 17 00:00:00 2001 From: geositta Date: Wed, 26 Aug 2026 23:46:13 -0500 Subject: [PATCH 1/5] fix: for historical orders reuse adaptOrderFromSDK, exposing trigger prices/types --- .../src/providers/HyperLiquidProvider.ts | 24 ++++----- .../HyperLiquidProvider.history.test.ts | 51 ++++++++++++++++++- 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/packages/perps-controller/src/providers/HyperLiquidProvider.ts b/packages/perps-controller/src/providers/HyperLiquidProvider.ts index 909aef544ba..da58302aaee 100644 --- a/packages/perps-controller/src/providers/HyperLiquidProvider.ts +++ b/packages/perps-controller/src/providers/HyperLiquidProvider.ts @@ -10493,8 +10493,6 @@ export class HyperLiquidProvider implements PerpsProvider { // Transform HyperLiquid orders to abstract Order type const orders: Order[] = (rawOrders || []).map((rawOrder) => { const { order, status, statusTimestamp } = rawOrder; - // Normalize side: HyperLiquid uses 'A' (Ask/Sell) and 'B' (Bid/Buy) - const normalizedSide = order.side === 'B' ? 'buy' : 'sell'; // Normalize status let normalizedStatus: Order['status']; @@ -10534,24 +10532,26 @@ export class HyperLiquidProvider implements PerpsProvider { const currentSize = parseFloat(order.sz); const filledSize = originalSize - currentSize; - return { - orderId: order.oid?.toString() || '', - symbol: order.coin, - side: normalizedSide, - orderType: order.orderType?.toLowerCase().includes('limit') + const adaptedOrder = adaptOrderFromSDK(order, undefined); + let historicalOrderType = adaptedOrder.orderType; + if (!adaptedOrder.triggerOrderType) { + historicalOrderType = order.orderType + ?.toLowerCase() + .includes('limit') ? 'limit' - : 'market', + : 'market'; + } + + return { + ...adaptedOrder, + orderType: historicalOrderType, size: order.sz, originalSize: order.origSz || order.sz, - price: order.limitPx || '0', filledSize: filledSize.toString(), remainingSize: currentSize.toString(), status: normalizedStatus, timestamp: statusTimestamp, lastUpdated: statusTimestamp, - detailedOrderType: order.orderType, // Full order type from exchange (e.g., 'Take Profit Limit', 'Stop Market') - isTrigger: order.isTrigger, - reduceOnly: order.reduceOnly, }; }); diff --git a/packages/perps-controller/tests/src/providers/HyperLiquidProvider.history.test.ts b/packages/perps-controller/tests/src/providers/HyperLiquidProvider.history.test.ts index 3585f6fc477..3bc5f736ce6 100644 --- a/packages/perps-controller/tests/src/providers/HyperLiquidProvider.history.test.ts +++ b/packages/perps-controller/tests/src/providers/HyperLiquidProvider.history.test.ts @@ -559,6 +559,7 @@ describe('HyperLiquidProvider', () => { sz: '0.0', origSz: '2.0', limitPx: '3500', + triggerPx: '3450', orderType: 'Take Profit Limit', reduceOnly: true, isTrigger: true, @@ -574,6 +575,7 @@ describe('HyperLiquidProvider', () => { sz: '0.1', origSz: '0.1', limitPx: '45000', + triggerPx: '45500', orderType: 'Stop Market', reduceOnly: true, isTrigger: true, @@ -581,6 +583,22 @@ describe('HyperLiquidProvider', () => { status: 'triggered', statusTimestamp: 1640995400000, }, + { + order: { + oid: 126, + coin: 'ETH', + side: 'B', + sz: '0.0', + origSz: '1.0', + limitPx: '3600', + triggerPx: '', + orderType: 'Market', + reduceOnly: false, + isTrigger: false, + }, + status: 'filled', + statusTimestamp: 1640995500000, + }, ]; mockClientService.getInfoClient = jest.fn().mockReturnValue({ maxBuilderFee: jest.fn().mockResolvedValue(1), @@ -596,7 +614,7 @@ describe('HyperLiquidProvider', () => { const result = await provider.getOrders(); - expect(result).toHaveLength(3); + expect(result).toHaveLength(4); // Check first order - regular limit order (not closing) expect(result[0]).toMatchObject({ @@ -622,7 +640,13 @@ describe('HyperLiquidProvider', () => { size: '0.0', originalSize: '2.0', price: '3500', + triggerPrice: '3450', + triggerOrderType: 'take_profit_limit', + filledSize: '2', + remainingSize: '0', status: 'filled', + timestamp: 1640995300000, + lastUpdated: 1640995300000, detailedOrderType: 'Take Profit Limit', reduceOnly: true, isTrigger: true, @@ -637,11 +661,36 @@ describe('HyperLiquidProvider', () => { size: '0.1', originalSize: '0.1', price: '45000', + triggerPrice: '45500', + triggerOrderType: 'stop_market', + filledSize: '0', + remainingSize: '0.1', status: 'triggered', + timestamp: 1640995400000, + lastUpdated: 1640995400000, detailedOrderType: 'Stop Market', reduceOnly: true, isTrigger: true, }); + + // Check fourth order - regular market order with a slippage-cap price + expect(result[3]).toMatchObject({ + orderId: '126', + symbol: 'ETH', + side: 'buy', + orderType: 'market', + size: '0.0', + originalSize: '1.0', + price: '3600', + filledSize: '1', + remainingSize: '0', + status: 'filled', + timestamp: 1640995500000, + lastUpdated: 1640995500000, + detailedOrderType: 'Market', + reduceOnly: false, + isTrigger: false, + }); }); it('properly transform getOpenOrders with reduceOnly and isTrigger fields', async () => { From 5738dfee5e5d20e60a200da9e74a8877cc93e201 Mon Sep 17 00:00:00 2001 From: geositta Date: Wed, 26 Aug 2026 23:54:49 -0500 Subject: [PATCH 2/5] chore(perps-controller): format historical order mapping Co-authored-by: Cursor --- .../perps-controller/src/providers/HyperLiquidProvider.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/perps-controller/src/providers/HyperLiquidProvider.ts b/packages/perps-controller/src/providers/HyperLiquidProvider.ts index da58302aaee..e732bdfbcfe 100644 --- a/packages/perps-controller/src/providers/HyperLiquidProvider.ts +++ b/packages/perps-controller/src/providers/HyperLiquidProvider.ts @@ -10535,9 +10535,7 @@ export class HyperLiquidProvider implements PerpsProvider { const adaptedOrder = adaptOrderFromSDK(order, undefined); let historicalOrderType = adaptedOrder.orderType; if (!adaptedOrder.triggerOrderType) { - historicalOrderType = order.orderType - ?.toLowerCase() - .includes('limit') + historicalOrderType = order.orderType?.toLowerCase().includes('limit') ? 'limit' : 'market'; } From a9f85e69f03181b628cc90903e83429507846d22 Mon Sep 17 00:00:00 2001 From: geositta Date: Thu, 27 Aug 2026 00:02:29 -0500 Subject: [PATCH 3/5] docs(perps-controller): document historical trigger fix Co-authored-by: Cursor --- packages/perps-controller/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/perps-controller/CHANGELOG.md b/packages/perps-controller/CHANGELOG.md index 7147ea5bc3b..3d08bf7aef0 100644 --- a/packages/perps-controller/CHANGELOG.md +++ b/packages/perps-controller/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Preserve trigger prices and normalized trigger order types in HyperLiquid historical orders while retaining their lifecycle and execution semantics ([#9982](https://github.com/MetaMask/core/pull/9982)). - Classify `xyz:CBRS` and `xyz:SPCX` as stocks in the Hyperliquid fallback market map ([#9988](https://github.com/MetaMask/core/pull/9988)) ## [13.0.0] From 4832ab1a17871c0926aacaad4c6e2ddc0b70a21a Mon Sep 17 00:00:00 2001 From: geositta Date: Thu, 27 Aug 2026 10:06:50 -0500 Subject: [PATCH 4/5] fix(perps-controller): preserve malformed order history Harden shared order adaptation so a malformed exchange row cannot erase the user's full activity history, while simplifying historical field normalization. Co-authored-by: Cursor --- .../src/providers/HyperLiquidProvider.ts | 23 +++------ .../src/utils/hyperLiquidAdapter.ts | 4 +- .../HyperLiquidProvider.history.test.ts | 49 +++++++++++++++++++ ...hyperLiquidAdapter.advanced-orders.test.ts | 17 +++++++ 4 files changed, 76 insertions(+), 17 deletions(-) diff --git a/packages/perps-controller/src/providers/HyperLiquidProvider.ts b/packages/perps-controller/src/providers/HyperLiquidProvider.ts index e732bdfbcfe..6b43ed781ef 100644 --- a/packages/perps-controller/src/providers/HyperLiquidProvider.ts +++ b/packages/perps-controller/src/providers/HyperLiquidProvider.ts @@ -10527,26 +10527,19 @@ export class HyperLiquidProvider implements PerpsProvider { normalizedStatus = 'queued'; } - // Calculate filled and remaining size - const originalSize = parseFloat(order.origSz || order.sz); - const currentSize = parseFloat(order.sz); - const filledSize = originalSize - currentSize; - const adaptedOrder = adaptOrderFromSDK(order, undefined); - let historicalOrderType = adaptedOrder.orderType; - if (!adaptedOrder.triggerOrderType) { - historicalOrderType = order.orderType?.toLowerCase().includes('limit') - ? 'limit' - : 'market'; - } + // limitPx is also populated as a slippage cap for market orders, so the + // exchange's detailed type is the reliable execution-mode source. + const historicalOrderType = order.orderType + ?.toLowerCase() + .includes('limit') + ? 'limit' + : 'market'; return { ...adaptedOrder, orderType: historicalOrderType, - size: order.sz, - originalSize: order.origSz || order.sz, - filledSize: filledSize.toString(), - remainingSize: currentSize.toString(), + remainingSize: parseFloat(order.sz).toString(), status: normalizedStatus, timestamp: statusTimestamp, lastUpdated: statusTimestamp, diff --git a/packages/perps-controller/src/utils/hyperLiquidAdapter.ts b/packages/perps-controller/src/utils/hyperLiquidAdapter.ts index a6008bea6c4..eb803b6b370 100644 --- a/packages/perps-controller/src/utils/hyperLiquidAdapter.ts +++ b/packages/perps-controller/src/utils/hyperLiquidAdapter.ts @@ -277,7 +277,7 @@ export function adaptOrderFromSDK( ); // Extract basic fields with appropriate conversions - const orderId = rawOrder.oid.toString(); + const orderId = rawOrder.oid?.toString() || ''; const symbol = rawOrder.coin; const side: 'buy' | 'sell' = rawOrder.side === 'B' ? 'buy' : 'sell'; const detailedOrderType = rawOrder.orderType; @@ -293,7 +293,7 @@ export function adaptOrderFromSDK( // source for how the order actually executes. orderType = getTriggerExecution(triggerOrderType); } else if ( - detailedOrderType.toLowerCase().includes('limit') || + detailedOrderType?.toLowerCase().includes('limit') || rawOrder.limitPx ) { orderType = 'limit'; diff --git a/packages/perps-controller/tests/src/providers/HyperLiquidProvider.history.test.ts b/packages/perps-controller/tests/src/providers/HyperLiquidProvider.history.test.ts index 3bc5f736ce6..abf8e15b7e9 100644 --- a/packages/perps-controller/tests/src/providers/HyperLiquidProvider.history.test.ts +++ b/packages/perps-controller/tests/src/providers/HyperLiquidProvider.history.test.ts @@ -534,6 +534,53 @@ describe('HyperLiquidProvider', () => { expect(result).toEqual([]); }); + it('preserves history when an order is missing runtime-required fields', async () => { + mockClientService.fetchHistoricalOrders = jest.fn().mockResolvedValue([ + { + order: { + oid: 123, + coin: 'BTC', + side: 'A', + sz: '0.5', + origSz: '1.0', + limitPx: '50000', + orderType: 'Limit', + reduceOnly: false, + isTrigger: false, + }, + status: 'filled', + statusTimestamp: 1640995200000, + }, + { + order: { + oid: undefined, + coin: 'ETH', + side: 'B', + sz: '0.1', + origSz: '0.1', + limitPx: '', + orderType: undefined, + reduceOnly: false, + isTrigger: false, + }, + status: 'open', + statusTimestamp: 1640995300000, + }, + ]); + + const result = await provider.getOrders(); + + expect(result).toHaveLength(2); + expect(result[0]).toMatchObject({ + orderId: '123', + orderType: 'limit', + }); + expect(result[1]).toMatchObject({ + orderId: '', + orderType: 'market', + }); + }); + it('properly transform getOrders with reduceOnly and isTrigger fields', async () => { const historicalOrdersData = [ { @@ -691,6 +738,8 @@ describe('HyperLiquidProvider', () => { reduceOnly: false, isTrigger: false, }); + expect(result[3].triggerPrice).toBeUndefined(); + expect(result[3].triggerOrderType).toBeUndefined(); }); it('properly transform getOpenOrders with reduceOnly and isTrigger fields', async () => { diff --git a/packages/perps-controller/tests/src/utils/hyperLiquidAdapter.advanced-orders.test.ts b/packages/perps-controller/tests/src/utils/hyperLiquidAdapter.advanced-orders.test.ts index 8ddf4c9feaa..9b3ed993cfd 100644 --- a/packages/perps-controller/tests/src/utils/hyperLiquidAdapter.advanced-orders.test.ts +++ b/packages/perps-controller/tests/src/utils/hyperLiquidAdapter.advanced-orders.test.ts @@ -117,6 +117,23 @@ describe('hyperLiquidAdapter - advanced order types', () => { expect(result.triggerOrderType).toBeUndefined(); }); + + it('tolerates runtime orders without an id or detailed type', () => { + const malformedOrder = buildFrontendOrder({ + oid: undefined, + orderType: undefined, + limitPx: '', + } as unknown as Partial); + + const result = adaptOrderFromSDK(malformedOrder); + + expect(result).toMatchObject({ + orderId: '', + orderType: 'market', + }); + expect(result.detailedOrderType).toBeUndefined(); + expect(result.triggerOrderType).toBeUndefined(); + }); }); describe('adaptOrderToSDK', () => { From cdd11cd0639a4f9fd143d2fa546f159f371b48c1 Mon Sep 17 00:00:00 2001 From: geositta Date: Thu, 27 Aug 2026 21:42:09 -0500 Subject: [PATCH 5/5] fix: cover all six HyperLiquid order types with a typed map --- .../src/providers/HyperLiquidProvider.ts | 20 ++++++++--- .../HyperLiquidProvider.history.test.ts | 35 +++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/packages/perps-controller/src/providers/HyperLiquidProvider.ts b/packages/perps-controller/src/providers/HyperLiquidProvider.ts index 6b43ed781ef..82b58b0e75f 100644 --- a/packages/perps-controller/src/providers/HyperLiquidProvider.ts +++ b/packages/perps-controller/src/providers/HyperLiquidProvider.ts @@ -32,6 +32,7 @@ import { USDC_DECIMALS, USDC_SYMBOL, } from '../constants/hyperLiquidConfig.js'; +import { DETAILED_ORDER_TYPES } from '../constants/orderTypes.js'; import { CHASE_ORDER_CONFIG, CHASE_ORDER_STATUS, @@ -59,6 +60,7 @@ import { } from '../services/TradingReadinessCache.js'; import type { FrontendOrder, + OrderType as HyperLiquidOrderType, SDKOrderParams, MetaResponse, PerpsAssetCtx, @@ -224,6 +226,15 @@ import { import { parseBoundedNonNegativeDecimal } from '../utils/stringParseUtils.js'; // getStreamManagerInstance removed: use this.#deps.streamManager instead +const HISTORICAL_ORDER_TYPE_BY_DETAILED_TYPE = { + [DETAILED_ORDER_TYPES.LIMIT]: 'limit', + [DETAILED_ORDER_TYPES.MARKET]: 'market', + [DETAILED_ORDER_TYPES.STOP_LIMIT]: 'limit', + [DETAILED_ORDER_TYPES.STOP_MARKET]: 'market', + [DETAILED_ORDER_TYPES.TAKE_PROFIT_LIMIT]: 'limit', + [DETAILED_ORDER_TYPES.TAKE_PROFIT_MARKET]: 'market', +} as const satisfies Record; + /** * Type guard to check if a status is an object (not a string literal like "waitingForFill") * The SDK returns status as a union of object types and string literals. @@ -10530,10 +10541,11 @@ export class HyperLiquidProvider implements PerpsProvider { const adaptedOrder = adaptOrderFromSDK(order, undefined); // limitPx is also populated as a slippage cap for market orders, so the // exchange's detailed type is the reliable execution-mode source. - const historicalOrderType = order.orderType - ?.toLowerCase() - .includes('limit') - ? 'limit' + const historicalOrderType = hasProperty( + HISTORICAL_ORDER_TYPE_BY_DETAILED_TYPE, + order.orderType, + ) + ? HISTORICAL_ORDER_TYPE_BY_DETAILED_TYPE[order.orderType] : 'market'; return { diff --git a/packages/perps-controller/tests/src/providers/HyperLiquidProvider.history.test.ts b/packages/perps-controller/tests/src/providers/HyperLiquidProvider.history.test.ts index abf8e15b7e9..67f8bfe7bc9 100644 --- a/packages/perps-controller/tests/src/providers/HyperLiquidProvider.history.test.ts +++ b/packages/perps-controller/tests/src/providers/HyperLiquidProvider.history.test.ts @@ -581,6 +581,41 @@ describe('HyperLiquidProvider', () => { }); }); + it.each([ + ['Limit', 'limit'], + ['Market', 'market'], + ['Stop Limit', 'limit'], + ['Stop Market', 'market'], + ['Take Profit Limit', 'limit'], + ['Take Profit Market', 'market'], + ['Unexpected Limit', 'market'], + ])( + 'maps the exact historical order type %s to %s', + async (orderType, expected) => { + mockClientService.fetchHistoricalOrders = jest.fn().mockResolvedValue([ + { + order: { + oid: 123, + coin: 'BTC', + side: 'B', + sz: '0.1', + origSz: '0.1', + limitPx: '50000', + orderType, + reduceOnly: false, + isTrigger: false, + }, + status: 'open', + statusTimestamp: 1640995200000, + }, + ]); + + const result = await provider.getOrders(); + + expect(result[0].orderType).toBe(expected); + }, + ); + it('properly transform getOrders with reduceOnly and isTrigger fields', async () => { const historicalOrdersData = [ {