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