From d08898370052d22e7db41745955807e9493565c6 Mon Sep 17 00:00:00 2001 From: Arthur Breton Date: Mon, 31 Aug 2026 17:58:36 +0800 Subject: [PATCH] feat(perps): export normalized Scale price ladder --- packages/perps-controller/CHANGELOG.md | 4 ++ packages/perps-controller/src/index.ts | 1 + .../src/providers/HyperLiquidProvider.ts | 7 +-- .../src/utils/orderCalculations.ts | 23 ++++++++ .../orderCalculations.scale-ladder.test.ts | 54 +++++++++++++++++++ 5 files changed, 86 insertions(+), 3 deletions(-) diff --git a/packages/perps-controller/CHANGELOG.md b/packages/perps-controller/CHANGELOG.md index bda6d6ae23..337e0b7ba9 100644 --- a/packages/perps-controller/CHANGELOG.md +++ b/packages/perps-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `normalizeHyperLiquidScalePriceLadder` for shared Scale price normalization ([#10021](https://github.com/MetaMask/core/pull/10021)) + ## [15.0.0] ### Added diff --git a/packages/perps-controller/src/index.ts b/packages/perps-controller/src/index.ts index eed9c8922e..6f729e5f12 100644 --- a/packages/perps-controller/src/index.ts +++ b/packages/perps-controller/src/index.ts @@ -586,6 +586,7 @@ export { buildTriggerOrderType, buildPositionTriggerOrderFromOrder, computeScalePriceLadder, + normalizeHyperLiquidScalePriceLadder, computeChaseQuotePrice, getPriceTick, splitScaleSizes, diff --git a/packages/perps-controller/src/providers/HyperLiquidProvider.ts b/packages/perps-controller/src/providers/HyperLiquidProvider.ts index f19a007ca4..fca2da9f00 100644 --- a/packages/perps-controller/src/providers/HyperLiquidProvider.ts +++ b/packages/perps-controller/src/providers/HyperLiquidProvider.ts @@ -210,10 +210,10 @@ import { calculateFinalPositionSize, calculateOrderPriceAndSize, computeChaseQuotePrice, - computeScalePriceLadder, floorToSizeDecimals, formatPartialTpslSize, getPriceTick, + normalizeHyperLiquidScalePriceLadder, splitScaleSizes, validateOrderPrecision, } from '../utils/orderCalculations.js'; @@ -5826,11 +5826,12 @@ export class HyperLiquidProvider implements PerpsProvider { throw new Error(PERPS_ERROR_CODES.ORDER_SCALE_COUNT_INVALID); } - const prices = computeScalePriceLadder({ + const prices = normalizeHyperLiquidScalePriceLadder({ minPrice: parseFloat(scaleMinPrice), maxPrice: parseFloat(scaleMaxPrice), count: scaleNumOrders, - }).map((price) => formatHyperLiquidPrice({ price, szDecimals })); + szDecimals, + }); if (new Set(prices).size !== prices.length) { throw new Error(PERPS_ERROR_CODES.ORDER_SCALE_RANGE_INVALID); diff --git a/packages/perps-controller/src/utils/orderCalculations.ts b/packages/perps-controller/src/utils/orderCalculations.ts index 11cbb9f97d..e0286275ed 100644 --- a/packages/perps-controller/src/utils/orderCalculations.ts +++ b/packages/perps-controller/src/utils/orderCalculations.ts @@ -432,6 +432,29 @@ export function computeScalePriceLadder(params: { ); } +/** + * Build the exact price strings a HyperLiquid Scale order will receive. + * + * @param params - Ladder and asset precision parameters. + * @param params.minPrice - Lowest price in the ladder. + * @param params.maxPrice - Highest price in the ladder. + * @param params.count - Number of rungs. + * @param params.szDecimals - Asset size precision used for price formatting. + * @returns The normalized rung prices, ascending. + */ +export function normalizeHyperLiquidScalePriceLadder(params: { + minPrice: number; + maxPrice: number; + count: number; + szDecimals: number; +}): string[] { + const { szDecimals, ...ladderParams } = params; + + return computeScalePriceLadder(ladderParams).map((price) => + formatHyperLiquidPrice({ price, szDecimals }), + ); +} + /** * Split a scale placement's total size across its ladder rungs. * diff --git a/packages/perps-controller/tests/src/utils/orderCalculations.scale-ladder.test.ts b/packages/perps-controller/tests/src/utils/orderCalculations.scale-ladder.test.ts index 6442c97770..981b85910d 100644 --- a/packages/perps-controller/tests/src/utils/orderCalculations.scale-ladder.test.ts +++ b/packages/perps-controller/tests/src/utils/orderCalculations.scale-ladder.test.ts @@ -3,6 +3,7 @@ import { computeChaseQuotePrice, computeScalePriceLadder, getPriceTick, + normalizeHyperLiquidScalePriceLadder, splitScaleSizes, } from '../../../src/utils/orderCalculations.js'; @@ -54,6 +55,59 @@ describe('orderCalculations - scale ladder', () => { }); }); + describe('normalizeHyperLiquidScalePriceLadder', () => { + it('formats every rung for HyperLiquid', () => { + expect( + normalizeHyperLiquidScalePriceLadder({ + minPrice: 100, + maxPrice: 200, + count: 3, + szDecimals: 3, + }), + ).toStrictEqual(['100', '150', '200']); + }); + + it('applies the asset precision and significant-figure limits', () => { + expect( + normalizeHyperLiquidScalePriceLadder({ + minPrice: 1234.567, + maxPrice: 1234.767, + count: 3, + szDecimals: 3, + }), + ).toStrictEqual(['1234.6', '1234.7', '1234.8']); + }); + + it('preserves duplicate formatted rungs for callers to reject', () => { + const prices = normalizeHyperLiquidScalePriceLadder({ + minPrice: 100.123456, + maxPrice: 100.123457, + count: 3, + szDecimals: 3, + }); + + expect(prices).toStrictEqual(['100.12', '100.12', '100.12']); + expect(new Set(prices).size).toBe(1); + }); + + it.each([ + [ + 'an invalid count', + { minPrice: 100, maxPrice: 200, count: 1, szDecimals: 3 }, + PERPS_ERROR_CODES.ORDER_SCALE_COUNT_INVALID, + ], + [ + 'an invalid range', + { minPrice: 200, maxPrice: 100, count: 3, szDecimals: 3 }, + PERPS_ERROR_CODES.ORDER_SCALE_RANGE_INVALID, + ], + ])('propagates %s', (_label, params, errorCode) => { + expect(() => normalizeHyperLiquidScalePriceLadder(params)).toThrow( + errorCode, + ); + }); + }); + describe('splitScaleSizes', () => { it('splits a size that divides evenly', () => { expect(