@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
@@ -10,7 +10,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
11
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
12
  };
13
- var _HyperLiquidProvider_instances, _HyperLiquidProvider_deps, _HyperLiquidProvider_clientService, _HyperLiquidProvider_walletService, _HyperLiquidProvider_subscriptionService, _HyperLiquidProvider_symbolToAssetId, _HyperLiquidProvider_userFeeCache, _HyperLiquidProvider_maxLeverageCache, _HyperLiquidProvider_cachedMetaByDex, _HyperLiquidProvider_cachedMarketDataWithPrices, _HyperLiquidProvider_cachedSpotMeta, _HyperLiquidProvider_dexDiscoveryCache, _HyperLiquidProvider_referralCheckCache, _HyperLiquidProvider_builderFeeCheckCache, _HyperLiquidProvider_ensureReadyPromise, _HyperLiquidProvider_pendingBuilderFeeApprovals, _HyperLiquidProvider_compiledAllowlistPatterns, _HyperLiquidProvider_compiledBlocklistPatterns, _HyperLiquidProvider_userFeeDiscountBips, _HyperLiquidProvider_hip3Enabled, _HyperLiquidProvider_allowlistMarkets, _HyperLiquidProvider_blocklistMarkets, _HyperLiquidProvider_useUnifiedAccount, _HyperLiquidProvider_dexDiscoveryComplete, _HyperLiquidProvider_unifiedAccountSetupNeedsRetry, _HyperLiquidProvider_pendingValidatedDexsPromise, _HyperLiquidProvider_cachedUsdcTokenId, _HyperLiquidProvider_errorMappings, _HyperLiquidProvider_clientsInitialized, _HyperLiquidProvider_initializationPromise, _HyperLiquidProvider_messenger, _HyperLiquidProvider_builderAddressTestnet, _HyperLiquidProvider_builderAddressMainnet, _HyperLiquidProvider_priceDeviationLimit, _HyperLiquidProvider_compilePatternsSafely, _HyperLiquidProvider_ensureClientsInitialized, _HyperLiquidProvider_isWalletOnHyperliquid, _HyperLiquidProvider_ensureUnifiedAccountEnabled, _HyperLiquidProvider_ensureReady, _HyperLiquidProvider_tradingSetupPromise, _HyperLiquidProvider_tradingSetupComplete, _HyperLiquidProvider_ensureReadyForTrading, _HyperLiquidProvider_getOrFetchPrice, _HyperLiquidProvider_filterFills, _HyperLiquidProvider_getAllAvailableDexs, _HyperLiquidProvider_getValidatedDexs, _HyperLiquidProvider_fetchValidatedDexsInternal, _HyperLiquidProvider_getCachedMeta, _HyperLiquidProvider_backfillAssetMapForDex, _HyperLiquidProvider_getAssetIdWithRepair, _HyperLiquidProvider_getCachedSpotMeta, _HyperLiquidProvider_getCachedPerpDexs, _HyperLiquidProvider_calculateHip3FeeMultiplier, _HyperLiquidProvider_getCacheKey, _HyperLiquidProvider_fetchMarketsForDex, _HyperLiquidProvider_getUsdcTokenId, _HyperLiquidProvider_isUsdcCollateralDex, _HyperLiquidProvider_buildAssetMapping, _HyperLiquidProvider_queryUserDataAcrossDexs, _HyperLiquidProvider_mapError, _HyperLiquidProvider_getErrorContext, _HyperLiquidProvider_checkBuilderFeeApproval, _HyperLiquidProvider_ensureBuilderFeeApproval, _HyperLiquidProvider_checkBuilderFeeStatus, _HyperLiquidProvider_getBalanceForDex, _HyperLiquidProvider_findSourceDexWithBalance, _HyperLiquidProvider_autoTransferForHip3Order, _HyperLiquidProvider_autoTransferBackAfterClose, _HyperLiquidProvider_calculateHip3RequiredMargin, _HyperLiquidProvider_handleHip3PostOrderRebalance, _HyperLiquidProvider_handleHip3OrderRollback, _HyperLiquidProvider_validateOrderBeforePlacement, _HyperLiquidProvider_getAssetInfo, _HyperLiquidProvider_prepareAssetForTrading, _HyperLiquidProvider_handleHip3PreOrder, _HyperLiquidProvider_submitOrderWithRollback, _HyperLiquidProvider_handleOrderError, _HyperLiquidProvider_getStandaloneValidatedDexs, _HyperLiquidProvider_getAllMids, _HyperLiquidProvider_fetchSingleDexFresh, _HyperLiquidProvider_excludeNonUsdcCollateralResults, _HyperLiquidProvider_mergeDexResultsInto, _HyperLiquidProvider_cacheFreshMarketDataSnapshot, _HyperLiquidProvider_getStaleMarketDataSnapshot, _HyperLiquidProvider_isFeeCacheValid, _HyperLiquidProvider_getBuilderAddress, _HyperLiquidProvider_getReferralCode, _HyperLiquidProvider_ensureReferralSet, _HyperLiquidProvider_isReferralCodeReady, _HyperLiquidProvider_checkReferralSet, _HyperLiquidProvider_setReferralCode;
13
+ var _HyperLiquidProvider_instances, _HyperLiquidProvider_deps, _HyperLiquidProvider_clientService, _HyperLiquidProvider_walletService, _HyperLiquidProvider_subscriptionService, _HyperLiquidProvider_symbolToAssetId, _HyperLiquidProvider_userFeeCache, _HyperLiquidProvider_maxLeverageCache, _HyperLiquidProvider_cachedMetaByDex, _HyperLiquidProvider_cachedMarketDataWithPrices, _HyperLiquidProvider_cachedSpotMeta, _HyperLiquidProvider_dexDiscoveryCache, _HyperLiquidProvider_referralCheckCache, _HyperLiquidProvider_builderFeeCheckCache, _HyperLiquidProvider_ensureReadyPromise, _HyperLiquidProvider_pendingBuilderFeeApprovals, _HyperLiquidProvider_compiledAllowlistPatterns, _HyperLiquidProvider_compiledBlocklistPatterns, _HyperLiquidProvider_userFeeDiscountBips, _HyperLiquidProvider_hip3Enabled, _HyperLiquidProvider_allowlistMarkets, _HyperLiquidProvider_blocklistMarkets, _HyperLiquidProvider_useUnifiedAccount, _HyperLiquidProvider_dexDiscoveryComplete, _HyperLiquidProvider_unifiedAccountSetupNeedsRetry, _HyperLiquidProvider_pendingValidatedDexsPromise, _HyperLiquidProvider_cachedUsdcTokenId, _HyperLiquidProvider_errorMappings, _HyperLiquidProvider_clientsInitialized, _HyperLiquidProvider_initializationPromise, _HyperLiquidProvider_messenger, _HyperLiquidProvider_builderAddressTestnet, _HyperLiquidProvider_builderAddressMainnet, _HyperLiquidProvider_priceDeviationLimit, _HyperLiquidProvider_compilePatternsSafely, _HyperLiquidProvider_ensureClientsInitialized, _HyperLiquidProvider_isWalletOnHyperliquid, _HyperLiquidProvider_ensureUnifiedAccountEnabled, _HyperLiquidProvider_ensureReady, _HyperLiquidProvider_tradingSetupPromise, _HyperLiquidProvider_tradingSetupComplete, _HyperLiquidProvider_ensureReadyForTrading, _HyperLiquidProvider_getOrFetchPrice, _HyperLiquidProvider_filterFills, _HyperLiquidProvider_getAllAvailableDexs, _HyperLiquidProvider_getValidatedDexs, _HyperLiquidProvider_fetchValidatedDexsInternal, _HyperLiquidProvider_getCachedMeta, _HyperLiquidProvider_backfillAssetMapForDex, _HyperLiquidProvider_getAssetIdWithRepair, _HyperLiquidProvider_getCachedSpotMeta, _HyperLiquidProvider_getCachedPerpDexs, _HyperLiquidProvider_calculateHip3FeeMultiplier, _HyperLiquidProvider_getCacheKey, _HyperLiquidProvider_fetchMarketsForDex, _HyperLiquidProvider_getUsdcTokenId, _HyperLiquidProvider_isUsdcCollateralDex, _HyperLiquidProvider_buildAssetMapping, _HyperLiquidProvider_queryUserDataAcrossDexs, _HyperLiquidProvider_mapError, _HyperLiquidProvider_getErrorContext, _HyperLiquidProvider_checkBuilderFeeApproval, _HyperLiquidProvider_ensureBuilderFeeApproval, _HyperLiquidProvider_checkBuilderFeeStatus, _HyperLiquidProvider_getBalanceForDex, _HyperLiquidProvider_findSourceDexWithBalance, _HyperLiquidProvider_autoTransferForHip3Order, _HyperLiquidProvider_autoTransferBackAfterClose, _HyperLiquidProvider_calculateHip3RequiredMargin, _HyperLiquidProvider_handleHip3PostOrderRebalance, _HyperLiquidProvider_handleHip3OrderRollback, _HyperLiquidProvider_validateOrderBeforePlacement, _HyperLiquidProvider_getAssetInfo, _HyperLiquidProvider_prepareAssetForTrading, _HyperLiquidProvider_handleHip3PreOrder, _HyperLiquidProvider_submitOrderWithRollback, _HyperLiquidProvider_handleOrderError, _HyperLiquidProvider_fetchOpenOrders, _HyperLiquidProvider_resolveReplacementOrderId, _HyperLiquidProvider_getStandaloneValidatedDexs, _HyperLiquidProvider_queryDexPositions, _HyperLiquidProvider_getAllMids, _HyperLiquidProvider_fetchSingleDexFresh, _HyperLiquidProvider_excludeNonUsdcCollateralResults, _HyperLiquidProvider_mergeDexResultsInto, _HyperLiquidProvider_cacheFreshMarketDataSnapshot, _HyperLiquidProvider_getStaleMarketDataSnapshot, _HyperLiquidProvider_isFeeCacheValid, _HyperLiquidProvider_getBuilderAddress, _HyperLiquidProvider_getReferralCode, _HyperLiquidProvider_ensureReferralSet, _HyperLiquidProvider_isReferralCodeReady, _HyperLiquidProvider_checkReferralSet, _HyperLiquidProvider_setReferralCode;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.HyperLiquidProvider = void 0;
16
16
  const utils_1 = require("@metamask/utils");
@@ -35,6 +35,7 @@ const hyperLiquidValidation_js_1 = require("../utils/hyperLiquidValidation.cjs")
35
35
  const marketDataTransform_js_1 = require("../utils/marketDataTransform.cjs");
36
36
  const marketUtils_js_1 = require("../utils/marketUtils.cjs");
37
37
  const orderCalculations_js_1 = require("../utils/orderCalculations.cjs");
38
+ const orderTypes_js_1 = require("../utils/orderTypes.cjs");
38
39
  const standaloneInfoClient_js_1 = require("../utils/standaloneInfoClient.cjs");
39
40
  // getStreamManagerInstance removed: use this.#deps.streamManager instead
40
41
  /**
@@ -45,6 +46,80 @@ const standaloneInfoClient_js_1 = require("../utils/standaloneInfoClient.cjs");
45
46
  * @returns The result of the operation.
46
47
  */
47
48
  const isStatusObject = (status) => typeof status === 'object' && status !== null;
49
+ /**
50
+ * Collect the order IDs of every TP/SL child carried by a parent order.
51
+ *
52
+ * HyperLiquid lists `normalTpsl` children both nested under their parent and as
53
+ * top-level entries in `frontendOpenOrders`. Those children protect the pending
54
+ * parent order rather than the position, so callers use this set to exclude them.
55
+ *
56
+ * @param orders - Raw frontend open orders for the account.
57
+ * @returns The set of child order IDs.
58
+ */
59
+ function collectChildOrderIds(orders) {
60
+ const childOrderIds = new Set();
61
+ orders.forEach((order) => {
62
+ order.children?.forEach((child) => {
63
+ childOrderIds.add(child.oid);
64
+ });
65
+ });
66
+ return childOrderIds;
67
+ }
68
+ /**
69
+ * Group orders by market, so a per-position pass does not rescan every order.
70
+ *
71
+ * @param orders - Raw frontend open orders across all DEXs.
72
+ * @returns Orders keyed by market symbol.
73
+ */
74
+ function groupOrdersBySymbol(orders) {
75
+ const bySymbol = new Map();
76
+ orders.forEach((order) => {
77
+ const existing = bySymbol.get(order.coin);
78
+ if (existing) {
79
+ existing.push(order);
80
+ }
81
+ else {
82
+ bySymbol.set(order.coin, [order]);
83
+ }
84
+ });
85
+ return bySymbol;
86
+ }
87
+ /**
88
+ * Build the trigger-order view of a position: position-bound TP/SL plus
89
+ * standalone (partial) reduce-only triggers on the same market, de-duplicated by
90
+ * order ID and excluding children of pending parent orders.
91
+ *
92
+ * @param params - Collection parameters.
93
+ * @param params.orders - Raw frontend open orders across all DEXs.
94
+ * @param params.position - Position the triggers are attached to.
95
+ * @param params.childOrderIds - Order IDs that belong to a pending parent order.
96
+ * @returns The take profit and stop loss trigger orders for the position.
97
+ */
98
+ function collectPositionTriggerOrders(params) {
99
+ const { orders, position, childOrderIds } = params;
100
+ const byOrderId = new Map();
101
+ orders.forEach((rawOrder) => {
102
+ if (rawOrder.coin !== position.symbol ||
103
+ !rawOrder.isTrigger ||
104
+ !rawOrder.reduceOnly ||
105
+ childOrderIds.has(rawOrder.oid)) {
106
+ return;
107
+ }
108
+ const triggerOrder = (0, hyperLiquidAdapter_js_1.adaptPositionTriggerOrderFromSDK)({
109
+ rawOrder,
110
+ positionSize: position.size,
111
+ entryPrice: position.entryPrice,
112
+ });
113
+ if (triggerOrder && !byOrderId.has(triggerOrder.orderId)) {
114
+ byOrderId.set(triggerOrder.orderId, triggerOrder);
115
+ }
116
+ });
117
+ const triggerOrders = Array.from(byOrderId.values());
118
+ return {
119
+ takeProfitOrders: triggerOrders.filter((order) => order.direction === 'take_profit'),
120
+ stopLossOrders: triggerOrders.filter((order) => order.direction !== 'take_profit'),
121
+ };
122
+ }
48
123
  /**
49
124
  * HyperLiquid provider implementation
50
125
  *
@@ -298,6 +373,14 @@ class HyperLiquidProvider {
298
373
  size: params.size,
299
374
  price: params.price,
300
375
  orderType: params.orderType,
376
+ triggerPrice: params.triggerPrice,
377
+ takeProfitPrice: params.takeProfitPrice,
378
+ stopLossPrice: params.stopLossPrice,
379
+ takeProfitSize: params.takeProfitSize,
380
+ stopLossSize: params.stopLossSize,
381
+ tpslLinkage: params.tpslLinkage,
382
+ grouping: params.grouping,
383
+ timeInForce: params.timeInForce,
301
384
  });
302
385
  if (!validation.isValid) {
303
386
  throw new Error(validation.error);
@@ -310,6 +393,22 @@ class HyperLiquidProvider {
310
393
  symbol: params.symbol,
311
394
  dexName,
312
395
  });
396
+ // A price or partial size that rounds away at the asset precision is
397
+ // caught here, as soon as szDecimals is known and before anything is
398
+ // committed: the signing prompts in #ensureReadyForTrading, the leverage
399
+ // change in #prepareAssetForTrading, and the HIP-3 margin transfer all
400
+ // come later.
401
+ const precision = (0, orderCalculations_js_1.validateOrderPrecision)({
402
+ triggerPrice: params.triggerPrice,
403
+ takeProfitPrice: params.takeProfitPrice,
404
+ stopLossPrice: params.stopLossPrice,
405
+ takeProfitSize: params.takeProfitSize,
406
+ stopLossSize: params.stopLossSize,
407
+ szDecimals: assetInfo.szDecimals,
408
+ });
409
+ if (!precision.isValid) {
410
+ throw new Error(precision.error);
411
+ }
313
412
  // Allow override with UI-provided price (optimization to avoid API call).
314
413
  effectivePrice =
315
414
  params.currentPrice && params.currentPrice > 0
@@ -362,6 +461,7 @@ class HyperLiquidProvider {
362
461
  maxSlippageBps: normalizedMaxSlippageBps,
363
462
  szDecimals: assetInfo.szDecimals,
364
463
  leverage: params.leverage,
464
+ reduceOnly: params.reduceOnly,
365
465
  });
366
466
  const { orderPrice, formattedSize, formattedPrice } = (0, orderCalculations_js_1.calculateOrderPriceAndSize)({
367
467
  orderType: params.orderType,
@@ -369,6 +469,7 @@ class HyperLiquidProvider {
369
469
  finalPositionSize,
370
470
  currentPrice: effectivePrice,
371
471
  limitPrice: params.price,
472
+ triggerPrice: params.triggerPrice,
372
473
  maxSlippageBps: normalizedMaxSlippageBps,
373
474
  szDecimals: assetInfo.szDecimals,
374
475
  });
@@ -413,11 +514,19 @@ class HyperLiquidProvider {
413
514
  formattedSize,
414
515
  reduceOnly: params.reduceOnly ?? false,
415
516
  orderType: params.orderType,
517
+ timeInForce: params.timeInForce,
416
518
  clientOrderId: params.clientOrderId,
519
+ triggerPrice: params.triggerPrice,
417
520
  takeProfitPrice: params.takeProfitPrice,
418
521
  stopLossPrice: params.stopLossPrice,
522
+ takeProfitSize: params.takeProfitSize,
523
+ stopLossSize: params.stopLossSize,
419
524
  szDecimals: assetInfo.szDecimals,
420
- grouping: params.grouping,
525
+ // The provider-agnostic linkage wins; `grouping` is the deprecated
526
+ // HyperLiquid-shaped spelling kept for existing callers.
527
+ grouping: params.tpslLinkage
528
+ ? (0, hyperLiquidAdapter_js_1.adaptTpslLinkageToGrouping)(params.tpslLinkage)
529
+ : params.grouping,
421
530
  });
422
531
  // 8. Submit order with atomic rollback
423
532
  return await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_submitOrderWithRollback).call(this, {
@@ -436,7 +545,13 @@ class HyperLiquidProvider {
436
545
  const errorMessage = (0, errorUtils_js_1.ensureError)(error, 'HyperLiquidProvider.placeOrder').message;
437
546
  const isMinimumOrderError = errorMessage.includes('Order must have minimum value of $10') ||
438
547
  errorMessage.includes('Order 0: Order must have minimum value');
439
- if (isMinimumOrderError && retryCount === 0) {
548
+ // Reduce-only orders are excluded. The retry works by growing the order
549
+ // 1.5%, which a close cannot do: a full close already submits the whole
550
+ // position, and a partial close is capped at the size the caller asked to
551
+ // close, so the retry would either be rejected as "Reduce only order would
552
+ // increase position" or resubmit an identical order. Surfacing the
553
+ // minimum-value error names the real problem instead.
554
+ if (isMinimumOrderError && retryCount === 0 && !params.reduceOnly) {
440
555
  let adjustedUsdAmount;
441
556
  let originalValue;
442
557
  if (params.usdAmount) {
@@ -503,20 +618,84 @@ class HyperLiquidProvider {
503
618
  error: perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_SIZE_POSITIVE,
504
619
  };
505
620
  }
621
+ // `modify` rebuilds an order as a plain limit/market order, so a trigger
622
+ // on either side of the edit would be silently dropped. Reject a resting
623
+ // trigger order as well as an edit *into* one; cancel and re-place instead.
624
+ if ((0, orderTypes_js_1.isTriggerOrderType)(params.newOrder.orderType)) {
625
+ return {
626
+ success: false,
627
+ error: perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_EDIT_TRIGGER_UNSUPPORTED,
628
+ };
629
+ }
630
+ // The WebSocket order cache is the cheap source for the resting order's
631
+ // placement type, but it may be cold or stale.
632
+ const cachedRestingOrder = __classPrivateFieldGet(this, _HyperLiquidProvider_subscriptionService, "f")
633
+ .getOrdersCacheIfInitialized()
634
+ ?.find((order) => order.orderId === params.orderId.toString());
635
+ if (cachedRestingOrder?.isTrigger === true) {
636
+ return {
637
+ success: false,
638
+ error: perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_EDIT_TRIGGER_UNSUPPORTED,
639
+ };
640
+ }
506
641
  // Validate new order parameters
507
642
  const validation = (0, hyperLiquidValidation_js_1.validateOrderParams)({
508
643
  coin: params.newOrder.symbol,
509
644
  size: params.newOrder.size,
510
645
  price: params.newOrder.price,
511
646
  orderType: params.newOrder.orderType,
647
+ triggerPrice: params.newOrder.triggerPrice,
648
+ takeProfitPrice: params.newOrder.takeProfitPrice,
649
+ stopLossPrice: params.newOrder.stopLossPrice,
650
+ takeProfitSize: params.newOrder.takeProfitSize,
651
+ stopLossSize: params.newOrder.stopLossSize,
652
+ tpslLinkage: params.newOrder.tpslLinkage,
653
+ grouping: params.newOrder.grouping,
654
+ timeInForce: params.newOrder.timeInForce,
512
655
  });
513
656
  if (!validation.isValid) {
514
657
  throw new Error(validation.error);
515
658
  }
516
- // Ensure provider is ready for trading (includes signing operations)
517
- await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_ensureReadyForTrading).call(this);
518
659
  // Extract DEX name for API calls (main DEX = null)
519
660
  const { dex: dexName } = (0, hyperLiquidAdapter_js_1.parseAssetName)(params.newOrder.symbol);
661
+ // Initialization only — clients and the asset mapping. The signing half
662
+ // of readiness is deferred until after the checks below, so a refused
663
+ // edit never prompts for a signature or writes an approval.
664
+ await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_ensureReady).call(this);
665
+ // What is resting before the edit serves two purposes, and they carry
666
+ // different weight. Verifying the target is REQUIRED when the cache could
667
+ // not do it — an unverified edit can rebuild a protective stop as a plain
668
+ // order — so that read must fail closed. Providing a baseline for the
669
+ // optional orderId resolution is not: when the cache already confirmed the
670
+ // order, a failed read must not sink a modify that would otherwise
671
+ // succeed, exactly as the post-modify lookup does not.
672
+ let ordersBeforeEdit;
673
+ if (cachedRestingOrder === undefined) {
674
+ ordersBeforeEdit = await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_fetchOpenOrders).call(this, { dexName });
675
+ const restingOrder = ordersBeforeEdit.find((order) => order.oid.toString() === params.orderId.toString());
676
+ if (!restingOrder) {
677
+ return {
678
+ success: false,
679
+ error: perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_EDIT_ORDER_UNVERIFIABLE,
680
+ };
681
+ }
682
+ if (restingOrder.isTrigger) {
683
+ return {
684
+ success: false,
685
+ error: perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_EDIT_TRIGGER_UNSUPPORTED,
686
+ };
687
+ }
688
+ }
689
+ else {
690
+ try {
691
+ ordersBeforeEdit = await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_fetchOpenOrders).call(this, { dexName });
692
+ }
693
+ catch (error) {
694
+ // Only the optional identity baseline is lost. Without it novelty
695
+ // cannot be judged, so the id is omitted below rather than guessed.
696
+ __classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Could not read the pre-edit orders baseline:', error);
697
+ }
698
+ }
520
699
  // Get asset info and prices (uses cache to avoid redundant API calls)
521
700
  const meta = await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_getCachedMeta).call(this, { dexName });
522
701
  // asset.name format: "BTC" for main DEX, "xyz:XYZ100" for HIP-3
@@ -556,14 +735,20 @@ class HyperLiquidProvider {
556
735
  p: formattedPrice,
557
736
  s: formattedSize,
558
737
  r: params.newOrder.reduceOnly ?? false,
559
- // Same TIF logic as placeOrder - see documentation above for details
738
+ // Same TIF logic as placeOrder - see documentation above for details.
739
+ // A limit order honours the caller's time in force; validation above has
740
+ // already rejected one on any other order shape.
560
741
  t: params.newOrder.orderType === 'limit'
561
- ? { limit: { tif: 'Gtc' } } // Standard limit order
742
+ ? { limit: { tif: (0, orderTypes_js_1.toSDKTimeInForce)(params.newOrder.timeInForce) } }
562
743
  : { limit: { tif: 'FrontendMarket' } }, // True market order
563
744
  c: params.newOrder.clientOrderId
564
745
  ? params.newOrder.clientOrderId
565
746
  : undefined,
566
747
  };
748
+ // Every refusal is behind us, so the setup that may prompt for signatures
749
+ // and write builder-fee/referral approvals can run now — a rejected edit
750
+ // costs the caller nothing, matching placeOrder and updatePositionTPSL.
751
+ await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_ensureReadyForTrading).call(this);
567
752
  // Submit modification via SDK
568
753
  const exchangeClient = __classPrivateFieldGet(this, _HyperLiquidProvider_clientService, "f").getExchangeClient();
569
754
  const result = await exchangeClient.modify({
@@ -575,9 +760,25 @@ class HyperLiquidProvider {
575
760
  if (result.status !== 'ok') {
576
761
  throw new Error(`Order modification failed: ${JSON.stringify(result)}`);
577
762
  }
763
+ // `params.orderId` is the order that was just REPLACED, so returning it
764
+ // as OrderResult.orderId (documented as the exchange order ID) names an
765
+ // order the venue has already cancelled. Report the replacement when it
766
+ // can be resolved unambiguously, and otherwise omit the optional id
767
+ // rather than fabricate identity.
768
+ const replacementOrderId = ordersBeforeEdit === undefined
769
+ ? undefined
770
+ : await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_resolveReplacementOrderId).call(this, {
771
+ previousOrders: ordersBeforeEdit,
772
+ dexName,
773
+ symbol: params.newOrder.symbol,
774
+ isBuy: params.newOrder.isBuy,
775
+ size: formattedSize,
776
+ });
578
777
  return {
579
778
  success: true,
580
- orderId: params.orderId.toString(),
779
+ ...(replacementOrderId === undefined
780
+ ? {}
781
+ : { orderId: replacementOrderId }),
581
782
  };
582
783
  }
583
784
  catch (error) {
@@ -744,10 +945,18 @@ class HyperLiquidProvider {
744
945
  ...new Set(positionsToClose.map((pos) => (0, hyperLiquidAdapter_js_1.parseAssetName)(pos.symbol).dex ?? 'main')),
745
946
  ];
746
947
  await Promise.all(uniqueDexs.map((dex) => __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_getCachedMeta).call(this, { dexName: dex === 'main' ? null : dex })));
747
- // Track HIP-3 positions and freed margins for post-close transfers
748
- const hip3Transfers = [];
749
- // Build orders array
948
+ // Freed-margin transfer for each submitted order, or null when that order
949
+ // needs none. One entry per order rather than one per HIP-3 position: a
950
+ // compacted list read with the response-status index credits the wrong
951
+ // order in a mixed main-DEX/HIP-3 batch.
952
+ const orderedHip3Transfers = [];
953
+ // Build orders array, plus the positions each order closes so response
954
+ // statuses stay index-aligned when a position is skipped below
750
955
  const orders = [];
956
+ const orderedPositions = [];
957
+ // Positions no order could be built for. Reported as failures so a caller
958
+ // cannot read "closed everything" from a result that left one open.
959
+ const skippedResults = [];
751
960
  for (const position of positionsToClose) {
752
961
  // Extract DEX name for HIP-3 positions
753
962
  const { dex: dexName } = (0, hyperLiquidAdapter_js_1.parseAssetName)(position.symbol);
@@ -769,13 +978,29 @@ class HyperLiquidProvider {
769
978
  const isBuy = positionSize < 0; // Close opposite side
770
979
  const closeSize = Math.abs(positionSize);
771
980
  const totalMarginUsed = parseFloat(position.marginUsed);
772
- // Track HIP-3 transfers (full position close means all margin is freed)
773
- if (isHip3Position && dexName && !__classPrivateFieldGet(this, _HyperLiquidProvider_useUnifiedAccount, "f")) {
774
- hip3Transfers.push({
775
- sourceDex: dexName,
776
- freedMargin: totalMarginUsed,
981
+ // formatHyperLiquidSize() below rounds half-up, so floor onto the size
982
+ // grid first: a reduce-only order rounded above the position is rejected
983
+ // with "Reduce only order would increase position".
984
+ const flooredCloseSize = (0, orderCalculations_js_1.floorToSizeDecimals)(closeSize, assetInfo.szDecimals);
985
+ // A dust position worth less than one size increment floors to 0, which
986
+ // would submit a zero-size order. Skip it rather than sending an order
987
+ // the exchange must reject; the remaining positions still close, and the
988
+ // skip is reported as a failure below.
989
+ if (flooredCloseSize <= 0) {
990
+ __classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Skipping position smaller than one size increment', { coin: position.symbol, size: position.size });
991
+ skippedResults.push({
992
+ symbol: position.symbol,
993
+ success: false,
994
+ error: perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_SIZE_POSITIVE,
777
995
  });
996
+ continue;
778
997
  }
998
+ // Track this order's HIP-3 transfer, if it needs one (a full position
999
+ // close frees all of its margin). Pushed below alongside the order so the
1000
+ // two stay index-aligned.
1001
+ const hip3Transfer = isHip3Position && dexName && !__classPrivateFieldGet(this, _HyperLiquidProvider_useUnifiedAccount, "f")
1002
+ ? { sourceDex: dexName, freedMargin: totalMarginUsed }
1003
+ : null;
779
1004
  const currentPrice = await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_getOrFetchPrice).call(this, {
780
1005
  symbol: position.symbol,
781
1006
  dexName: dexName ?? null,
@@ -785,9 +1010,8 @@ class HyperLiquidProvider {
785
1010
  const orderPrice = isBuy
786
1011
  ? currentPrice * (1 + slippage)
787
1012
  : currentPrice * (1 - slippage);
788
- // Format size and price
789
1013
  const formattedSize = (0, hyperLiquidAdapter_js_1.formatHyperLiquidSize)({
790
- size: closeSize,
1014
+ size: flooredCloseSize,
791
1015
  szDecimals: assetInfo.szDecimals,
792
1016
  });
793
1017
  const formattedPrice = (0, hyperLiquidAdapter_js_1.formatHyperLiquidPrice)({
@@ -803,6 +1027,19 @@ class HyperLiquidProvider {
803
1027
  r: true, // reduceOnly
804
1028
  t: { limit: { tif: 'Ioc' } }, // Immediate or cancel for market-like execution
805
1029
  });
1030
+ orderedPositions.push(position);
1031
+ orderedHip3Transfers.push(hip3Transfer);
1032
+ }
1033
+ // Every position was smaller than one size increment. Return their
1034
+ // failures rather than an empty result, which would be indistinguishable
1035
+ // from "no positions matched".
1036
+ if (orders.length === 0) {
1037
+ return {
1038
+ success: false,
1039
+ successCount: 0,
1040
+ failureCount: skippedResults.length,
1041
+ results: skippedResults,
1042
+ };
806
1043
  }
807
1044
  // Calculate discounted builder fee if reward discount is active
808
1045
  let builderFee = hyperLiquidConfig_js_1.BUILDER_FEE_CONFIG.MaxFeeTenthsBps;
@@ -822,16 +1059,17 @@ class HyperLiquidProvider {
822
1059
  const { statuses } = result.response.data;
823
1060
  const successCount = statuses.filter((stat) => isStatusObject(stat) &&
824
1061
  ((0, utils_1.hasProperty)(stat, 'filled') || (0, utils_1.hasProperty)(stat, 'resting'))).length;
825
- const failureCount = statuses.length - successCount;
1062
+ const failureCount = statuses.length - successCount + skippedResults.length;
826
1063
  // Handle HIP-3 margin transfers for successful closes
827
1064
  if (!__classPrivateFieldGet(this, _HyperLiquidProvider_useUnifiedAccount, "f")) {
828
1065
  for (let i = 0; i < statuses.length; i++) {
829
1066
  const status = statuses[i];
830
1067
  const isSuccess = isStatusObject(status) &&
831
1068
  ((0, utils_1.hasProperty)(status, 'filled') || (0, utils_1.hasProperty)(status, 'resting'));
832
- if (isSuccess && hip3Transfers[i]) {
833
- const { sourceDex, freedMargin } = hip3Transfers[i];
834
- __classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Position closed successfully, initiating manual auto-transfer back', { symbol: positionsToClose[i].symbol, freedMargin });
1069
+ const transfer = orderedHip3Transfers[i];
1070
+ if (isSuccess && transfer) {
1071
+ const { sourceDex, freedMargin } = transfer;
1072
+ __classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Position closed successfully, initiating manual auto-transfer back', { symbol: orderedPositions[i].symbol, freedMargin });
835
1073
  // Non-blocking: Transfer freed margin back to main DEX
836
1074
  await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_autoTransferBackAfterClose).call(this, {
837
1075
  sourceDex,
@@ -840,18 +1078,29 @@ class HyperLiquidProvider {
840
1078
  }
841
1079
  }
842
1080
  }
843
- return {
844
- success: successCount > 0,
845
- successCount,
846
- failureCount,
847
- results: statuses.map((status, index) => ({
848
- symbol: positionsToClose[index].symbol,
1081
+ // Index submitted and skipped outcomes by symbol so `results` can keep the
1082
+ // order of the requested positions: consumers may correlate them by index.
1083
+ const submittedResults = new Map(statuses.map((status, index) => [
1084
+ orderedPositions[index].symbol,
1085
+ {
1086
+ symbol: orderedPositions[index].symbol,
849
1087
  success: isStatusObject(status) &&
850
1088
  ((0, utils_1.hasProperty)(status, 'filled') || (0, utils_1.hasProperty)(status, 'resting')),
851
1089
  error: isStatusObject(status) && (0, utils_1.hasProperty)(status, 'error')
852
1090
  ? String(status.error)
853
1091
  : undefined,
854
- })),
1092
+ },
1093
+ ]));
1094
+ const skippedBySymbol = new Map(skippedResults.map((skipped) => [skipped.symbol, skipped]));
1095
+ return {
1096
+ success: successCount > 0,
1097
+ successCount,
1098
+ failureCount,
1099
+ results: positionsToClose.flatMap((position) => {
1100
+ const outcome = submittedResults.get(position.symbol) ??
1101
+ skippedBySymbol.get(position.symbol);
1102
+ return outcome ? [outcome] : [];
1103
+ }),
855
1104
  };
856
1105
  }
857
1106
  catch (error) {
@@ -887,18 +1136,35 @@ class HyperLiquidProvider {
887
1136
  * 1. 'normalTpsl' - Tied to a parent order (set when placing the order)
888
1137
  * 2. 'positionTpsl' - Tied to a position (can be set/modified after fill)
889
1138
  *
1139
+ * Partial TP/SL: when `takeProfitSize` or `stopLossSize` is supplied, the
1140
+ * orders cannot use 'positionTpsl' (which always covers the whole position and
1141
+ * requires size 0). They are submitted as standalone reduce-only trigger orders
1142
+ * with 'na' grouping and explicit sizes instead.
1143
+ *
1144
+ * Note that the pre-cancel sweep clears every standalone reduce-only trigger
1145
+ * on the symbol — whether this update is partial or whole-position — not only
1146
+ * the ones this method placed. A trigger the caller placed independently
1147
+ * through `placeOrder` (for example a manual reduce-only stop) is therefore
1148
+ * cancelled too. Only TP/SL children of another pending order are protected.
1149
+ *
890
1150
  * @param params - The operation parameters.
891
1151
  * @param params.symbol - Asset symbol of the position
892
1152
  * @param params.takeProfitPrice - TP price (undefined to remove)
893
1153
  * @param params.stopLossPrice - SL price (undefined to remove)
1154
+ * @param params.takeProfitSize - Partial TP size (undefined for the whole position)
1155
+ * @param params.stopLossSize - Partial SL size (undefined for the whole position)
894
1156
  * @returns A promise that resolves to the result.
895
1157
  */
896
1158
  async updatePositionTPSL(params) {
897
1159
  try {
898
1160
  __classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Updating position TP/SL:', params);
899
- const { symbol, takeProfitPrice, stopLossPrice, position: livePosition, } = params;
900
- // Ensure provider is ready for trading (includes signing operations)
901
- await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_ensureReadyForTrading).call(this);
1161
+ const { symbol, takeProfitPrice, stopLossPrice, takeProfitSize, stopLossSize, position: livePosition, } = params;
1162
+ const isPartialTpsl = takeProfitSize !== undefined || stopLossSize !== undefined;
1163
+ // Basic initialization only. The trading setup that can prompt a hardware
1164
+ // wallet and write the referral / builder-fee approvals is deferred until
1165
+ // every validation below has passed, so a rejected update leaves nothing
1166
+ // behind.
1167
+ await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_ensureReady).call(this);
902
1168
  // Use live position (from WebSocket) if available, otherwise fetch via REST
903
1169
  // Preferring WebSocket data avoids rate limiting issues with the REST API
904
1170
  let position = livePosition;
@@ -928,12 +1194,67 @@ class HyperLiquidProvider {
928
1194
  }
929
1195
  const positionSize = Math.abs(parseFloat(position.size));
930
1196
  const isLong = parseFloat(position.size) > 0;
931
- // Get clients for API calls (ensureReady already called at method start)
1197
+ // Partial TP/SL sizes must be positive, paired with their price, and no
1198
+ // larger than the position they close.
1199
+ const tpslSizeValidation = (0, hyperLiquidValidation_js_1.validateOrderParams)({
1200
+ coin: symbol,
1201
+ size: positionSize.toString(),
1202
+ takeProfitPrice,
1203
+ stopLossPrice,
1204
+ takeProfitSize,
1205
+ stopLossSize,
1206
+ });
1207
+ if (!tpslSizeValidation.isValid) {
1208
+ return {
1209
+ success: false,
1210
+ error: tpslSizeValidation.error,
1211
+ };
1212
+ }
1213
+ // Get clients for API calls (#ensureReady already called at method start).
1214
+ // Holding the exchange client reference is not itself a write; it is only
1215
+ // used below, after the trading setup has run.
932
1216
  const infoClient = __classPrivateFieldGet(this, _HyperLiquidProvider_clientService, "f").getInfoClient();
933
1217
  const exchangeClient = __classPrivateFieldGet(this, _HyperLiquidProvider_clientService, "f").getExchangeClient();
934
1218
  const userAddress = await __classPrivateFieldGet(this, _HyperLiquidProvider_walletService, "f").getUserAddressWithDefault();
935
1219
  // Extract DEX name for API calls (main DEX = null)
936
1220
  const { dex: dexName } = (0, hyperLiquidAdapter_js_1.parseAssetName)(symbol);
1221
+ // Asset info is resolved before the pre-cancel sweep so a partial size
1222
+ // that rounds away at the asset precision is rejected while the
1223
+ // position's existing triggers are still in place. Rejecting it after the
1224
+ // sweep would leave the position unprotected with nothing put back.
1225
+ const meta = await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_getCachedMeta).call(this, { dexName });
1226
+ // Check if meta is an error response (string) or doesn't have universe property
1227
+ if (!meta ||
1228
+ typeof meta === 'string' ||
1229
+ !meta.universe ||
1230
+ !Array.isArray(meta.universe)) {
1231
+ __classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Failed to fetch metadata for asset mapping', {
1232
+ meta,
1233
+ dex: dexName ?? 'main',
1234
+ });
1235
+ throw new Error(`Failed to fetch market metadata for DEX ${dexName ?? 'main'}`);
1236
+ }
1237
+ // asset.name format: "BTC" for main DEX, "xyz:XYZ100" for HIP-3
1238
+ const assetInfo = meta.universe.find((asset) => asset.name === symbol);
1239
+ if (!assetInfo) {
1240
+ throw new Error(`Asset ${symbol} not found in ${dexName ?? 'main'} DEX universe`);
1241
+ }
1242
+ const precision = (0, orderCalculations_js_1.validateOrderPrecision)({
1243
+ takeProfitPrice,
1244
+ stopLossPrice,
1245
+ takeProfitSize,
1246
+ stopLossSize,
1247
+ szDecimals: assetInfo.szDecimals,
1248
+ });
1249
+ if (!precision.isValid) {
1250
+ return {
1251
+ success: false,
1252
+ error: precision.error,
1253
+ };
1254
+ }
1255
+ // Everything is validated: only now run the trading setup that can prompt
1256
+ // for signatures and write the referral / builder-fee approvals.
1257
+ await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_ensureReadyForTrading).call(this);
937
1258
  // Cancel existing TP/SL orders for this position
938
1259
  // OPTIMIZATION: Use WebSocket cache first (0 weight), fall back to single-DEX REST (20 weight)
939
1260
  // Previously: queryUserDataAcrossDexs queried ALL DEXs (20 weight × N DEXs = 40+ weight)
@@ -944,18 +1265,48 @@ class HyperLiquidProvider {
944
1265
  let cancelRequests = [];
945
1266
  // Use atomic getter to prevent race condition between check and get
946
1267
  const cachedOrders = __classPrivateFieldGet(this, _HyperLiquidProvider_subscriptionService, "f").getOrdersCacheIfInitialized();
947
- if (cachedOrders === null) {
1268
+ // Replacing TP/SL has to consider standalone ('na' grouping) triggers —
1269
+ // left by a partial update or placed independently — which are not
1270
+ // position-bound. Telling those apart from a pending order's normalTpsl
1271
+ // child requires the parent/child relationship, which only the REST
1272
+ // payload carries. The cache path is therefore only safe when the cache
1273
+ // shows no such trigger on this market: a partial update always places
1274
+ // standalone triggers, and a whole-position update must still clear any
1275
+ // standalone leftovers instead of letting them fire beside the new
1276
+ // position-bound orders.
1277
+ const cacheShowsStandaloneTriggers = Boolean(cachedOrders?.some((order) => order.symbol === symbol &&
1278
+ order.reduceOnly === true &&
1279
+ order.isTrigger === true &&
1280
+ order.isPositionTpsl !==
1281
+ Boolean(perpsConfig_js_1.TP_SL_CONFIG.UsePositionBoundTpsl) &&
1282
+ order.detailedOrderType &&
1283
+ (order.detailedOrderType.includes('Take Profit') ||
1284
+ order.detailedOrderType.includes('Stop'))));
1285
+ if (cachedOrders === null ||
1286
+ isPartialTpsl ||
1287
+ cacheShowsStandaloneTriggers) {
948
1288
  // Fallback: Query only the specific DEX (20 weight instead of 40+)
949
- __classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('WebSocket cache not initialized, falling back to single-DEX REST query', { dex: dexName ?? 'main' });
1289
+ __classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log(cachedOrders === null
1290
+ ? 'WebSocket cache not initialized, falling back to single-DEX REST query'
1291
+ : 'TP/SL update needs parent/child order context: using single-DEX REST query', { dex: dexName ?? 'main', isPartialTpsl });
950
1292
  const orders = await infoClient.frontendOpenOrders({
951
1293
  user: userAddress,
952
1294
  dex: dexName ?? undefined,
953
1295
  });
1296
+ // Orders that belong to a pending parent order (normalTpsl children) are
1297
+ // also listed at the top level, so collect their IDs to exclude them:
1298
+ // they protect that pending order, not this position.
1299
+ const childOrderIds = collectChildOrderIds(orders);
954
1300
  // Filter using raw SDK response properties
955
1301
  const tpslOrders = orders.filter((order) => order.coin === symbol &&
956
1302
  order.reduceOnly &&
957
- order.isPositionTpsl ===
958
- Boolean(perpsConfig_js_1.TP_SL_CONFIG.UsePositionBoundTpsl) &&
1303
+ // Position-bound TP/SL always qualifies, and so do standalone
1304
+ // triggers on this market (they belong to the position too, whether
1305
+ // this update is partial or whole) — but never another order's
1306
+ // TP/SL children.
1307
+ (order.isPositionTpsl ===
1308
+ Boolean(perpsConfig_js_1.TP_SL_CONFIG.UsePositionBoundTpsl) ||
1309
+ !childOrderIds.has(order.oid)) &&
959
1310
  order.isTrigger &&
960
1311
  (order.orderType.includes('Take Profit') ||
961
1312
  order.orderType.includes('Stop')));
@@ -994,33 +1345,24 @@ class HyperLiquidProvider {
994
1345
  });
995
1346
  __classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Cancel result:', cancelResult);
996
1347
  }
997
- // Get asset info (dexName already extracted above) - uses cache
998
- const meta = await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_getCachedMeta).call(this, { dexName });
999
- // Check if meta is an error response (string) or doesn't have universe property
1000
- if (!meta ||
1001
- typeof meta === 'string' ||
1002
- !meta.universe ||
1003
- !Array.isArray(meta.universe)) {
1004
- __classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Failed to fetch metadata for asset mapping', {
1005
- meta,
1006
- dex: dexName ?? 'main',
1007
- });
1008
- throw new Error(`Failed to fetch market metadata for DEX ${dexName ?? 'main'}`);
1009
- }
1010
- // asset.name format: "BTC" for main DEX, "xyz:XYZ100" for HIP-3
1011
- const assetInfo = meta.universe.find((asset) => asset.name === symbol);
1012
- if (!assetInfo) {
1013
- throw new Error(`Asset ${symbol} not found in ${dexName ?? 'main'} DEX universe`);
1014
- }
1015
1348
  // assetId already validated above when building cancelRequests
1016
1349
  // Build orders array for TP/SL
1017
1350
  const orders = [];
1018
- const size = perpsConfig_js_1.TP_SL_CONFIG.UsePositionBoundTpsl
1351
+ const fullSize = perpsConfig_js_1.TP_SL_CONFIG.UsePositionBoundTpsl && !isPartialTpsl
1019
1352
  ? '0'
1020
1353
  : (0, hyperLiquidAdapter_js_1.formatHyperLiquidSize)({
1021
1354
  size: positionSize,
1022
1355
  szDecimals: assetInfo.szDecimals,
1023
1356
  });
1357
+ // Partial TP/SL orders carry their own size; the rest cover the position.
1358
+ // A partial size that rounds away at the asset precision is rejected
1359
+ // rather than sent as '0', which the exchange reads as whole-position.
1360
+ const resolveTpslSize = (tpslSize) => tpslSize === undefined
1361
+ ? fullSize
1362
+ : (0, orderCalculations_js_1.formatPartialTpslSize)({
1363
+ size: parseFloat(tpslSize),
1364
+ szDecimals: assetInfo.szDecimals,
1365
+ });
1024
1366
  // Take Profit order
1025
1367
  if (takeProfitPrice) {
1026
1368
  const tpOrder = {
@@ -1030,7 +1372,7 @@ class HyperLiquidProvider {
1030
1372
  price: parseFloat(takeProfitPrice),
1031
1373
  szDecimals: assetInfo.szDecimals,
1032
1374
  }),
1033
- s: size,
1375
+ s: resolveTpslSize(takeProfitSize),
1034
1376
  r: true, // Always reduce-only for position TP
1035
1377
  t: {
1036
1378
  trigger: {
@@ -1054,7 +1396,7 @@ class HyperLiquidProvider {
1054
1396
  price: parseFloat(stopLossPrice),
1055
1397
  szDecimals: assetInfo.szDecimals,
1056
1398
  }),
1057
- s: size,
1399
+ s: resolveTpslSize(stopLossSize),
1058
1400
  r: true, // Always reduce-only for position SL
1059
1401
  t: {
1060
1402
  trigger: {
@@ -1087,10 +1429,12 @@ class HyperLiquidProvider {
1087
1429
  discountedFee: builderFee,
1088
1430
  });
1089
1431
  }
1090
- // Submit via SDK exchange client with positionTpsl grouping
1432
+ // Submit via SDK exchange client. Position-bound TP/SL uses 'positionTpsl';
1433
+ // partial TP/SL must be standalone reduce-only triggers ('na'), since a
1434
+ // position-bound TP/SL always closes the whole position.
1091
1435
  const result = await exchangeClient.order({
1092
1436
  orders,
1093
- grouping: 'positionTpsl',
1437
+ grouping: isPartialTpsl ? 'na' : 'positionTpsl',
1094
1438
  builder: {
1095
1439
  b: __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_getBuilderAddress).call(this, __classPrivateFieldGet(this, _HyperLiquidProvider_clientService, "f").isTestnetMode()),
1096
1440
  f: builderFee,
@@ -1130,6 +1474,68 @@ class HyperLiquidProvider {
1130
1474
  // Use provided position (from WebSocket) or fetch from cache
1131
1475
  // This avoids unnecessary API calls and prevents 429 rate limiting
1132
1476
  let { position } = params;
1477
+ // Re-validate the caller-supplied snapshot against the freshest WebSocket
1478
+ // position cache. Clients pass a throttled snapshot (~1s old on mobile),
1479
+ // so a concurrent TP/SL fill, a liquidation, or a double-tapped close
1480
+ // leaves the snapshot's side/size larger than (or opposite to) the real
1481
+ // position and HyperLiquid rejects the reduce-only order with "Reduce
1482
+ // only order would increase position". Reading the cache never issues a
1483
+ // REST request, so this does not reintroduce 429 rate limiting.
1484
+ if (position && __classPrivateFieldGet(this, _HyperLiquidProvider_subscriptionService, "f").isPositionsCacheInitialized()) {
1485
+ // Read the symbol's own DEX slice, not the aggregate. The aggregate is
1486
+ // only rebuilt once every expected DEX has published, so after a
1487
+ // WebSocket reconnect — which resets the initialized-DEX set without
1488
+ // clearing these caches — it can sit frozen at pre-reconnect contents
1489
+ // while the per-DEX slices keep updating. Deciding "this DEX is covered"
1490
+ // from the per-DEX map and then reading the position from the aggregate
1491
+ // mixed a fresh answer with stale data: a close could reuse a stale size,
1492
+ // or throw for a position that is open.
1493
+ const dexPositions = __classPrivateFieldGet(this, _HyperLiquidProvider_subscriptionService, "f").getCachedPositionsForDex((0, hyperLiquidAdapter_js_1.parseAssetName)(params.symbol).dex ?? '');
1494
+ const livePosition = dexPositions?.find((pos) => pos.symbol === params.symbol);
1495
+ if (livePosition) {
1496
+ if (livePosition.size !== position.size) {
1497
+ __classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Stale close position snapshot: using live WebSocket position', {
1498
+ coin: params.symbol,
1499
+ snapshotSize: position.size,
1500
+ liveSize: livePosition.size,
1501
+ });
1502
+ }
1503
+ position = livePosition;
1504
+ }
1505
+ else if (dexPositions) {
1506
+ // That DEX has published and does not hold this symbol, so the position
1507
+ // is already closed (e.g. a double-tapped close). This is the same read
1508
+ // the lookup above used, so the two can never disagree. Fail here rather
1509
+ // than falling back to REST: the cache is the freshest source, so a REST
1510
+ // lookup can only burn a request that risks 429s and, if it lags, hand
1511
+ // back a position that no longer exists.
1512
+ throw new Error(`No position found for ${params.symbol}`);
1513
+ }
1514
+ else {
1515
+ // The cache holds nothing for this symbol's DEX — a HIP-3 DEX whose
1516
+ // subscription has not published this session — so the symbol's
1517
+ // absence proves nothing. Spend one REST request to get live data
1518
+ // rather than trusting a snapshot the exchange may have moved past.
1519
+ __classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Position cache does not cover this DEX: fetching live positions', { coin: params.symbol });
1520
+ // Query the symbol's own DEX so the outcome carries provenance.
1521
+ // getPositions() fans out across every enabled DEX, flattens the subset
1522
+ // that answered and turns any failure into [], so it cannot distinguish
1523
+ // "this DEX answered and holds nothing" from "this DEX failed or was
1524
+ // never queried" — and those two need opposite decisions.
1525
+ const { answered, positions } = await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_queryDexPositions).call(this, (0, hyperLiquidAdapter_js_1.parseAssetName)(params.symbol).dex);
1526
+ const livePositionFromApi = positions.find((pos) => pos.symbol === params.symbol);
1527
+ if (livePositionFromApi) {
1528
+ position = livePositionFromApi;
1529
+ }
1530
+ else if (answered) {
1531
+ // The DEX answered without this symbol — even with no positions at
1532
+ // all — so it is genuinely closed.
1533
+ throw new Error(`No position found for ${params.symbol}`);
1534
+ }
1535
+ // Otherwise the query failed, so the absence proves nothing: keep the
1536
+ // caller's snapshot rather than block a position that may be closable.
1537
+ }
1538
+ }
1133
1539
  if (!position) {
1134
1540
  const positions = await this.getPositions();
1135
1541
  position = positions.find((pos) => pos.symbol === params.symbol);
@@ -1139,18 +1545,37 @@ class HyperLiquidProvider {
1139
1545
  }
1140
1546
  const positionSize = parseFloat(position.size);
1141
1547
  const isBuy = positionSize < 0;
1142
- const closeSize = params.size ?? Math.abs(positionSize).toString();
1548
+ const absPositionSize = Math.abs(positionSize);
1549
+ // Only an omitted (or empty) size means "close 100%". A supplied size must
1550
+ // be a positive number: silently promoting '0' or 'abc' to a full close
1551
+ // would liquidate the whole position on a caller-side formatting slip.
1552
+ // A supplied size is clamped to the live position size, because
1553
+ // HyperLiquid rejects reduce-only orders that exceed the position and the
1554
+ // caller computed its size from a snapshot that may already be too large.
1555
+ const hasRequestedSize = params.size !== undefined && params.size !== '';
1556
+ let closeSizeNumber = absPositionSize;
1557
+ if (hasRequestedSize) {
1558
+ const requestedSize = parseFloat(params.size);
1559
+ if (!Number.isFinite(requestedSize) || requestedSize <= 0) {
1560
+ throw new Error(perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_SIZE_POSITIVE);
1561
+ }
1562
+ closeSizeNumber = Math.min(requestedSize, absPositionSize);
1563
+ }
1564
+ const closeSize = closeSizeNumber.toString();
1143
1565
  // Capture position details BEFORE closing for freed margin calculation
1144
1566
  const totalMarginUsed = parseFloat(position.marginUsed);
1145
- const totalPositionSize = Math.abs(positionSize);
1146
- const closeSizeNum = parseFloat(closeSize);
1567
+ const totalPositionSize = absPositionSize;
1568
+ const closeSizeNum = closeSizeNumber;
1147
1569
  const isHip3Position = position.symbol.includes(':');
1148
1570
  const hip3Dex = isHip3Position ? position.symbol.split(':')[0] : null;
1149
1571
  // Calculate freed margin proportionally
1150
1572
  const freedMarginRatio = closeSizeNum / totalPositionSize;
1151
1573
  const freedMargin = totalMarginUsed * freedMarginRatio;
1152
- // Get current price for validation if not provided (and not a full close)
1153
- // Full closes don't need price for validation
1574
+ // Get current price for USD/minimum validation if not provided. A full
1575
+ // close skips *that* validation because it submits the exact live size —
1576
+ // but not the price-staleness guard: calculateFinalPositionSize checks
1577
+ // priceAtCalculation against the live price for every close that supplies
1578
+ // it, using the price placeOrder fetches when none is passed here.
1154
1579
  let { currentPrice } = params;
1155
1580
  if (!currentPrice && params.size && !params.usdAmount) {
1156
1581
  // Partial close without USD or price: use limit price as fallback for validation
@@ -1173,6 +1598,10 @@ class HyperLiquidProvider {
1173
1598
  closedSize: closeSize,
1174
1599
  freedMargin: freedMargin.toFixed(2),
1175
1600
  });
1601
+ // True when the order closes 100% of the position: either no size was
1602
+ // provided, or the requested size covers (or was clamped to) the whole
1603
+ // position.
1604
+ const isFullClose = closeSizeNum >= absPositionSize;
1176
1605
  // Execute position close with consistent slippage handling
1177
1606
  const result = await this.placeOrder({
1178
1607
  symbol: params.symbol,
@@ -1181,10 +1610,17 @@ class HyperLiquidProvider {
1181
1610
  orderType: params.orderType ?? 'market',
1182
1611
  price: params.price,
1183
1612
  reduceOnly: true,
1184
- isFullClose: !params.size, // True if closing 100% (size not provided)
1613
+ isFullClose,
1185
1614
  // Pass through price and slippage parameters for consistent validation
1186
1615
  currentPrice,
1187
- usdAmount: params.usdAmount,
1616
+ // A close of the whole position must submit exactly the live position
1617
+ // size. Forwarding usdAmount would make placeOrder recompute the size as
1618
+ // usdAmount / currentPrice — discarding the clamp above, since usdAmount
1619
+ // is the source of truth there — and submit more than the position
1620
+ // holds, which is rejected with "Reduce only order would increase
1621
+ // position". Genuine partial closes keep usdAmount so their size stays
1622
+ // USD-accurate.
1623
+ usdAmount: isFullClose ? undefined : params.usdAmount,
1188
1624
  priceAtCalculation: params.priceAtCalculation,
1189
1625
  maxSlippageBps: params.maxSlippageBps,
1190
1626
  });
@@ -1368,6 +1804,12 @@ class HyperLiquidProvider {
1368
1804
  }
1369
1805
  // Combine all orders from all DEXs for TP/SL lookup
1370
1806
  const allOrders = orderResults.flatMap((result) => result.data);
1807
+ // TP/SL children of pending parent orders are listed at the top level too;
1808
+ // they belong to that order, not to a position.
1809
+ const allOrdersChildIds = collectChildOrderIds(allOrders);
1810
+ // Grouped once here rather than rescanned per position, mirroring the
1811
+ // positionsBySymbol map on the WebSocket path.
1812
+ const ordersBySymbol = groupOrdersBySymbol(allOrders);
1371
1813
  __classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Frontend open orders (all DEXs):', {
1372
1814
  count: allOrders.length,
1373
1815
  orders: allOrders.map((ord) => ({
@@ -1403,6 +1845,16 @@ class HyperLiquidProvider {
1403
1845
  // Look for TP and SL trigger orders
1404
1846
  let takeProfitPrice;
1405
1847
  let stopLossPrice;
1848
+ // Trigger orders attached to this position: position-bound TP/SL plus
1849
+ // standalone ('na' grouping) partial TP/SL. A pending order's
1850
+ // normalTpsl children are excluded — they are also listed at the top
1851
+ // level, but they protect that order, not this position (same rule as
1852
+ // the positionOrders filter above).
1853
+ const { takeProfitOrders, stopLossOrders } = collectPositionTriggerOrders({
1854
+ orders: ordersBySymbol.get(position.symbol) ?? [],
1855
+ position,
1856
+ childOrderIds: allOrdersChildIds,
1857
+ });
1406
1858
  // Check direct trigger orders
1407
1859
  positionOrders.forEach((order) => {
1408
1860
  // Frontend orders have explicit orderType field
@@ -1460,6 +1912,10 @@ class HyperLiquidProvider {
1460
1912
  ...position,
1461
1913
  takeProfitPrice,
1462
1914
  stopLossPrice,
1915
+ takeProfitCount: takeProfitOrders.length,
1916
+ stopLossCount: stopLossOrders.length,
1917
+ takeProfitOrders,
1918
+ stopLossOrders,
1463
1919
  };
1464
1920
  }));
1465
1921
  return allPositions;
@@ -2464,6 +2920,14 @@ class HyperLiquidProvider {
2464
2920
  size: params.size,
2465
2921
  price: params.price,
2466
2922
  orderType: params.orderType,
2923
+ triggerPrice: params.triggerPrice,
2924
+ takeProfitPrice: params.takeProfitPrice,
2925
+ stopLossPrice: params.stopLossPrice,
2926
+ takeProfitSize: params.takeProfitSize,
2927
+ stopLossSize: params.stopLossSize,
2928
+ tpslLinkage: params.tpslLinkage,
2929
+ grouping: params.grouping,
2930
+ timeInForce: params.timeInForce,
2467
2931
  });
2468
2932
  if (!basicValidation.isValid) {
2469
2933
  return basicValidation;
@@ -2492,16 +2956,29 @@ class HyperLiquidProvider {
2492
2956
  // Fallback: Calculate from size × price
2493
2957
  const size = parseFloat(params.size || '0');
2494
2958
  let priceForValidation = params.currentPrice;
2495
- // For limit orders without currentPrice, use limit price as fallback
2959
+ // For limit-executing orders without currentPrice, use limit price as
2960
+ // fallback (plain limit, stop_limit, take_profit_limit)
2496
2961
  if (!priceForValidation &&
2497
2962
  params.price &&
2498
- params.orderType === 'limit') {
2963
+ (0, orderTypes_js_1.isLimitExecutionOrderType)(params.orderType)) {
2499
2964
  priceForValidation = parseFloat(params.price);
2500
2965
  __classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Using limit price for order validation (limit order):', {
2501
2966
  size,
2502
2967
  limitPrice: priceForValidation,
2503
2968
  });
2504
2969
  }
2970
+ // Market-executing trigger orders (stop_market, take_profit_market)
2971
+ // have no limit price; the trigger price is the best notional estimate.
2972
+ if (!priceForValidation &&
2973
+ params.triggerPrice &&
2974
+ (0, orderTypes_js_1.isTriggerOrderType)(params.orderType)) {
2975
+ priceForValidation = parseFloat(params.triggerPrice);
2976
+ __classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Using trigger price for order validation (trigger order):', {
2977
+ size,
2978
+ triggerPrice: priceForValidation,
2979
+ orderType: params.orderType,
2980
+ });
2981
+ }
2505
2982
  if (!priceForValidation) {
2506
2983
  return {
2507
2984
  isValid: false,
@@ -3288,8 +3765,11 @@ class HyperLiquidProvider {
3288
3765
  */
3289
3766
  async calculateFees(params) {
3290
3767
  const { orderType, isMaker = false, amount, symbol } = params;
3768
+ // Trigger placements are charged as their execution kind: a stop_market fills
3769
+ // as a market order (taker), a stop_limit as a limit order.
3770
+ const isMarketExecution = (0, orderTypes_js_1.getTriggerExecution)(orderType) === 'market';
3291
3771
  // Start with base rates from config
3292
- let feeRate = orderType === 'market' || !isMaker ? hyperLiquidConfig_js_1.FEE_RATES.taker : hyperLiquidConfig_js_1.FEE_RATES.maker;
3772
+ let feeRate = isMarketExecution || !isMaker ? hyperLiquidConfig_js_1.FEE_RATES.taker : hyperLiquidConfig_js_1.FEE_RATES.maker;
3293
3773
  // Parse symbol to detect HIP-3 DEX (e.g., "xyz:TSLA" → dex="xyz", parsedSymbol="TSLA")
3294
3774
  const { dex, symbol: parsedSymbol } = (0, hyperLiquidAdapter_js_1.parseAssetName)(symbol);
3295
3775
  const isHip3Asset = dex !== null;
@@ -3334,7 +3814,7 @@ class HyperLiquidProvider {
3334
3814
  const cached = __classPrivateFieldGet(this, _HyperLiquidProvider_userFeeCache, "f").get(userAddress);
3335
3815
  if (cached) {
3336
3816
  // Market orders always use taker rate, limit orders check isMaker
3337
- let userFeeRate = orderType === 'market' || !isMaker
3817
+ let userFeeRate = isMarketExecution || !isMaker
3338
3818
  ? cached.perpsTakerRate
3339
3819
  : cached.perpsMakerRate;
3340
3820
  // Apply HIP-3 dynamic multiplier to user-specific rates (includes Growth Mode)
@@ -3442,7 +3922,7 @@ class HyperLiquidProvider {
3442
3922
  };
3443
3923
  __classPrivateFieldGet(this, _HyperLiquidProvider_userFeeCache, "f").set(userAddress, rates);
3444
3924
  // Market orders always use taker rate, limit orders check isMaker
3445
- let userFeeRate = orderType === 'market' || !isMaker
3925
+ let userFeeRate = isMarketExecution || !isMaker
3446
3926
  ? rates.perpsTakerRate
3447
3927
  : rates.perpsMakerRate;
3448
3928
  // Apply HIP-3 dynamic multiplier to API-fetched rates (includes Growth Mode)
@@ -4976,6 +5456,14 @@ async function _HyperLiquidProvider_queryUserDataAcrossDexs(baseParams, queryFn)
4976
5456
  return { results, failedDexs };
4977
5457
  }, _HyperLiquidProvider_mapError = function _HyperLiquidProvider_mapError(error) {
4978
5458
  const { message } = (0, errorUtils_js_1.ensureError)(error, 'HyperLiquidProvider.mapError');
5459
+ // "User or API Wallet 0x... does not exist." carries the user's address, so
5460
+ // it cannot be matched by the static substring table below. It means the
5461
+ // wallet has no Hyperliquid account yet — surface an actionable code the
5462
+ // client can translate ("fund your account") instead of leaking the raw
5463
+ // exchange string to the UI and to failed-trade analytics.
5464
+ if ((0, errorUtils_js_1.isHyperLiquidUserNotFoundError)(error)) {
5465
+ return new Error(perpsErrorCodes_js_1.PERPS_ERROR_CODES.EXCHANGE_ACCOUNT_NOT_FOUND);
5466
+ }
4979
5467
  for (const [pattern, code] of Object.entries(__classPrivateFieldGet(this, _HyperLiquidProvider_errorMappings, "f"))) {
4980
5468
  if (message.toLowerCase().includes(pattern.toLowerCase())) {
4981
5469
  return new Error(code);
@@ -5738,13 +6226,80 @@ async function _HyperLiquidProvider_submitOrderWithRollback(params) {
5738
6226
  }
5739
6227
  }, _HyperLiquidProvider_handleOrderError = function _HyperLiquidProvider_handleOrderError(params) {
5740
6228
  const { error, symbol, orderType, isBuy } = params;
5741
- __classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").logger.error((0, errorUtils_js_1.ensureError)(error, 'HyperLiquidProvider.handleOrderError'), __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_getErrorContext).call(this, 'placeOrder', {
5742
- symbol,
5743
- orderType,
5744
- isBuy,
5745
- }));
6229
+ // A wallet with no Hyperliquid account is an expected pre-account state,
6230
+ // not an app defect — same policy already applied to every other
6231
+ // user-scoped exchange write in this provider. Keep it out of Sentry; the
6232
+ // failure is still reported to the caller (and to trade analytics) via the
6233
+ // mapped EXCHANGE_ACCOUNT_NOT_FOUND code below.
6234
+ if ((0, errorUtils_js_1.isHyperLiquidUserNotFoundError)(error)) {
6235
+ __classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('[handleOrderError] Wallet has no Hyperliquid account, order cannot be placed', { symbol, orderType, isBuy });
6236
+ }
6237
+ else {
6238
+ __classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").logger.error((0, errorUtils_js_1.ensureError)(error, 'HyperLiquidProvider.handleOrderError'), __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_getErrorContext).call(this, 'placeOrder', {
6239
+ symbol,
6240
+ orderType,
6241
+ isBuy,
6242
+ }));
6243
+ }
5746
6244
  const mappedError = __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_mapError).call(this, error);
5747
6245
  return (0, hyperLiquidValidation_js_1.createErrorResult)(mappedError, { success: false });
6246
+ }, _HyperLiquidProvider_fetchOpenOrders =
6247
+ /**
6248
+ * Read the account's currently resting orders.
6249
+ *
6250
+ * @param params - The lookup parameters.
6251
+ * @param params.dexName - DEX to query, or null for the main DEX.
6252
+ * @returns The raw open orders.
6253
+ */
6254
+ async function _HyperLiquidProvider_fetchOpenOrders(params) {
6255
+ const userAddress = await __classPrivateFieldGet(this, _HyperLiquidProvider_walletService, "f").getUserAddressWithDefault();
6256
+ return await __classPrivateFieldGet(this, _HyperLiquidProvider_clientService, "f").getInfoClient().frontendOpenOrders({
6257
+ user: userAddress,
6258
+ dex: params.dexName ?? undefined,
6259
+ });
6260
+ }, _HyperLiquidProvider_resolveReplacementOrderId =
6261
+ /**
6262
+ * Resolve the order id that a `modify` rested the replacement under.
6263
+ *
6264
+ * HyperLiquid does not edit an order in place: it cancels the target and
6265
+ * rests a replacement under a NEW oid, which the SDK's modify response does
6266
+ * not carry. The submitted oid therefore names an order that no longer
6267
+ * exists, so the only honest source of identity is a post-modify read.
6268
+ *
6269
+ * An id is returned only when exactly one newly-rested order carries the
6270
+ * attributes just submitted. Everything else leaves it absent: a market edit
6271
+ * that filled rather than rested, a read that has not caught up yet, or two
6272
+ * equally plausible candidates. Novelty is judged against the pre-edit
6273
+ * snapshot rather than attributes alone, because an order that was already
6274
+ * resting can share a market, side and size with the replacement.
6275
+ *
6276
+ * @param params - The resolution parameters.
6277
+ * @param params.previousOrders - Orders resting immediately before the edit.
6278
+ * @param params.dexName - DEX to query, or null for the main DEX.
6279
+ * @param params.symbol - Market the edit was submitted against.
6280
+ * @param params.isBuy - Direction submitted.
6281
+ * @param params.size - Formatted size submitted.
6282
+ * @returns The replacement order id, or undefined when it cannot be resolved unambiguously.
6283
+ */
6284
+ async function _HyperLiquidProvider_resolveReplacementOrderId(params) {
6285
+ try {
6286
+ const previousOrderIds = new Set(params.previousOrders.map((order) => order.oid.toString()));
6287
+ const ordersAfterEdit = await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_fetchOpenOrders).call(this, {
6288
+ dexName: params.dexName,
6289
+ });
6290
+ const submittedSize = parseFloat(params.size);
6291
+ const candidates = ordersAfterEdit.filter((order) => !previousOrderIds.has(order.oid.toString()) &&
6292
+ order.coin === params.symbol &&
6293
+ (order.side === 'B') === params.isBuy &&
6294
+ parseFloat(order.sz) === submittedSize);
6295
+ return candidates.length === 1 ? candidates[0].oid.toString() : undefined;
6296
+ }
6297
+ catch (error) {
6298
+ // The modify was accepted; only the identity lookup failed. Reporting a
6299
+ // failed edit here would misstate an order that really was changed.
6300
+ __classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Could not resolve the replacement order id after modify:', error);
6301
+ return undefined;
6302
+ }
5748
6303
  }, _HyperLiquidProvider_getStandaloneValidatedDexs =
5749
6304
  /**
5750
6305
  * Get validated DEXs for standalone mode using a standalone InfoClient.
@@ -5785,6 +6340,48 @@ async function _HyperLiquidProvider_getStandaloneValidatedDexs() {
5785
6340
  // buildAssetMapping uses state.raw for perpDexIndex computation.
5786
6341
  const state = __classPrivateFieldGet(this, _HyperLiquidProvider_dexDiscoveryCache, "f").update(allDexs);
5787
6342
  return state.validated;
6343
+ }, _HyperLiquidProvider_queryDexPositions =
6344
+ /**
6345
+ * Query one DEX's positions directly, preserving whether that DEX answered.
6346
+ *
6347
+ * `getPositions()` fans out across every enabled DEX, flattens the subset that
6348
+ * answered and converts any thrown error into an empty array, so its result
6349
+ * cannot distinguish "this DEX answered and holds no positions" from "this
6350
+ * DEX's request failed or it was never queried". `closePosition` needs that
6351
+ * distinction: the first means the position is closed and the close must fail
6352
+ * before submitting, the second means the absence proves nothing and the
6353
+ * caller's snapshot should stand.
6354
+ *
6355
+ * TP/SL enrichment is skipped, as in standalone mode: the close path only reads
6356
+ * size, side and margin.
6357
+ *
6358
+ * @param dexName - DEX identifier, or null for the main DEX.
6359
+ * @returns Whether the DEX answered, and the positions it reported.
6360
+ */
6361
+ async function _HyperLiquidProvider_queryDexPositions(dexName) {
6362
+ try {
6363
+ await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_ensureClientsInitialized).call(this);
6364
+ __classPrivateFieldGet(this, _HyperLiquidProvider_clientService, "f").ensureInitialized();
6365
+ const infoClient = __classPrivateFieldGet(this, _HyperLiquidProvider_clientService, "f").getInfoClient();
6366
+ const userAddress = await __classPrivateFieldGet(this, _HyperLiquidProvider_walletService, "f").getUserAddressWithDefault();
6367
+ const state = await infoClient.clearinghouseState(dexName ? { user: userAddress, dex: dexName } : { user: userAddress });
6368
+ const positions = (state.assetPositions ?? [])
6369
+ .filter((assetPos) => assetPos.position.szi !== '0')
6370
+ .map((assetPos) => (0, hyperLiquidAdapter_js_1.adaptPositionFromSDK)(assetPos));
6371
+ __classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Target DEX position query answered', {
6372
+ dex: dexName ?? 'main',
6373
+ count: positions.length,
6374
+ });
6375
+ return { answered: true, positions };
6376
+ }
6377
+ catch (error) {
6378
+ __classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Target DEX position query failed; its silence proves nothing', {
6379
+ dex: dexName ?? 'main',
6380
+ error: (0, errorUtils_js_1.ensureError)(error, 'HyperLiquidProvider.queryDexPositions')
6381
+ .message,
6382
+ });
6383
+ return { answered: false, positions: [] };
6384
+ }
5788
6385
  }, _HyperLiquidProvider_getAllMids =
5789
6386
  /**
5790
6387
  * Get allMids for a DEX — uses WS snapshot as primary source, REST as fallback.