@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.
- package/CHANGELOG.md +77 -0
- package/dist/constants/eventNames.cjs +6 -0
- package/dist/constants/eventNames.cjs.map +1 -1
- package/dist/constants/eventNames.d.cts +4 -0
- package/dist/constants/eventNames.d.cts.map +1 -1
- package/dist/constants/eventNames.d.mts +4 -0
- package/dist/constants/eventNames.d.mts.map +1 -1
- package/dist/constants/eventNames.mjs +6 -0
- package/dist/constants/eventNames.mjs.map +1 -1
- package/dist/index.cjs +86 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +3 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -1
- package/dist/perpsErrorCodes.cjs +16 -0
- package/dist/perpsErrorCodes.cjs.map +1 -1
- package/dist/perpsErrorCodes.d.cts +12 -0
- package/dist/perpsErrorCodes.d.cts.map +1 -1
- package/dist/perpsErrorCodes.d.mts +12 -0
- package/dist/perpsErrorCodes.d.mts.map +1 -1
- package/dist/perpsErrorCodes.mjs +16 -0
- package/dist/perpsErrorCodes.mjs.map +1 -1
- package/dist/providers/HyperLiquidProvider.cjs +674 -77
- package/dist/providers/HyperLiquidProvider.cjs.map +1 -1
- package/dist/providers/HyperLiquidProvider.d.cts +13 -0
- package/dist/providers/HyperLiquidProvider.d.cts.map +1 -1
- package/dist/providers/HyperLiquidProvider.d.mts +13 -0
- package/dist/providers/HyperLiquidProvider.d.mts.map +1 -1
- package/dist/providers/HyperLiquidProvider.mjs +676 -79
- package/dist/providers/HyperLiquidProvider.mjs.map +1 -1
- package/dist/selectors.cjs.map +1 -1
- package/dist/selectors.d.cts +17 -17
- package/dist/selectors.d.cts.map +1 -1
- package/dist/selectors.d.mts +17 -17
- package/dist/selectors.d.mts.map +1 -1
- package/dist/selectors.mjs.map +1 -1
- package/dist/services/HyperLiquidSubscriptionService.cjs +121 -11
- package/dist/services/HyperLiquidSubscriptionService.cjs.map +1 -1
- package/dist/services/HyperLiquidSubscriptionService.d.cts +21 -0
- package/dist/services/HyperLiquidSubscriptionService.d.cts.map +1 -1
- package/dist/services/HyperLiquidSubscriptionService.d.mts +21 -0
- package/dist/services/HyperLiquidSubscriptionService.d.mts.map +1 -1
- package/dist/services/HyperLiquidSubscriptionService.mjs +121 -11
- package/dist/services/HyperLiquidSubscriptionService.mjs.map +1 -1
- package/dist/services/TradingService.cjs +6 -2
- package/dist/services/TradingService.cjs.map +1 -1
- package/dist/services/TradingService.d.cts.map +1 -1
- package/dist/services/TradingService.d.mts.map +1 -1
- package/dist/services/TradingService.mjs +6 -2
- package/dist/services/TradingService.mjs.map +1 -1
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.d.cts +69 -4
- package/dist/types/index.d.cts.map +1 -1
- package/dist/types/index.d.mts +69 -4
- package/dist/types/index.d.mts.map +1 -1
- package/dist/types/index.mjs.map +1 -1
- package/dist/types/perps-types.cjs.map +1 -1
- package/dist/types/perps-types.d.cts +35 -1
- package/dist/types/perps-types.d.cts.map +1 -1
- package/dist/types/perps-types.d.mts +35 -1
- package/dist/types/perps-types.d.mts.map +1 -1
- package/dist/types/perps-types.mjs.map +1 -1
- package/dist/utils/hyperLiquidAdapter.cjs +168 -10
- package/dist/utils/hyperLiquidAdapter.cjs.map +1 -1
- package/dist/utils/hyperLiquidAdapter.d.cts +35 -1
- package/dist/utils/hyperLiquidAdapter.d.cts.map +1 -1
- package/dist/utils/hyperLiquidAdapter.d.mts +35 -1
- package/dist/utils/hyperLiquidAdapter.d.mts.map +1 -1
- package/dist/utils/hyperLiquidAdapter.mjs +166 -11
- package/dist/utils/hyperLiquidAdapter.mjs.map +1 -1
- package/dist/utils/hyperLiquidValidation.cjs +160 -5
- package/dist/utils/hyperLiquidValidation.cjs.map +1 -1
- package/dist/utils/hyperLiquidValidation.d.cts +23 -4
- package/dist/utils/hyperLiquidValidation.d.cts.map +1 -1
- package/dist/utils/hyperLiquidValidation.d.mts +23 -4
- package/dist/utils/hyperLiquidValidation.d.mts.map +1 -1
- package/dist/utils/hyperLiquidValidation.mjs +160 -5
- package/dist/utils/hyperLiquidValidation.mjs.map +1 -1
- package/dist/utils/index.cjs +5 -1
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.cts +2 -1
- package/dist/utils/index.d.cts.map +1 -1
- package/dist/utils/index.d.mts +2 -1
- package/dist/utils/index.d.mts.map +1 -1
- package/dist/utils/index.mjs +2 -1
- package/dist/utils/index.mjs.map +1 -1
- package/dist/utils/orderCalculations.cjs +363 -37
- package/dist/utils/orderCalculations.cjs.map +1 -1
- package/dist/utils/orderCalculations.d.cts +87 -2
- package/dist/utils/orderCalculations.d.cts.map +1 -1
- package/dist/utils/orderCalculations.d.mts +87 -2
- package/dist/utils/orderCalculations.d.mts.map +1 -1
- package/dist/utils/orderCalculations.mjs +359 -36
- package/dist/utils/orderCalculations.mjs.map +1 -1
- package/dist/utils/orderTypes.cjs +222 -0
- package/dist/utils/orderTypes.cjs.map +1 -0
- package/dist/utils/orderTypes.d.cts +114 -0
- package/dist/utils/orderTypes.d.cts.map +1 -0
- package/dist/utils/orderTypes.d.mts +114 -0
- package/dist/utils/orderTypes.d.mts.map +1 -0
- package/dist/utils/orderTypes.mjs +210 -0
- package/dist/utils/orderTypes.mjs.map +1 -0
- 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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
//
|
|
748
|
-
|
|
749
|
-
//
|
|
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
|
-
//
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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:
|
|
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
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
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
|
-
|
|
901
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
958
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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 =
|
|
1146
|
-
const closeSizeNum =
|
|
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
|
|
1153
|
-
//
|
|
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
|
|
1613
|
+
isFullClose,
|
|
1185
1614
|
// Pass through price and slippage parameters for consistent validation
|
|
1186
1615
|
currentPrice,
|
|
1187
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
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.
|