@metamask-previews/perps-controller 10.0.0-preview-a42e8d0d2 → 10.0.0-preview-5a03e1b92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/constants/eventNames.cjs +6 -0
  3. package/dist/constants/eventNames.cjs.map +1 -1
  4. package/dist/constants/eventNames.d.cts +4 -0
  5. package/dist/constants/eventNames.d.cts.map +1 -1
  6. package/dist/constants/eventNames.d.mts +4 -0
  7. package/dist/constants/eventNames.d.mts.map +1 -1
  8. package/dist/constants/eventNames.mjs +6 -0
  9. package/dist/constants/eventNames.mjs.map +1 -1
  10. package/dist/index.cjs +86 -74
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +3 -1
  13. package/dist/index.d.cts.map +1 -1
  14. package/dist/index.d.mts +3 -1
  15. package/dist/index.d.mts.map +1 -1
  16. package/dist/index.mjs +2 -0
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/perpsErrorCodes.cjs +16 -0
  19. package/dist/perpsErrorCodes.cjs.map +1 -1
  20. package/dist/perpsErrorCodes.d.cts +12 -0
  21. package/dist/perpsErrorCodes.d.cts.map +1 -1
  22. package/dist/perpsErrorCodes.d.mts +12 -0
  23. package/dist/perpsErrorCodes.d.mts.map +1 -1
  24. package/dist/perpsErrorCodes.mjs +16 -0
  25. package/dist/perpsErrorCodes.mjs.map +1 -1
  26. package/dist/providers/HyperLiquidProvider.cjs +674 -77
  27. package/dist/providers/HyperLiquidProvider.cjs.map +1 -1
  28. package/dist/providers/HyperLiquidProvider.d.cts +13 -0
  29. package/dist/providers/HyperLiquidProvider.d.cts.map +1 -1
  30. package/dist/providers/HyperLiquidProvider.d.mts +13 -0
  31. package/dist/providers/HyperLiquidProvider.d.mts.map +1 -1
  32. package/dist/providers/HyperLiquidProvider.mjs +676 -79
  33. package/dist/providers/HyperLiquidProvider.mjs.map +1 -1
  34. package/dist/selectors.cjs.map +1 -1
  35. package/dist/selectors.d.cts +17 -17
  36. package/dist/selectors.d.cts.map +1 -1
  37. package/dist/selectors.d.mts +17 -17
  38. package/dist/selectors.d.mts.map +1 -1
  39. package/dist/selectors.mjs.map +1 -1
  40. package/dist/services/HyperLiquidSubscriptionService.cjs +121 -11
  41. package/dist/services/HyperLiquidSubscriptionService.cjs.map +1 -1
  42. package/dist/services/HyperLiquidSubscriptionService.d.cts +21 -0
  43. package/dist/services/HyperLiquidSubscriptionService.d.cts.map +1 -1
  44. package/dist/services/HyperLiquidSubscriptionService.d.mts +21 -0
  45. package/dist/services/HyperLiquidSubscriptionService.d.mts.map +1 -1
  46. package/dist/services/HyperLiquidSubscriptionService.mjs +121 -11
  47. package/dist/services/HyperLiquidSubscriptionService.mjs.map +1 -1
  48. package/dist/services/TradingService.cjs +6 -2
  49. package/dist/services/TradingService.cjs.map +1 -1
  50. package/dist/services/TradingService.d.cts.map +1 -1
  51. package/dist/services/TradingService.d.mts.map +1 -1
  52. package/dist/services/TradingService.mjs +6 -2
  53. package/dist/services/TradingService.mjs.map +1 -1
  54. package/dist/types/index.cjs.map +1 -1
  55. package/dist/types/index.d.cts +69 -4
  56. package/dist/types/index.d.cts.map +1 -1
  57. package/dist/types/index.d.mts +69 -4
  58. package/dist/types/index.d.mts.map +1 -1
  59. package/dist/types/index.mjs.map +1 -1
  60. package/dist/types/perps-types.cjs.map +1 -1
  61. package/dist/types/perps-types.d.cts +35 -1
  62. package/dist/types/perps-types.d.cts.map +1 -1
  63. package/dist/types/perps-types.d.mts +35 -1
  64. package/dist/types/perps-types.d.mts.map +1 -1
  65. package/dist/types/perps-types.mjs.map +1 -1
  66. package/dist/utils/hyperLiquidAdapter.cjs +168 -10
  67. package/dist/utils/hyperLiquidAdapter.cjs.map +1 -1
  68. package/dist/utils/hyperLiquidAdapter.d.cts +35 -1
  69. package/dist/utils/hyperLiquidAdapter.d.cts.map +1 -1
  70. package/dist/utils/hyperLiquidAdapter.d.mts +35 -1
  71. package/dist/utils/hyperLiquidAdapter.d.mts.map +1 -1
  72. package/dist/utils/hyperLiquidAdapter.mjs +166 -11
  73. package/dist/utils/hyperLiquidAdapter.mjs.map +1 -1
  74. package/dist/utils/hyperLiquidValidation.cjs +160 -5
  75. package/dist/utils/hyperLiquidValidation.cjs.map +1 -1
  76. package/dist/utils/hyperLiquidValidation.d.cts +23 -4
  77. package/dist/utils/hyperLiquidValidation.d.cts.map +1 -1
  78. package/dist/utils/hyperLiquidValidation.d.mts +23 -4
  79. package/dist/utils/hyperLiquidValidation.d.mts.map +1 -1
  80. package/dist/utils/hyperLiquidValidation.mjs +160 -5
  81. package/dist/utils/hyperLiquidValidation.mjs.map +1 -1
  82. package/dist/utils/index.cjs +5 -1
  83. package/dist/utils/index.cjs.map +1 -1
  84. package/dist/utils/index.d.cts +2 -1
  85. package/dist/utils/index.d.cts.map +1 -1
  86. package/dist/utils/index.d.mts +2 -1
  87. package/dist/utils/index.d.mts.map +1 -1
  88. package/dist/utils/index.mjs +2 -1
  89. package/dist/utils/index.mjs.map +1 -1
  90. package/dist/utils/orderCalculations.cjs +363 -37
  91. package/dist/utils/orderCalculations.cjs.map +1 -1
  92. package/dist/utils/orderCalculations.d.cts +87 -2
  93. package/dist/utils/orderCalculations.d.cts.map +1 -1
  94. package/dist/utils/orderCalculations.d.mts +87 -2
  95. package/dist/utils/orderCalculations.d.mts.map +1 -1
  96. package/dist/utils/orderCalculations.mjs +359 -36
  97. package/dist/utils/orderCalculations.mjs.map +1 -1
  98. package/dist/utils/orderTypes.cjs +222 -0
  99. package/dist/utils/orderTypes.cjs.map +1 -0
  100. package/dist/utils/orderTypes.d.cts +114 -0
  101. package/dist/utils/orderTypes.d.cts.map +1 -0
  102. package/dist/utils/orderTypes.d.mts +114 -0
  103. package/dist/utils/orderTypes.d.mts.map +1 -0
  104. package/dist/utils/orderTypes.mjs +210 -0
  105. package/dist/utils/orderTypes.mjs.map +1 -0
  106. package/package.json +7 -6
@@ -1,10 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.buildOrdersArray = exports.calculateOrderPriceAndSize = exports.calculateFinalPositionSize = exports.getMaxAllowedAmount = exports.calculateMarginRequired = exports.calculatePositionSize = void 0;
3
+ exports.buildOrdersArray = exports.formatPartialTpslSize = exports.validateOrderPrecision = exports.calculateOrderPriceAndSize = exports.calculateFinalPositionSize = exports.floorToSizeDecimals = exports.getMaxAllowedAmount = exports.calculateMarginRequired = exports.calculatePositionSize = void 0;
4
4
  const hyperLiquidConfig_js_1 = require("../constants/hyperLiquidConfig.cjs");
5
5
  const perpsConfig_js_1 = require("../constants/perpsConfig.cjs");
6
6
  const perpsErrorCodes_js_1 = require("../perpsErrorCodes.cjs");
7
7
  const hyperLiquidAdapter_js_1 = require("./hyperLiquidAdapter.cjs");
8
+ const orderTypes_js_1 = require("./orderTypes.cjs");
9
+ /**
10
+ * Tolerance used when deciding whether a scaled size is already on the size
11
+ * grid, guarding against floating-point representation error.
12
+ */
13
+ const FLOAT_TOLERANCE = 1e-6;
8
14
  /**
9
15
  * Calculate position size based on USD amount and asset price
10
16
  *
@@ -54,12 +60,23 @@ function calculateMarginRequired(params) {
54
60
  }
55
61
  exports.calculateMarginRequired = calculateMarginRequired;
56
62
  function getMaxAllowedAmount(params) {
57
- const { spendableBalance, assetPrice, assetSzDecimals, leverage } = params;
63
+ const { spendableBalance, assetPrice, assetSzDecimals, leverage, orderType = 'market', limitPrice, } = params;
58
64
  if (spendableBalance === 0 || !assetPrice || assetSzDecimals === undefined) {
59
65
  return 0;
60
66
  }
61
- // The theoretical maximum is simply spendableBalance * leverage
62
- const theoreticalMax = spendableBalance * leverage;
67
+ // HyperLiquid reserves initial margin for a RESTING order against the price
68
+ // the order is submitted at, not the market price its size was derived from.
69
+ // A limit order resting above the market price - typically a sell - therefore
70
+ // needs more margin than a market-priced notional budgets for, and the
71
+ // exchange refuses it with "insufficient margin to place order". Price the max
72
+ // off that submitted price instead. A marketable order is charged at the fill
73
+ // price, so it needs no adjustment.
74
+ const executionPriceRatio = orderType === 'limit' && limitPrice && limitPrice > assetPrice
75
+ ? limitPrice / assetPrice
76
+ : 1;
77
+ // The theoretical maximum is spendableBalance * leverage, expressed in the
78
+ // market-price notional the caller works with.
79
+ const theoreticalMax = (spendableBalance * leverage) / executionPriceRatio;
63
80
  // But we need to account for position size rounding
64
81
  // Find the largest whole dollar amount that fits within this limit
65
82
  let maxAmount = Math.floor(theoreticalMax);
@@ -69,7 +86,7 @@ function getMaxAllowedAmount(params) {
69
86
  price: assetPrice,
70
87
  szDecimals: assetSzDecimals,
71
88
  });
72
- const actualNotionalValue = parseFloat(testPositionSize) * assetPrice;
89
+ const actualNotionalValue = parseFloat(testPositionSize) * assetPrice * executionPriceRatio;
73
90
  const requiredMargin = actualNotionalValue / leverage;
74
91
  // If rounding caused us to exceed available balance, step down by one position increment
75
92
  if (requiredMargin > spendableBalance) {
@@ -83,6 +100,60 @@ function getMaxAllowedAmount(params) {
83
100
  return Math.max(0, Math.floor(bufferedMax));
84
101
  }
85
102
  exports.getMaxAllowedAmount = getMaxAllowedAmount;
103
+ /**
104
+ * Round a size down onto the asset's size grid.
105
+ *
106
+ * Used for reduce-only orders, where rounding up would push the size past the
107
+ * live position size. Values already on the grid are snapped rather than
108
+ * truncated, because floating-point math can leave them just below a grid
109
+ * point (0.0123 * 10000 === 122.99999999999999) and truncating would drop a
110
+ * whole increment.
111
+ *
112
+ * The result is never greater than `size`, for negative sizes as well as
113
+ * positive: the snap only ever recovers a grid point the input already
114
+ * represents, so a value genuinely below a grid point is stepped down even when
115
+ * the tolerance would have reached the point above it.
116
+ *
117
+ * A size whose scaled form reaches `2^53` is returned unchanged: doubles cannot
118
+ * represent consecutive integers there, so the grid is finer than the spacing
119
+ * between representable values and there is nothing to round down to.
120
+ *
121
+ * @param size - Size to round down.
122
+ * @param szDecimals - The asset's size decimal precision.
123
+ * @returns The size rounded down onto the size grid, never exceeding `size`.
124
+ */
125
+ function floorToSizeDecimals(size, szDecimals) {
126
+ const multiplier = Math.pow(10, szDecimals);
127
+ const scaled = size * multiplier;
128
+ // Past 2^53 a double cannot represent consecutive integers, so `units -= 1`
129
+ // below would be a no-op and the step-down loop would never terminate. The
130
+ // size grid is finer than the spacing between representable values at that
131
+ // magnitude, so there is no increment to shave: return the input unchanged.
132
+ if (!Number.isFinite(scaled) || Math.abs(scaled) >= Number.MAX_SAFE_INTEGER) {
133
+ return size;
134
+ }
135
+ const nearest = Math.round(scaled);
136
+ // The tolerance scales with the magnitude, because double-precision error
137
+ // does too: a fixed epsilon would stop absorbing representation error for
138
+ // sizes that scale past ~1e10 and would then shave off a whole increment.
139
+ const tolerance = Math.max(FLOAT_TOLERANCE, Math.abs(scaled) * Number.EPSILON * 8);
140
+ let units = Math.abs(scaled - nearest) < tolerance ? nearest : Math.floor(scaled);
141
+ // Step down until the result no longer exceeds the input. One pass is not
142
+ // enough: a tolerance wide enough to absorb representation error at large
143
+ // magnitudes also reaches the next grid point, and for an input less than half
144
+ // an ulp below a grid point `size * multiplier` evaluates to exactly that grid
145
+ // integer, so flooring the scaled value returns the same too-large result.
146
+ // The comparison alone is the whole termination condition: for a non-negative
147
+ // size the loop stops at or before zero, and for a negative size it stops once
148
+ // the value is no longer above the input. Guarding on `units` instead would
149
+ // skip a negative size below the tolerance, which snaps to `-0` — and
150
+ // `-0 !== 0` is false. The 2^53 bail-out above keeps this bounded.
151
+ while (units / multiplier > size) {
152
+ units -= 1;
153
+ }
154
+ return units / multiplier;
155
+ }
156
+ exports.floorToSizeDecimals = floorToSizeDecimals;
86
157
  /**
87
158
  * Calculates final position size using USD as source of truth with price validation
88
159
  *
@@ -93,34 +164,63 @@ exports.getMaxAllowedAmount = getMaxAllowedAmount;
93
164
  * @returns Final position size as a number
94
165
  */
95
166
  function calculateFinalPositionSize(params) {
96
- const { usdAmount, size, currentPrice, priceAtCalculation, maxSlippageBps, szDecimals, leverage, debugLogger, } = params;
167
+ const { usdAmount, size, currentPrice, priceAtCalculation, maxSlippageBps, szDecimals, leverage, reduceOnly, debugLogger, } = params;
97
168
  let finalPositionSize;
169
+ // Validate price staleness whenever the caller supplied a calculation-time
170
+ // price. This runs before the sizing branches on purpose: a full close submits
171
+ // the exact live position size rather than a USD-derived one, and it must still
172
+ // be rejected when the price has moved past the caller's tolerance.
173
+ if (priceAtCalculation) {
174
+ const priceDeltaBps = Math.abs(((currentPrice - priceAtCalculation) / priceAtCalculation) * 10000);
175
+ const maxSlippageBpsValue = maxSlippageBps ?? perpsConfig_js_1.ORDER_SLIPPAGE_CONFIG.DefaultMarketSlippageBps;
176
+ if (priceDeltaBps > maxSlippageBpsValue) {
177
+ throw new Error(`Price moved too much: ${priceDeltaBps.toFixed(0)} bps (max: ${maxSlippageBpsValue} bps). ` +
178
+ `Expected: ${priceAtCalculation.toFixed(2)}, Current: ${currentPrice.toFixed(2)}`);
179
+ }
180
+ debugLogger?.log('Price validation passed:', {
181
+ priceAtCalculation,
182
+ currentPrice,
183
+ deltaBps: priceDeltaBps.toFixed(2),
184
+ maxSlippageBps: maxSlippageBpsValue,
185
+ });
186
+ }
98
187
  if (usdAmount && parseFloat(usdAmount) > 0) {
99
188
  // USD amount provided - use it as source of truth
100
189
  const usdValue = parseFloat(usdAmount);
101
- // 1. Validate price staleness if priceAtCalculation provided
102
- if (priceAtCalculation) {
103
- const priceDeltaBps = Math.abs(((currentPrice - priceAtCalculation) / priceAtCalculation) * 10000);
104
- const maxSlippageBpsValue = maxSlippageBps ?? perpsConfig_js_1.ORDER_SLIPPAGE_CONFIG.DefaultMarketSlippageBps;
105
- if (priceDeltaBps > maxSlippageBpsValue) {
106
- throw new Error(`Price moved too much: ${priceDeltaBps.toFixed(0)} bps (max: ${maxSlippageBpsValue} bps). ` +
107
- `Expected: ${priceAtCalculation.toFixed(2)}, Current: ${currentPrice.toFixed(2)}`);
190
+ // Recalculate position size with fresh price
191
+ finalPositionSize = usdValue / currentPrice;
192
+ // A reduce-only order may never exceed the size the caller asked to close:
193
+ // that size is already clamped to the live position, while the USD amount was
194
+ // computed against an older price and can imply a larger size after an
195
+ // adverse move. Capping here keeps USD accuracy in the common case and makes
196
+ // the caller's clamp binding.
197
+ if (reduceOnly && size) {
198
+ const requestedSize = parseFloat(size);
199
+ // A supplied size must be positive, or the cap below would submit a
200
+ // zero/negative order. Reject it rather than silently falling back to the
201
+ // USD-derived size, matching how closePosition treats the same input.
202
+ if (!Number.isFinite(requestedSize) || requestedSize <= 0) {
203
+ throw new Error(perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_SIZE_POSITIVE);
108
204
  }
109
- debugLogger?.log('Price validation passed:', {
110
- priceAtCalculation,
111
- currentPrice,
112
- deltaBps: priceDeltaBps.toFixed(2),
113
- maxSlippageBps: maxSlippageBpsValue,
114
- });
205
+ finalPositionSize = Math.min(finalPositionSize, requestedSize);
115
206
  }
116
- // 2. Recalculate position size with fresh price
117
- finalPositionSize = usdValue / currentPrice;
118
- // 3. Apply size decimals rounding
207
+ // 3. Apply size decimals rounding (reduce-only never rounds up)
119
208
  const multiplier = Math.pow(10, szDecimals);
120
- finalPositionSize = Math.round(finalPositionSize * multiplier) / multiplier;
121
- // 4. Ensure rounded size meets requested USD (fix validation gap)
209
+ const sizeBeforeRounding = finalPositionSize;
210
+ finalPositionSize = reduceOnly
211
+ ? floorToSizeDecimals(finalPositionSize, szDecimals)
212
+ : Math.round(finalPositionSize * multiplier) / multiplier;
213
+ // Rounding down can zero out a reduce-only order whose USD value is worth
214
+ // less than one size increment. Fail with a clear error instead of
215
+ // submitting a size of "0" the exchange will reject.
216
+ if (reduceOnly && finalPositionSize <= 0 && sizeBeforeRounding > 0) {
217
+ throw new Error(perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_SIZE_POSITIVE);
218
+ }
219
+ // 4. Ensure rounded size meets requested USD (fix validation gap).
220
+ // Skipped for reduce-only orders: adding an increment there would submit
221
+ // more than the position holds and HyperLiquid rejects the order.
122
222
  let actualNotionalValue = finalPositionSize * currentPrice;
123
- if (actualNotionalValue < usdValue) {
223
+ if (!reduceOnly && actualNotionalValue < usdValue) {
124
224
  // Add 1 minimum increment to meet requested USD
125
225
  finalPositionSize += 1 / multiplier;
126
226
  actualNotionalValue = finalPositionSize * currentPrice;
@@ -155,6 +255,23 @@ function calculateFinalPositionSize(params) {
155
255
  else {
156
256
  // Legacy: Use provided size (backward compatibility)
157
257
  finalPositionSize = parseFloat(size ?? '0');
258
+ // Reduce-only sizes are formatted with toFixed() further down, which rounds
259
+ // up; truncate onto the size grid first so a close can never exceed the
260
+ // position it is closing.
261
+ if (reduceOnly) {
262
+ // A supplied size must be positive, or formatHyperLiquidSize would render
263
+ // a zero or negative order size. The USD branch above rejects the same
264
+ // input.
265
+ if (size && !(finalPositionSize > 0)) {
266
+ throw new Error(perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_SIZE_POSITIVE);
267
+ }
268
+ const sizeBeforeFlooring = finalPositionSize;
269
+ finalPositionSize = floorToSizeDecimals(finalPositionSize, szDecimals);
270
+ // A positive size that floors to zero is worth less than one increment
271
+ if (finalPositionSize <= 0 && sizeBeforeFlooring > 0) {
272
+ throw new Error(perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_SIZE_POSITIVE);
273
+ }
274
+ }
158
275
  debugLogger?.log('Using legacy size calculation (no USD amount provided):', {
159
276
  providedSize: size,
160
277
  finalSize: finalPositionSize,
@@ -170,10 +287,41 @@ exports.calculateFinalPositionSize = calculateFinalPositionSize;
170
287
  * @returns Formatted order price, size, and price string
171
288
  */
172
289
  function calculateOrderPriceAndSize(params) {
173
- const { orderType, isBuy, finalPositionSize, currentPrice, limitPrice, maxSlippageBps, szDecimals, } = params;
290
+ const { orderType, isBuy, finalPositionSize, currentPrice, limitPrice, triggerPrice, maxSlippageBps, szDecimals, } = params;
174
291
  let orderPrice;
175
292
  let formattedSize;
176
- if (orderType === 'market') {
293
+ if ((0, orderTypes_js_1.isTriggerOrderType)(orderType)) {
294
+ // Trigger placements price off the trigger, not the live market: the order
295
+ // rests off-book until the trigger fires.
296
+ if (!triggerPrice) {
297
+ throw new Error(perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_TRIGGER_PRICE_REQUIRED);
298
+ }
299
+ const triggerPriceNum = parseFloat(triggerPrice);
300
+ if (isNaN(triggerPriceNum) || triggerPriceNum <= 0) {
301
+ throw new Error(perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_TRIGGER_PRICE_POSITIVE);
302
+ }
303
+ if ((0, orderTypes_js_1.isLimitExecutionOrderType)(orderType)) {
304
+ if (!limitPrice) {
305
+ throw new Error(perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_LIMIT_PRICE_REQUIRED);
306
+ }
307
+ orderPrice = parseFloat(limitPrice);
308
+ }
309
+ else {
310
+ // Market execution on trigger: HyperLiquid still needs a limit price, used
311
+ // as a slippage cap. The caller's tolerance wins when supplied; otherwise
312
+ // the 10% convention of the existing TP/SL children applies.
313
+ const effectiveBps = maxSlippageBps ?? perpsConfig_js_1.ORDER_SLIPPAGE_CONFIG.DefaultTpslSlippageBps;
314
+ const slippageValue = effectiveBps / hyperLiquidConfig_js_1.BASIS_POINTS_DIVISOR;
315
+ orderPrice = isBuy
316
+ ? triggerPriceNum * (1 + slippageValue)
317
+ : triggerPriceNum * (1 - slippageValue);
318
+ }
319
+ formattedSize = (0, hyperLiquidAdapter_js_1.formatHyperLiquidSize)({
320
+ size: finalPositionSize,
321
+ szDecimals,
322
+ });
323
+ }
324
+ else if (orderType === 'market') {
177
325
  // Market orders: apply slippage buffer to the live price so HyperLiquid
178
326
  // receives a worst-case acceptable limit price. Falls back to the
179
327
  // documented default if the caller does not provide one.
@@ -205,6 +353,175 @@ function calculateOrderPriceAndSize(params) {
205
353
  return { orderPrice, formattedSize, formattedPrice };
206
354
  }
207
355
  exports.calculateOrderPriceAndSize = calculateOrderPriceAndSize;
356
+ /**
357
+ * Build the SDK order-type field for the main order.
358
+ *
359
+ * Trigger placements map to the SDK's trigger shape; everything else keeps the
360
+ * existing Gtc/FrontendMarket limit shape.
361
+ *
362
+ * @param params - Order type parameters
363
+ * @param params.orderType - Placement type
364
+ * @param params.timeInForce - Time in force; only limit orders may carry one
365
+ * @param params.triggerPrice - Trigger price (required for trigger placements)
366
+ * @param params.szDecimals - Asset size decimals, for price formatting
367
+ * @returns The SDK `t` field for the main order
368
+ */
369
+ function buildMainOrderTypeField(params) {
370
+ const { orderType, timeInForce, triggerPrice, szDecimals } = params;
371
+ if (!(0, orderTypes_js_1.isTriggerOrderType)(orderType)) {
372
+ if (orderType === 'limit') {
373
+ return { limit: { tif: (0, orderTypes_js_1.toSDKTimeInForce)(timeInForce) } };
374
+ }
375
+ if (timeInForce !== undefined) {
376
+ throw new Error(perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_TIME_IN_FORCE_NOT_SUPPORTED);
377
+ }
378
+ return { limit: { tif: 'FrontendMarket' } };
379
+ }
380
+ if (timeInForce !== undefined) {
381
+ throw new Error(perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_TIME_IN_FORCE_NOT_SUPPORTED);
382
+ }
383
+ if (!triggerPrice) {
384
+ throw new Error(perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_TRIGGER_PRICE_REQUIRED);
385
+ }
386
+ return {
387
+ trigger: {
388
+ isMarket: !(0, orderTypes_js_1.isLimitExecutionOrderType)(orderType),
389
+ triggerPx: formatTriggerPrice({
390
+ price: triggerPrice,
391
+ szDecimals,
392
+ error: perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_TRIGGER_PRICE_POSITIVE,
393
+ }),
394
+ tpsl: (0, orderTypes_js_1.getTriggerDirection)(orderType) === 'stop' ? 'sl' : 'tp',
395
+ },
396
+ };
397
+ }
398
+ /**
399
+ * Format a price that becomes a `triggerPx`, rejecting one that disappears at
400
+ * the asset's precision.
401
+ *
402
+ * A positive price below the asset's tick (`0.0004` where the asset quotes to
403
+ * three places) formats to `'0'`, which the exchange rejects. Callers validate
404
+ * this up front via `validateOrderPrecision`; this is the guard on the build
405
+ * path itself, so no caller can assemble an order that cannot be accepted.
406
+ *
407
+ * @param params - Price parameters
408
+ * @param params.price - The requested price
409
+ * @param params.szDecimals - Asset size decimals
410
+ * @param params.error - Typed error to throw when the price rounds away
411
+ * @returns The exchange-formatted price, guaranteed positive.
412
+ */
413
+ function formatTriggerPrice(params) {
414
+ const { price, szDecimals, error } = params;
415
+ const formatted = (0, hyperLiquidAdapter_js_1.formatHyperLiquidPrice)({ price, szDecimals });
416
+ if (parseFloat(formatted) <= 0) {
417
+ throw new Error(error);
418
+ }
419
+ return formatted;
420
+ }
421
+ /**
422
+ * Resolve the size of an attached TP/SL order.
423
+ *
424
+ * @param params - Size parameters
425
+ * @param params.tpslSize - Requested partial size, if any
426
+ * @param params.formattedSize - Full order size, used when no partial size is given
427
+ * @param params.szDecimals - Asset size decimals
428
+ * @returns The exchange-formatted TP/SL order size
429
+ */
430
+ function formatTpslSize(params) {
431
+ const { tpslSize, formattedSize, szDecimals } = params;
432
+ if (tpslSize === undefined) {
433
+ return formattedSize;
434
+ }
435
+ // Validation compares the requested size against `params.size`, but a
436
+ // usdAmount-based order is finally sized from a fresher price, so the parent
437
+ // can end up smaller than the child that validated cleanly. Clamp so the
438
+ // attached TP/SL never exceeds the order it protects.
439
+ const requested = parseFloat(tpslSize);
440
+ const parentSize = parseFloat(formattedSize);
441
+ const size = Number.isFinite(parentSize) && Number.isFinite(requested)
442
+ ? Math.min(requested, parentSize)
443
+ : requested;
444
+ return formatPartialTpslSize({ size, szDecimals });
445
+ }
446
+ /**
447
+ * Check that an order's prices and partial sizes survive the asset's precision.
448
+ *
449
+ * Validation elsewhere sees the values the caller supplied; this sees what the
450
+ * exchange will actually receive. A positive value below the asset's tick
451
+ * formats to `'0'`, which either changes the order's meaning (a zero-sized
452
+ * trigger covers the whole position) or is rejected outright (a zero
453
+ * `triggerPx`).
454
+ *
455
+ * Callers run this before taking any side effect — cancelling the position's
456
+ * existing triggers, changing leverage, moving HIP-3 margin — so a value that
457
+ * would only fail once the orders are built cannot leave a position stripped of
458
+ * its protection, or an account with leverage moved, for an order that was
459
+ * never going to be accepted.
460
+ *
461
+ * @param params - Price and size parameters
462
+ * @param params.triggerPrice - Trigger price for a trigger placement, if any
463
+ * @param params.takeProfitPrice - Attached take profit price, if any
464
+ * @param params.stopLossPrice - Attached stop loss price, if any
465
+ * @param params.takeProfitSize - Requested partial take profit size, if any
466
+ * @param params.stopLossSize - Requested partial stop loss size, if any
467
+ * @param params.szDecimals - Asset size decimals
468
+ * @returns Validation result with isValid flag and optional error message
469
+ */
470
+ function validateOrderPrecision(params) {
471
+ const { triggerPrice, takeProfitPrice, stopLossPrice, takeProfitSize, stopLossSize, szDecimals, } = params;
472
+ for (const size of [takeProfitSize, stopLossSize]) {
473
+ if (size === undefined) {
474
+ continue;
475
+ }
476
+ if (parseFloat((0, hyperLiquidAdapter_js_1.formatHyperLiquidSize)({ size, szDecimals })) <= 0) {
477
+ return {
478
+ isValid: false,
479
+ error: perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_TPSL_SIZE_INVALID,
480
+ };
481
+ }
482
+ }
483
+ // Prices carry their own precision: an asset quotes to
484
+ // `MaxPriceDecimals - szDecimals` places, so a positive price under that tick
485
+ // formats to '0'. Every one of these becomes a `triggerPx` the exchange
486
+ // rejects outright.
487
+ const priceChecks = [
488
+ [triggerPrice, perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_TRIGGER_PRICE_POSITIVE],
489
+ [takeProfitPrice, perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_PRICE_POSITIVE],
490
+ [stopLossPrice, perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_PRICE_POSITIVE],
491
+ ];
492
+ for (const [price, error] of priceChecks) {
493
+ if (price === undefined) {
494
+ continue;
495
+ }
496
+ if (parseFloat((0, hyperLiquidAdapter_js_1.formatHyperLiquidPrice)({ price, szDecimals })) <= 0) {
497
+ return { isValid: false, error };
498
+ }
499
+ }
500
+ return { isValid: true };
501
+ }
502
+ exports.validateOrderPrecision = validateOrderPrecision;
503
+ /**
504
+ * Format a partial TP/SL size, rejecting one that disappears at the asset
505
+ * precision.
506
+ *
507
+ * Validation only sees the requested size, so a positive value below the
508
+ * asset's precision (0.0004 against `szDecimals: 3`) passes and then formats to
509
+ * `'0'`. HyperLiquid reads a zero-sized trigger as covering the whole position,
510
+ * which would silently turn a partial TP/SL into a full close.
511
+ *
512
+ * @param params - Size parameters
513
+ * @param params.size - The requested partial size
514
+ * @param params.szDecimals - Asset size decimals
515
+ * @returns The exchange-formatted size, guaranteed positive.
516
+ */
517
+ function formatPartialTpslSize(params) {
518
+ const formatted = (0, hyperLiquidAdapter_js_1.formatHyperLiquidSize)(params);
519
+ if (parseFloat(formatted) <= 0) {
520
+ throw new Error(perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_TPSL_SIZE_INVALID);
521
+ }
522
+ return formatted;
523
+ }
524
+ exports.formatPartialTpslSize = formatPartialTpslSize;
208
525
  /**
209
526
  * Builds orders array including main order and optional TP/SL orders
210
527
  *
@@ -212,7 +529,7 @@ exports.calculateOrderPriceAndSize = calculateOrderPriceAndSize;
212
529
  * @returns Array of SDK order params and grouping type
213
530
  */
214
531
  function buildOrdersArray(params) {
215
- const { assetId, isBuy, formattedPrice, formattedSize, reduceOnly, orderType, clientOrderId, takeProfitPrice, stopLossPrice, szDecimals, grouping, } = params;
532
+ const { assetId, isBuy, formattedPrice, formattedSize, reduceOnly, orderType, timeInForce, clientOrderId, triggerPrice, takeProfitPrice, stopLossPrice, takeProfitSize, stopLossSize, szDecimals, grouping, } = params;
216
533
  const orders = [];
217
534
  // 1. Main order
218
535
  const mainOrder = {
@@ -221,9 +538,12 @@ function buildOrdersArray(params) {
221
538
  p: formattedPrice,
222
539
  s: formattedSize,
223
540
  r: reduceOnly || false,
224
- t: orderType === 'limit'
225
- ? { limit: { tif: 'Gtc' } }
226
- : { limit: { tif: 'FrontendMarket' } },
541
+ t: buildMainOrderTypeField({
542
+ orderType,
543
+ timeInForce,
544
+ triggerPrice,
545
+ szDecimals,
546
+ }),
227
547
  c: clientOrderId ? clientOrderId : undefined,
228
548
  };
229
549
  orders.push(mainOrder);
@@ -236,14 +556,19 @@ function buildOrdersArray(params) {
236
556
  price: parseFloat(takeProfitPrice),
237
557
  szDecimals,
238
558
  }),
239
- s: formattedSize,
559
+ s: formatTpslSize({
560
+ tpslSize: takeProfitSize,
561
+ formattedSize,
562
+ szDecimals,
563
+ }),
240
564
  r: true,
241
565
  t: {
242
566
  trigger: {
243
567
  isMarket: false,
244
- triggerPx: (0, hyperLiquidAdapter_js_1.formatHyperLiquidPrice)({
245
- price: parseFloat(takeProfitPrice),
568
+ triggerPx: formatTriggerPrice({
569
+ price: takeProfitPrice,
246
570
  szDecimals,
571
+ error: perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_PRICE_POSITIVE,
247
572
  }),
248
573
  tpsl: 'tp',
249
574
  },
@@ -267,14 +592,15 @@ function buildOrdersArray(params) {
267
592
  price: limitPriceWithSlippage,
268
593
  szDecimals,
269
594
  }),
270
- s: formattedSize,
595
+ s: formatTpslSize({ tpslSize: stopLossSize, formattedSize, szDecimals }),
271
596
  r: true,
272
597
  t: {
273
598
  trigger: {
274
599
  isMarket: true,
275
- triggerPx: (0, hyperLiquidAdapter_js_1.formatHyperLiquidPrice)({
276
- price: stopLossPriceNum,
600
+ triggerPx: formatTriggerPrice({
601
+ price: stopLossPrice,
277
602
  szDecimals,
603
+ error: perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_PRICE_POSITIVE,
278
604
  }),
279
605
  tpsl: 'sl',
280
606
  },