Skip to content
Draft
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
4 changes: 4 additions & 0 deletions packages/perps-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 @@ -586,6 +586,7 @@ export {
buildTriggerOrderType,
buildPositionTriggerOrderFromOrder,
computeScalePriceLadder,
normalizeHyperLiquidScalePriceLadder,
computeChaseQuotePrice,
getPriceTick,
splitScaleSizes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,10 @@ import {
calculateFinalPositionSize,
calculateOrderPriceAndSize,
computeChaseQuotePrice,
computeScalePriceLadder,
floorToSizeDecimals,
formatPartialTpslSize,
getPriceTick,
normalizeHyperLiquidScalePriceLadder,
splitScaleSizes,
validateOrderPrecision,
} from '../utils/orderCalculations.js';
Expand Down Expand Up @@ -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);
Expand Down
23 changes: 23 additions & 0 deletions packages/perps-controller/src/utils/orderCalculations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
computeChaseQuotePrice,
computeScalePriceLadder,
getPriceTick,
normalizeHyperLiquidScalePriceLadder,
splitScaleSizes,
} from '../../../src/utils/orderCalculations.js';

Expand Down Expand Up @@ -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(
Expand Down