@metamask-previews/perps-controller 10.0.0-preview-48f22dbe8 → 10.0.0-preview-a42e8d0d2
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 +0 -15
- package/dist/providers/HyperLiquidProvider.cjs +30 -219
- package/dist/providers/HyperLiquidProvider.cjs.map +1 -1
- package/dist/providers/HyperLiquidProvider.d.cts.map +1 -1
- package/dist/providers/HyperLiquidProvider.d.mts.map +1 -1
- package/dist/providers/HyperLiquidProvider.mjs +31 -220
- package/dist/providers/HyperLiquidProvider.mjs.map +1 -1
- package/dist/services/HyperLiquidSubscriptionService.cjs +0 -23
- package/dist/services/HyperLiquidSubscriptionService.cjs.map +1 -1
- package/dist/services/HyperLiquidSubscriptionService.d.cts +0 -21
- package/dist/services/HyperLiquidSubscriptionService.d.cts.map +1 -1
- package/dist/services/HyperLiquidSubscriptionService.d.mts +0 -21
- package/dist/services/HyperLiquidSubscriptionService.d.mts.map +1 -1
- package/dist/services/HyperLiquidSubscriptionService.mjs +0 -23
- package/dist/services/HyperLiquidSubscriptionService.mjs.map +1 -1
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.d.cts +2 -18
- package/dist/types/index.d.cts.map +1 -1
- package/dist/types/index.d.mts +2 -18
- package/dist/types/index.d.mts.map +1 -1
- package/dist/types/index.mjs.map +1 -1
- package/dist/utils/orderCalculations.cjs +21 -126
- package/dist/utils/orderCalculations.cjs.map +1 -1
- package/dist/utils/orderCalculations.d.cts +0 -24
- package/dist/utils/orderCalculations.d.cts.map +1 -1
- package/dist/utils/orderCalculations.d.mts +0 -24
- package/dist/utils/orderCalculations.d.mts.map +1 -1
- package/dist/utils/orderCalculations.mjs +20 -124
- package/dist/utils/orderCalculations.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,21 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
### Added
|
|
11
|
-
|
|
12
|
-
- Add `floorToSizeDecimals(size, szDecimals)` (exported from `@metamask/perps-controller/utils/*`), which rounds an order size down onto an asset's size grid, snapping values that floating-point error leaves just below a grid point. The result is never greater than the input: a value genuinely below a grid point is truncated rather than snapped up ([#9719](https://github.com/MetaMask/core/pull/9719))
|
|
13
|
-
|
|
14
|
-
### Fixed
|
|
15
|
-
|
|
16
|
-
- Stop `closePosition` from submitting reduce-only orders that HyperLiquid rejects with "Reduce only order would increase position" ([#9719](https://github.com/MetaMask/core/pull/9719))
|
|
17
|
-
- The position snapshot callers pass (to avoid a `getPositions()` REST call) is now re-validated against the freshest WebSocket position cache, so the order side and size follow the live position instead of a snapshot that a concurrent TP/SL fill, liquidation, or repeated close has already invalidated. No additional network request is made when the cache covers the symbol's DEX: a missing entry there means the position is already closed, so the close now fails fast with `No position found for <symbol>` instead of submitting a doomed order. When the cache does not cover that DEX — a HIP-3 DEX whose subscription has not published this session — the absence proves nothing, so a single `clearinghouseState` request for that DEX alone supplies live data, which keeps the outcome attributable to the symbol's own DEX: if the DEX answers without this symbol — including when it reports no positions at all — the position is genuinely closed and the close fails with `No position found for <symbol>`; only if that request fails does the caller's snapshot stand, since a failed lookup proves nothing and must not block a position that is open and closable. `HyperLiquidSubscriptionService` exposes the new `getCachedPositionsForDex(dexName)` method this uses, which returns that DEX's own cached positions rather than the cross-DEX aggregate: the aggregate is only rebuilt once every expected DEX has published, so after a WebSocket reconnect it can sit frozen at pre-reconnect contents while the per-DEX slices keep updating.
|
|
18
|
-
- A caller-supplied close size is clamped to the live position size, and that clamp is binding: for a partial close the `usdAmount` clients also send can no longer recompute the size above it. A close size that is supplied but not a positive number (e.g. `'0'` or `'abc'`) now fails with `ORDER_SIZE_POSITIVE`; only an omitted or empty `size` means "close 100%".
|
|
19
|
-
- The batch `closePositions` path also rounds each reduce-only size down onto the asset's size grid, instead of letting `formatHyperLiquidSize`'s half-up rounding push it above the position. A position smaller than one size increment is skipped rather than submitted as a zero-size order, and the remaining positions still close. Each skipped position is reported in `results` with `success: false` and `error: ORDER_SIZE_POSITIVE` and counted in `failureCount`, so a caller cannot read "closed everything" from a batch that left one open. `results` keeps the order of the requested positions, so a consumer correlating results to positions by index is unaffected by a skip.
|
|
20
|
-
- A close that covers the whole position — no `size`, or a `size` that reaches (or was clamped to) the position size — now submits exactly the live position size. The `usdAmount` clients send for slippage protection is no longer forwarded for such a close, because `placeOrder` treats it as the source of truth and would recompute the size from it, discarding the clamp and rounding the size up. Genuine partial closes still use `usdAmount`. As a result, a close of the entire position requested via an explicit `size` is now treated as a full close for validation too, so it skips the USD/$10-minimum check as an omitted `size` already did. The `priceAtCalculation` staleness check is unaffected: `calculateFinalPositionSize` now runs it whenever a caller supplies that field, rather than only inside its `usdAmount` branch, so a full close that drifted past `maxSlippageBps` is still rejected with "Price moved too much" even though its size no longer comes from `usdAmount`.
|
|
21
|
-
- `placeOrder` no longer retries **any** reduce-only order that the exchange rejected for the $10 minimum order value with a 1.5% larger size; the minimum-value error is surfaced instead. This covers reduce-only limit and TP/SL orders submitted directly through `placeOrder`, not just closes. **It is a behaviour change for partial closes**, which previously recovered from that rejection by closing ~1.5% more than requested: a reduce-only order can no longer grow past the position (full close) or past the size the caller asked to close (partial close), so the retry could only be rejected again or resubmit an identical order.
|
|
22
|
-
- `calculateFinalPositionSize` throws `ORDER_SIZE_POSITIVE` when a `reduceOnly` call supplies a `size` that is not a positive number, in both its `usdAmount` and legacy-size branches, instead of capping the USD-derived size to that value or passing it through to be formatted as a zero or negative size.
|
|
23
|
-
- `calculateFinalPositionSize` accepts an optional `reduceOnly` flag. When set, the size is rounded down onto the asset's size grid and the "add one increment to meet the requested USD" adjustment is skipped, so a reduce-only size can never round up past the position. If rounding down leaves a size of `0` (the order is worth less than one size increment), it throws `ORDER_SIZE_POSITIVE` rather than submitting a zero-size order. **This is a behaviour change for a reduce-only close between half an increment and one full increment**: `formatHyperLiquidSize` uses `toFixed`, which rounds half-up, so such a close previously succeeded by closing one whole increment and now fails client-side instead. It is most visible on coarse-grid (low `szDecimals`) assets, and reachable from any partial close that omits `usdAmount` — Mobile limit partial closes do (`usePerpsClosePosition` sends `usdAmount: undefined` for limit orders).
|
|
24
|
-
|
|
25
10
|
## [10.0.0]
|
|
26
11
|
|
|
27
12
|
### Added
|
|
@@ -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,
|
|
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;
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.HyperLiquidProvider = void 0;
|
|
16
16
|
const utils_1 = require("@metamask/utils");
|
|
@@ -362,7 +362,6 @@ class HyperLiquidProvider {
|
|
|
362
362
|
maxSlippageBps: normalizedMaxSlippageBps,
|
|
363
363
|
szDecimals: assetInfo.szDecimals,
|
|
364
364
|
leverage: params.leverage,
|
|
365
|
-
reduceOnly: params.reduceOnly,
|
|
366
365
|
});
|
|
367
366
|
const { orderPrice, formattedSize, formattedPrice } = (0, orderCalculations_js_1.calculateOrderPriceAndSize)({
|
|
368
367
|
orderType: params.orderType,
|
|
@@ -437,13 +436,7 @@ class HyperLiquidProvider {
|
|
|
437
436
|
const errorMessage = (0, errorUtils_js_1.ensureError)(error, 'HyperLiquidProvider.placeOrder').message;
|
|
438
437
|
const isMinimumOrderError = errorMessage.includes('Order must have minimum value of $10') ||
|
|
439
438
|
errorMessage.includes('Order 0: Order must have minimum value');
|
|
440
|
-
|
|
441
|
-
// 1.5%, which a close cannot do: a full close already submits the whole
|
|
442
|
-
// position, and a partial close is capped at the size the caller asked to
|
|
443
|
-
// close, so the retry would either be rejected as "Reduce only order would
|
|
444
|
-
// increase position" or resubmit an identical order. Surfacing the
|
|
445
|
-
// minimum-value error names the real problem instead.
|
|
446
|
-
if (isMinimumOrderError && retryCount === 0 && !params.reduceOnly) {
|
|
439
|
+
if (isMinimumOrderError && retryCount === 0) {
|
|
447
440
|
let adjustedUsdAmount;
|
|
448
441
|
let originalValue;
|
|
449
442
|
if (params.usdAmount) {
|
|
@@ -751,18 +744,10 @@ class HyperLiquidProvider {
|
|
|
751
744
|
...new Set(positionsToClose.map((pos) => (0, hyperLiquidAdapter_js_1.parseAssetName)(pos.symbol).dex ?? 'main')),
|
|
752
745
|
];
|
|
753
746
|
await Promise.all(uniqueDexs.map((dex) => __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_getCachedMeta).call(this, { dexName: dex === 'main' ? null : dex })));
|
|
754
|
-
//
|
|
755
|
-
|
|
756
|
-
//
|
|
757
|
-
// order in a mixed main-DEX/HIP-3 batch.
|
|
758
|
-
const orderedHip3Transfers = [];
|
|
759
|
-
// Build orders array, plus the positions each order closes so response
|
|
760
|
-
// statuses stay index-aligned when a position is skipped below
|
|
747
|
+
// Track HIP-3 positions and freed margins for post-close transfers
|
|
748
|
+
const hip3Transfers = [];
|
|
749
|
+
// Build orders array
|
|
761
750
|
const orders = [];
|
|
762
|
-
const orderedPositions = [];
|
|
763
|
-
// Positions no order could be built for. Reported as failures so a caller
|
|
764
|
-
// cannot read "closed everything" from a result that left one open.
|
|
765
|
-
const skippedResults = [];
|
|
766
751
|
for (const position of positionsToClose) {
|
|
767
752
|
// Extract DEX name for HIP-3 positions
|
|
768
753
|
const { dex: dexName } = (0, hyperLiquidAdapter_js_1.parseAssetName)(position.symbol);
|
|
@@ -784,29 +769,13 @@ class HyperLiquidProvider {
|
|
|
784
769
|
const isBuy = positionSize < 0; // Close opposite side
|
|
785
770
|
const closeSize = Math.abs(positionSize);
|
|
786
771
|
const totalMarginUsed = parseFloat(position.marginUsed);
|
|
787
|
-
//
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
// would submit a zero-size order. Skip it rather than sending an order
|
|
793
|
-
// the exchange must reject; the remaining positions still close, and the
|
|
794
|
-
// skip is reported as a failure below.
|
|
795
|
-
if (flooredCloseSize <= 0) {
|
|
796
|
-
__classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Skipping position smaller than one size increment', { coin: position.symbol, size: position.size });
|
|
797
|
-
skippedResults.push({
|
|
798
|
-
symbol: position.symbol,
|
|
799
|
-
success: false,
|
|
800
|
-
error: perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_SIZE_POSITIVE,
|
|
772
|
+
// Track HIP-3 transfers (full position close means all margin is freed)
|
|
773
|
+
if (isHip3Position && dexName && !__classPrivateFieldGet(this, _HyperLiquidProvider_useUnifiedAccount, "f")) {
|
|
774
|
+
hip3Transfers.push({
|
|
775
|
+
sourceDex: dexName,
|
|
776
|
+
freedMargin: totalMarginUsed,
|
|
801
777
|
});
|
|
802
|
-
continue;
|
|
803
778
|
}
|
|
804
|
-
// Track this order's HIP-3 transfer, if it needs one (a full position
|
|
805
|
-
// close frees all of its margin). Pushed below alongside the order so the
|
|
806
|
-
// two stay index-aligned.
|
|
807
|
-
const hip3Transfer = isHip3Position && dexName && !__classPrivateFieldGet(this, _HyperLiquidProvider_useUnifiedAccount, "f")
|
|
808
|
-
? { sourceDex: dexName, freedMargin: totalMarginUsed }
|
|
809
|
-
: null;
|
|
810
779
|
const currentPrice = await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_getOrFetchPrice).call(this, {
|
|
811
780
|
symbol: position.symbol,
|
|
812
781
|
dexName: dexName ?? null,
|
|
@@ -816,8 +785,9 @@ class HyperLiquidProvider {
|
|
|
816
785
|
const orderPrice = isBuy
|
|
817
786
|
? currentPrice * (1 + slippage)
|
|
818
787
|
: currentPrice * (1 - slippage);
|
|
788
|
+
// Format size and price
|
|
819
789
|
const formattedSize = (0, hyperLiquidAdapter_js_1.formatHyperLiquidSize)({
|
|
820
|
-
size:
|
|
790
|
+
size: closeSize,
|
|
821
791
|
szDecimals: assetInfo.szDecimals,
|
|
822
792
|
});
|
|
823
793
|
const formattedPrice = (0, hyperLiquidAdapter_js_1.formatHyperLiquidPrice)({
|
|
@@ -833,19 +803,6 @@ class HyperLiquidProvider {
|
|
|
833
803
|
r: true, // reduceOnly
|
|
834
804
|
t: { limit: { tif: 'Ioc' } }, // Immediate or cancel for market-like execution
|
|
835
805
|
});
|
|
836
|
-
orderedPositions.push(position);
|
|
837
|
-
orderedHip3Transfers.push(hip3Transfer);
|
|
838
|
-
}
|
|
839
|
-
// Every position was smaller than one size increment. Return their
|
|
840
|
-
// failures rather than an empty result, which would be indistinguishable
|
|
841
|
-
// from "no positions matched".
|
|
842
|
-
if (orders.length === 0) {
|
|
843
|
-
return {
|
|
844
|
-
success: false,
|
|
845
|
-
successCount: 0,
|
|
846
|
-
failureCount: skippedResults.length,
|
|
847
|
-
results: skippedResults,
|
|
848
|
-
};
|
|
849
806
|
}
|
|
850
807
|
// Calculate discounted builder fee if reward discount is active
|
|
851
808
|
let builderFee = hyperLiquidConfig_js_1.BUILDER_FEE_CONFIG.MaxFeeTenthsBps;
|
|
@@ -865,17 +822,16 @@ class HyperLiquidProvider {
|
|
|
865
822
|
const { statuses } = result.response.data;
|
|
866
823
|
const successCount = statuses.filter((stat) => isStatusObject(stat) &&
|
|
867
824
|
((0, utils_1.hasProperty)(stat, 'filled') || (0, utils_1.hasProperty)(stat, 'resting'))).length;
|
|
868
|
-
const failureCount = statuses.length - successCount
|
|
825
|
+
const failureCount = statuses.length - successCount;
|
|
869
826
|
// Handle HIP-3 margin transfers for successful closes
|
|
870
827
|
if (!__classPrivateFieldGet(this, _HyperLiquidProvider_useUnifiedAccount, "f")) {
|
|
871
828
|
for (let i = 0; i < statuses.length; i++) {
|
|
872
829
|
const status = statuses[i];
|
|
873
830
|
const isSuccess = isStatusObject(status) &&
|
|
874
831
|
((0, utils_1.hasProperty)(status, 'filled') || (0, utils_1.hasProperty)(status, 'resting'));
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
__classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Position closed successfully, initiating manual auto-transfer back', { symbol: orderedPositions[i].symbol, freedMargin });
|
|
832
|
+
if (isSuccess && hip3Transfers[i]) {
|
|
833
|
+
const { sourceDex, freedMargin } = hip3Transfers[i];
|
|
834
|
+
__classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Position closed successfully, initiating manual auto-transfer back', { symbol: positionsToClose[i].symbol, freedMargin });
|
|
879
835
|
// Non-blocking: Transfer freed margin back to main DEX
|
|
880
836
|
await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_autoTransferBackAfterClose).call(this, {
|
|
881
837
|
sourceDex,
|
|
@@ -884,29 +840,18 @@ class HyperLiquidProvider {
|
|
|
884
840
|
}
|
|
885
841
|
}
|
|
886
842
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
{
|
|
892
|
-
symbol:
|
|
843
|
+
return {
|
|
844
|
+
success: successCount > 0,
|
|
845
|
+
successCount,
|
|
846
|
+
failureCount,
|
|
847
|
+
results: statuses.map((status, index) => ({
|
|
848
|
+
symbol: positionsToClose[index].symbol,
|
|
893
849
|
success: isStatusObject(status) &&
|
|
894
850
|
((0, utils_1.hasProperty)(status, 'filled') || (0, utils_1.hasProperty)(status, 'resting')),
|
|
895
851
|
error: isStatusObject(status) && (0, utils_1.hasProperty)(status, 'error')
|
|
896
852
|
? String(status.error)
|
|
897
853
|
: undefined,
|
|
898
|
-
},
|
|
899
|
-
]));
|
|
900
|
-
const skippedBySymbol = new Map(skippedResults.map((skipped) => [skipped.symbol, skipped]));
|
|
901
|
-
return {
|
|
902
|
-
success: successCount > 0,
|
|
903
|
-
successCount,
|
|
904
|
-
failureCount,
|
|
905
|
-
results: positionsToClose.flatMap((position) => {
|
|
906
|
-
const outcome = submittedResults.get(position.symbol) ??
|
|
907
|
-
skippedBySymbol.get(position.symbol);
|
|
908
|
-
return outcome ? [outcome] : [];
|
|
909
|
-
}),
|
|
854
|
+
})),
|
|
910
855
|
};
|
|
911
856
|
}
|
|
912
857
|
catch (error) {
|
|
@@ -1185,68 +1130,6 @@ class HyperLiquidProvider {
|
|
|
1185
1130
|
// Use provided position (from WebSocket) or fetch from cache
|
|
1186
1131
|
// This avoids unnecessary API calls and prevents 429 rate limiting
|
|
1187
1132
|
let { position } = params;
|
|
1188
|
-
// Re-validate the caller-supplied snapshot against the freshest WebSocket
|
|
1189
|
-
// position cache. Clients pass a throttled snapshot (~1s old on mobile),
|
|
1190
|
-
// so a concurrent TP/SL fill, a liquidation, or a double-tapped close
|
|
1191
|
-
// leaves the snapshot's side/size larger than (or opposite to) the real
|
|
1192
|
-
// position and HyperLiquid rejects the reduce-only order with "Reduce
|
|
1193
|
-
// only order would increase position". Reading the cache never issues a
|
|
1194
|
-
// REST request, so this does not reintroduce 429 rate limiting.
|
|
1195
|
-
if (position && __classPrivateFieldGet(this, _HyperLiquidProvider_subscriptionService, "f").isPositionsCacheInitialized()) {
|
|
1196
|
-
// Read the symbol's own DEX slice, not the aggregate. The aggregate is
|
|
1197
|
-
// only rebuilt once every expected DEX has published, so after a
|
|
1198
|
-
// WebSocket reconnect — which resets the initialized-DEX set without
|
|
1199
|
-
// clearing these caches — it can sit frozen at pre-reconnect contents
|
|
1200
|
-
// while the per-DEX slices keep updating. Deciding "this DEX is covered"
|
|
1201
|
-
// from the per-DEX map and then reading the position from the aggregate
|
|
1202
|
-
// mixed a fresh answer with stale data: a close could reuse a stale size,
|
|
1203
|
-
// or throw for a position that is open.
|
|
1204
|
-
const dexPositions = __classPrivateFieldGet(this, _HyperLiquidProvider_subscriptionService, "f").getCachedPositionsForDex((0, hyperLiquidAdapter_js_1.parseAssetName)(params.symbol).dex ?? '');
|
|
1205
|
-
const livePosition = dexPositions?.find((pos) => pos.symbol === params.symbol);
|
|
1206
|
-
if (livePosition) {
|
|
1207
|
-
if (livePosition.size !== position.size) {
|
|
1208
|
-
__classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Stale close position snapshot: using live WebSocket position', {
|
|
1209
|
-
coin: params.symbol,
|
|
1210
|
-
snapshotSize: position.size,
|
|
1211
|
-
liveSize: livePosition.size,
|
|
1212
|
-
});
|
|
1213
|
-
}
|
|
1214
|
-
position = livePosition;
|
|
1215
|
-
}
|
|
1216
|
-
else if (dexPositions) {
|
|
1217
|
-
// That DEX has published and does not hold this symbol, so the position
|
|
1218
|
-
// is already closed (e.g. a double-tapped close). This is the same read
|
|
1219
|
-
// the lookup above used, so the two can never disagree. Fail here rather
|
|
1220
|
-
// than falling back to REST: the cache is the freshest source, so a REST
|
|
1221
|
-
// lookup can only burn a request that risks 429s and, if it lags, hand
|
|
1222
|
-
// back a position that no longer exists.
|
|
1223
|
-
throw new Error(`No position found for ${params.symbol}`);
|
|
1224
|
-
}
|
|
1225
|
-
else {
|
|
1226
|
-
// The cache holds nothing for this symbol's DEX — a HIP-3 DEX whose
|
|
1227
|
-
// subscription has not published this session — so the symbol's
|
|
1228
|
-
// absence proves nothing. Spend one REST request to get live data
|
|
1229
|
-
// rather than trusting a snapshot the exchange may have moved past.
|
|
1230
|
-
__classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Position cache does not cover this DEX: fetching live positions', { coin: params.symbol });
|
|
1231
|
-
// Query the symbol's own DEX so the outcome carries provenance.
|
|
1232
|
-
// getPositions() fans out across every enabled DEX, flattens the subset
|
|
1233
|
-
// that answered and turns any failure into [], so it cannot distinguish
|
|
1234
|
-
// "this DEX answered and holds nothing" from "this DEX failed or was
|
|
1235
|
-
// never queried" — and those two need opposite decisions.
|
|
1236
|
-
const { answered, positions } = await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_queryDexPositions).call(this, (0, hyperLiquidAdapter_js_1.parseAssetName)(params.symbol).dex);
|
|
1237
|
-
const livePositionFromApi = positions.find((pos) => pos.symbol === params.symbol);
|
|
1238
|
-
if (livePositionFromApi) {
|
|
1239
|
-
position = livePositionFromApi;
|
|
1240
|
-
}
|
|
1241
|
-
else if (answered) {
|
|
1242
|
-
// The DEX answered without this symbol — even with no positions at
|
|
1243
|
-
// all — so it is genuinely closed.
|
|
1244
|
-
throw new Error(`No position found for ${params.symbol}`);
|
|
1245
|
-
}
|
|
1246
|
-
// Otherwise the query failed, so the absence proves nothing: keep the
|
|
1247
|
-
// caller's snapshot rather than block a position that may be closable.
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
1133
|
if (!position) {
|
|
1251
1134
|
const positions = await this.getPositions();
|
|
1252
1135
|
position = positions.find((pos) => pos.symbol === params.symbol);
|
|
@@ -1256,37 +1139,18 @@ class HyperLiquidProvider {
|
|
|
1256
1139
|
}
|
|
1257
1140
|
const positionSize = parseFloat(position.size);
|
|
1258
1141
|
const isBuy = positionSize < 0;
|
|
1259
|
-
const
|
|
1260
|
-
// Only an omitted (or empty) size means "close 100%". A supplied size must
|
|
1261
|
-
// be a positive number: silently promoting '0' or 'abc' to a full close
|
|
1262
|
-
// would liquidate the whole position on a caller-side formatting slip.
|
|
1263
|
-
// A supplied size is clamped to the live position size, because
|
|
1264
|
-
// HyperLiquid rejects reduce-only orders that exceed the position and the
|
|
1265
|
-
// caller computed its size from a snapshot that may already be too large.
|
|
1266
|
-
const hasRequestedSize = params.size !== undefined && params.size !== '';
|
|
1267
|
-
let closeSizeNumber = absPositionSize;
|
|
1268
|
-
if (hasRequestedSize) {
|
|
1269
|
-
const requestedSize = parseFloat(params.size);
|
|
1270
|
-
if (!Number.isFinite(requestedSize) || requestedSize <= 0) {
|
|
1271
|
-
throw new Error(perpsErrorCodes_js_1.PERPS_ERROR_CODES.ORDER_SIZE_POSITIVE);
|
|
1272
|
-
}
|
|
1273
|
-
closeSizeNumber = Math.min(requestedSize, absPositionSize);
|
|
1274
|
-
}
|
|
1275
|
-
const closeSize = closeSizeNumber.toString();
|
|
1142
|
+
const closeSize = params.size ?? Math.abs(positionSize).toString();
|
|
1276
1143
|
// Capture position details BEFORE closing for freed margin calculation
|
|
1277
1144
|
const totalMarginUsed = parseFloat(position.marginUsed);
|
|
1278
|
-
const totalPositionSize =
|
|
1279
|
-
const closeSizeNum =
|
|
1145
|
+
const totalPositionSize = Math.abs(positionSize);
|
|
1146
|
+
const closeSizeNum = parseFloat(closeSize);
|
|
1280
1147
|
const isHip3Position = position.symbol.includes(':');
|
|
1281
1148
|
const hip3Dex = isHip3Position ? position.symbol.split(':')[0] : null;
|
|
1282
1149
|
// Calculate freed margin proportionally
|
|
1283
1150
|
const freedMarginRatio = closeSizeNum / totalPositionSize;
|
|
1284
1151
|
const freedMargin = totalMarginUsed * freedMarginRatio;
|
|
1285
|
-
// Get current price for
|
|
1286
|
-
//
|
|
1287
|
-
// but not the price-staleness guard: calculateFinalPositionSize checks
|
|
1288
|
-
// priceAtCalculation against the live price for every close that supplies
|
|
1289
|
-
// it, using the price placeOrder fetches when none is passed here.
|
|
1152
|
+
// Get current price for validation if not provided (and not a full close)
|
|
1153
|
+
// Full closes don't need price for validation
|
|
1290
1154
|
let { currentPrice } = params;
|
|
1291
1155
|
if (!currentPrice && params.size && !params.usdAmount) {
|
|
1292
1156
|
// Partial close without USD or price: use limit price as fallback for validation
|
|
@@ -1309,10 +1173,6 @@ class HyperLiquidProvider {
|
|
|
1309
1173
|
closedSize: closeSize,
|
|
1310
1174
|
freedMargin: freedMargin.toFixed(2),
|
|
1311
1175
|
});
|
|
1312
|
-
// True when the order closes 100% of the position: either no size was
|
|
1313
|
-
// provided, or the requested size covers (or was clamped to) the whole
|
|
1314
|
-
// position.
|
|
1315
|
-
const isFullClose = closeSizeNum >= absPositionSize;
|
|
1316
1176
|
// Execute position close with consistent slippage handling
|
|
1317
1177
|
const result = await this.placeOrder({
|
|
1318
1178
|
symbol: params.symbol,
|
|
@@ -1321,17 +1181,10 @@ class HyperLiquidProvider {
|
|
|
1321
1181
|
orderType: params.orderType ?? 'market',
|
|
1322
1182
|
price: params.price,
|
|
1323
1183
|
reduceOnly: true,
|
|
1324
|
-
isFullClose,
|
|
1184
|
+
isFullClose: !params.size, // True if closing 100% (size not provided)
|
|
1325
1185
|
// Pass through price and slippage parameters for consistent validation
|
|
1326
1186
|
currentPrice,
|
|
1327
|
-
|
|
1328
|
-
// size. Forwarding usdAmount would make placeOrder recompute the size as
|
|
1329
|
-
// usdAmount / currentPrice — discarding the clamp above, since usdAmount
|
|
1330
|
-
// is the source of truth there — and submit more than the position
|
|
1331
|
-
// holds, which is rejected with "Reduce only order would increase
|
|
1332
|
-
// position". Genuine partial closes keep usdAmount so their size stays
|
|
1333
|
-
// USD-accurate.
|
|
1334
|
-
usdAmount: isFullClose ? undefined : params.usdAmount,
|
|
1187
|
+
usdAmount: params.usdAmount,
|
|
1335
1188
|
priceAtCalculation: params.priceAtCalculation,
|
|
1336
1189
|
maxSlippageBps: params.maxSlippageBps,
|
|
1337
1190
|
});
|
|
@@ -5932,48 +5785,6 @@ async function _HyperLiquidProvider_getStandaloneValidatedDexs() {
|
|
|
5932
5785
|
// buildAssetMapping uses state.raw for perpDexIndex computation.
|
|
5933
5786
|
const state = __classPrivateFieldGet(this, _HyperLiquidProvider_dexDiscoveryCache, "f").update(allDexs);
|
|
5934
5787
|
return state.validated;
|
|
5935
|
-
}, _HyperLiquidProvider_queryDexPositions =
|
|
5936
|
-
/**
|
|
5937
|
-
* Query one DEX's positions directly, preserving whether that DEX answered.
|
|
5938
|
-
*
|
|
5939
|
-
* `getPositions()` fans out across every enabled DEX, flattens the subset that
|
|
5940
|
-
* answered and converts any thrown error into an empty array, so its result
|
|
5941
|
-
* cannot distinguish "this DEX answered and holds no positions" from "this
|
|
5942
|
-
* DEX's request failed or it was never queried". `closePosition` needs that
|
|
5943
|
-
* distinction: the first means the position is closed and the close must fail
|
|
5944
|
-
* before submitting, the second means the absence proves nothing and the
|
|
5945
|
-
* caller's snapshot should stand.
|
|
5946
|
-
*
|
|
5947
|
-
* TP/SL enrichment is skipped, as in standalone mode: the close path only reads
|
|
5948
|
-
* size, side and margin.
|
|
5949
|
-
*
|
|
5950
|
-
* @param dexName - DEX identifier, or null for the main DEX.
|
|
5951
|
-
* @returns Whether the DEX answered, and the positions it reported.
|
|
5952
|
-
*/
|
|
5953
|
-
async function _HyperLiquidProvider_queryDexPositions(dexName) {
|
|
5954
|
-
try {
|
|
5955
|
-
await __classPrivateFieldGet(this, _HyperLiquidProvider_instances, "m", _HyperLiquidProvider_ensureClientsInitialized).call(this);
|
|
5956
|
-
__classPrivateFieldGet(this, _HyperLiquidProvider_clientService, "f").ensureInitialized();
|
|
5957
|
-
const infoClient = __classPrivateFieldGet(this, _HyperLiquidProvider_clientService, "f").getInfoClient();
|
|
5958
|
-
const userAddress = await __classPrivateFieldGet(this, _HyperLiquidProvider_walletService, "f").getUserAddressWithDefault();
|
|
5959
|
-
const state = await infoClient.clearinghouseState(dexName ? { user: userAddress, dex: dexName } : { user: userAddress });
|
|
5960
|
-
const positions = (state.assetPositions ?? [])
|
|
5961
|
-
.filter((assetPos) => assetPos.position.szi !== '0')
|
|
5962
|
-
.map((assetPos) => (0, hyperLiquidAdapter_js_1.adaptPositionFromSDK)(assetPos));
|
|
5963
|
-
__classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Target DEX position query answered', {
|
|
5964
|
-
dex: dexName ?? 'main',
|
|
5965
|
-
count: positions.length,
|
|
5966
|
-
});
|
|
5967
|
-
return { answered: true, positions };
|
|
5968
|
-
}
|
|
5969
|
-
catch (error) {
|
|
5970
|
-
__classPrivateFieldGet(this, _HyperLiquidProvider_deps, "f").debugLogger.log('Target DEX position query failed; its silence proves nothing', {
|
|
5971
|
-
dex: dexName ?? 'main',
|
|
5972
|
-
error: (0, errorUtils_js_1.ensureError)(error, 'HyperLiquidProvider.queryDexPositions')
|
|
5973
|
-
.message,
|
|
5974
|
-
});
|
|
5975
|
-
return { answered: false, positions: [] };
|
|
5976
|
-
}
|
|
5977
5788
|
}, _HyperLiquidProvider_getAllMids =
|
|
5978
5789
|
/**
|
|
5979
5790
|
* Get allMids for a DEX — uses WS snapshot as primary source, REST as fallback.
|