@lifi/perps-sdk-provider-hyperliquid 16.0.0 → 17.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/cjs/HyperliquidProvider.js +1 -1
  2. package/dist/cjs/HyperliquidProvider.js.map +1 -1
  3. package/dist/cjs/services/getOrder.d.ts.map +1 -1
  4. package/dist/cjs/services/getOrder.js +7 -3
  5. package/dist/cjs/services/getOrder.js.map +1 -1
  6. package/dist/cjs/services/getOrders.d.ts.map +1 -1
  7. package/dist/cjs/services/getOrders.js +4 -23
  8. package/dist/cjs/services/getOrders.js.map +1 -1
  9. package/dist/cjs/services/getWithdrawableBalances.d.ts.map +1 -1
  10. package/dist/cjs/services/getWithdrawableBalances.js +4 -2
  11. package/dist/cjs/services/getWithdrawableBalances.js.map +1 -1
  12. package/dist/cjs/utils/explorer.d.ts +2 -1
  13. package/dist/cjs/utils/explorer.d.ts.map +1 -1
  14. package/dist/cjs/utils/explorer.js +24 -1
  15. package/dist/cjs/utils/explorer.js.map +1 -1
  16. package/dist/cjs/utils/index.d.ts +1 -1
  17. package/dist/cjs/utils/index.d.ts.map +1 -1
  18. package/dist/cjs/utils/index.js +2 -2
  19. package/dist/cjs/utils/index.js.map +1 -1
  20. package/dist/cjs/utils/transferMargin.d.ts +2 -2
  21. package/dist/cjs/utils/transferMargin.d.ts.map +1 -1
  22. package/dist/cjs/utils/transferMargin.js +20 -12
  23. package/dist/cjs/utils/transferMargin.js.map +1 -1
  24. package/dist/cjs/utils/withdrawableBalances.d.ts +1 -1
  25. package/dist/cjs/utils/withdrawableBalances.d.ts.map +1 -1
  26. package/dist/cjs/utils/withdrawableBalances.js +9 -2
  27. package/dist/cjs/utils/withdrawableBalances.js.map +1 -1
  28. package/dist/esm/HyperliquidProvider.js +2 -2
  29. package/dist/esm/HyperliquidProvider.js.map +1 -1
  30. package/dist/esm/services/getOrder.d.ts.map +1 -1
  31. package/dist/esm/services/getOrder.js +7 -3
  32. package/dist/esm/services/getOrder.js.map +1 -1
  33. package/dist/esm/services/getOrders.d.ts.map +1 -1
  34. package/dist/esm/services/getOrders.js +6 -34
  35. package/dist/esm/services/getOrders.js.map +1 -1
  36. package/dist/esm/services/getWithdrawableBalances.d.ts +5 -3
  37. package/dist/esm/services/getWithdrawableBalances.d.ts.map +1 -1
  38. package/dist/esm/services/getWithdrawableBalances.js +10 -6
  39. package/dist/esm/services/getWithdrawableBalances.js.map +1 -1
  40. package/dist/esm/utils/explorer.d.ts +9 -1
  41. package/dist/esm/utils/explorer.d.ts.map +1 -1
  42. package/dist/esm/utils/explorer.js +34 -1
  43. package/dist/esm/utils/explorer.js.map +1 -1
  44. package/dist/esm/utils/index.d.ts +1 -1
  45. package/dist/esm/utils/index.d.ts.map +1 -1
  46. package/dist/esm/utils/index.js +1 -1
  47. package/dist/esm/utils/index.js.map +1 -1
  48. package/dist/esm/utils/transferMargin.d.ts +12 -8
  49. package/dist/esm/utils/transferMargin.d.ts.map +1 -1
  50. package/dist/esm/utils/transferMargin.js +30 -18
  51. package/dist/esm/utils/transferMargin.js.map +1 -1
  52. package/dist/esm/utils/withdrawableBalances.d.ts +3 -1
  53. package/dist/esm/utils/withdrawableBalances.d.ts.map +1 -1
  54. package/dist/esm/utils/withdrawableBalances.js +11 -2
  55. package/dist/esm/utils/withdrawableBalances.js.map +1 -1
  56. package/dist/types/services/getOrder.d.ts.map +1 -1
  57. package/dist/types/services/getOrders.d.ts.map +1 -1
  58. package/dist/types/services/getWithdrawableBalances.d.ts +5 -3
  59. package/dist/types/services/getWithdrawableBalances.d.ts.map +1 -1
  60. package/dist/types/utils/explorer.d.ts +9 -1
  61. package/dist/types/utils/explorer.d.ts.map +1 -1
  62. package/dist/types/utils/index.d.ts +1 -1
  63. package/dist/types/utils/index.d.ts.map +1 -1
  64. package/dist/types/utils/transferMargin.d.ts +12 -8
  65. package/dist/types/utils/transferMargin.d.ts.map +1 -1
  66. package/dist/types/utils/withdrawableBalances.d.ts +3 -1
  67. package/dist/types/utils/withdrawableBalances.d.ts.map +1 -1
  68. package/package.json +3 -3
  69. package/src/HyperliquidProvider.ts +2 -2
  70. package/src/services/getOrder.ts +10 -3
  71. package/src/services/getOrders.ts +5 -56
  72. package/src/services/getWithdrawableBalances.ts +32 -22
  73. package/src/utils/explorer.ts +51 -2
  74. package/src/utils/index.ts +1 -1
  75. package/src/utils/transferMargin.ts +31 -20
  76. package/src/utils/withdrawableBalances.ts +14 -2
@@ -1,14 +1,18 @@
1
- import { type Position, type PositionMarginConstraints } from '@lifi/perps-types';
1
+ import { type Position } from '@lifi/perps-types';
2
2
  /**
3
- * Hyperliquid's exact documented transfer-margin requirement:
4
- * `max(initial_margin_required, 0.1 * total_position_value)`.
5
- *
6
- * Strict-isolated markets still return constraints because margin can be
7
- * added; their market capability prevents removal. Cross positions and
8
- * markets without individual margin adjustment return `undefined`.
3
+ * Hyperliquid removable isolated margin:
4
+ * `marginUsed − max(initial_margin_required, 0.1 × total_position_value)`.
5
+ * Hyperliquid isolated `marginUsed` already includes the unrealized PnL of
6
+ * the position, so the PnL is not added again, and it can reach zero or less
7
+ * before liquidation.
9
8
  *
9
+ * @returns `undefined` for cross positions and markets without individual
10
+ * margin adjustment; `'0'` for add-only strict-isolated markets and when
11
+ * nothing is removable.
12
+ * @throws {PerpsError} `ValidationError` when a required `Position` decimal
13
+ * is malformed, or when a size, price or requirement is not positive.
10
14
  * @see https://hyperliquid.gitbook.io/hyperliquid-docs/trading/margining
11
15
  * @public
12
16
  */
13
- export declare function positionMarginConstraints(position: Position): PositionMarginConstraints | undefined;
17
+ export declare function positionRemovableMargin(position: Position): string | undefined;
14
18
  //# sourceMappingURL=transferMargin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"transferMargin.d.ts","sourceRoot":"","sources":["../../../src/utils/transferMargin.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,yBAAyB,EAE/B,MAAM,mBAAmB,CAAA;AAyB1B;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,QAAQ,GACjB,yBAAyB,GAAG,SAAS,CAoBvC"}
1
+ {"version":3,"file":"transferMargin.d.ts","sourceRoot":"","sources":["../../../src/utils/transferMargin.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,QAAQ,EAGd,MAAM,mBAAmB,CAAA;AA4B1B;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,QAAQ,GACjB,MAAM,GAAG,SAAS,CAwBpB"}
@@ -8,7 +8,9 @@ import type { HlAbstractionMode, HlClearinghouseState, HlSpotClearinghouseState
8
8
  * per-asset venue minimum.
9
9
  *
10
10
  * @param quoteAssetId - `Asset.id` the perps-route row is keyed by.
11
+ * @param withdrawalFee - Flat venue fee in quote-asset units, set on every
12
+ * quote-asset row. Absent leaves every row without a fee.
11
13
  * @public
12
14
  */
13
- export declare const hyperliquidWithdrawableBalances: (abstraction: HlAbstractionMode | null, state: HlClearinghouseState, spotState: HlSpotClearinghouseState, quoteAssetId: string) => ProviderWithdrawableBalance[];
15
+ export declare const hyperliquidWithdrawableBalances: (abstraction: HlAbstractionMode | null, state: HlClearinghouseState, spotState: HlSpotClearinghouseState, quoteAssetId: string, withdrawalFee?: string) => ProviderWithdrawableBalance[];
14
16
  //# sourceMappingURL=withdrawableBalances.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"withdrawableBalances.d.ts","sourceRoot":"","sources":["../../../src/utils/withdrawableBalances.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,iBAAiB,CAAA;AAClE,OAAO,KAAK,EACV,iBAAiB,EACjB,oBAAoB,EACpB,wBAAwB,EACzB,MAAM,mBAAmB,CAAA;AAK1B;;;;;;;;;GASG;AACH,eAAO,MAAM,+BAA+B,GAC1C,aAAa,iBAAiB,GAAG,IAAI,EACrC,OAAO,oBAAoB,EAC3B,WAAW,wBAAwB,EACnC,cAAc,MAAM,KACnB,2BAA2B,EA+B7B,CAAA"}
1
+ {"version":3,"file":"withdrawableBalances.d.ts","sourceRoot":"","sources":["../../../src/utils/withdrawableBalances.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,iBAAiB,CAAA;AAClE,OAAO,KAAK,EACV,iBAAiB,EACjB,oBAAoB,EACpB,wBAAwB,EACzB,MAAM,mBAAmB,CAAA;AAK1B;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,+BAA+B,GAC1C,aAAa,iBAAiB,GAAG,IAAI,EACrC,OAAO,oBAAoB,EAC3B,WAAW,wBAAwB,EACnC,cAAc,MAAM,EACpB,gBAAgB,MAAM,KACrB,2BAA2B,EAwC7B,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifi/perps-sdk-provider-hyperliquid",
3
- "version": "16.0.0",
3
+ "version": "17.0.0",
4
4
  "description": "Hyperliquid provider plugin for @lifi/perps-sdk",
5
5
  "homepage": "https://github.com/lifinance/perps-sdk",
6
6
  "bugs": {
@@ -49,10 +49,10 @@
49
49
  "dependencies": {
50
50
  "big.js": "^7.0.1",
51
51
  "viem": "^2.55.18",
52
- "@lifi/perps-types": "^15.0.0"
52
+ "@lifi/perps-types": "^16.0.0"
53
53
  },
54
54
  "peerDependencies": {
55
- "@lifi/perps-sdk": "^16.0.0"
55
+ "@lifi/perps-sdk": "^17.0.0"
56
56
  },
57
57
  "publishConfig": {
58
58
  "access": "public"
@@ -80,7 +80,7 @@ import type { HlExtraAgents } from './types/index.js'
80
80
  import { hlInfoOptions, infoRequest } from './utils/infoClient.js'
81
81
  import { calculateLiquidationPrice } from './utils/liquidation.js'
82
82
  import { formatOrderPrice, formatOrderSize } from './utils/orderFormatting.js'
83
- import { positionMarginConstraints } from './utils/transferMargin.js'
83
+ import { positionRemovableMargin } from './utils/transferMargin.js'
84
84
 
85
85
  /**
86
86
  * Options for {@link hyperliquidProvider}.
@@ -452,7 +452,7 @@ export function hyperliquidProvider(
452
452
  market.maxLeverage
453
453
  ),
454
454
 
455
- positionMarginConstraints,
455
+ positionRemovableMargin,
456
456
 
457
457
  projectConfig: (
458
458
  config: AccountConfig,
@@ -12,6 +12,7 @@ import type {
12
12
  HlOrderStatusResponse,
13
13
  HlTwapHistoryEntry,
14
14
  } from '../types/index.js'
15
+ import { withExplorerLinks } from '../utils/explorer.js'
15
16
  import { assetIsOutcome, mapOrder } from '../utils/index.js'
16
17
  import { hlInfoOptions, infoRequest } from '../utils/infoClient.js'
17
18
 
@@ -47,17 +48,18 @@ export const getOrder = async (
47
48
  throw err
48
49
  }
49
50
 
51
+ const infoOpts = hlInfoOptions(client, options)
50
52
  const status = await infoRequest<HlOrderStatusResponse>(
51
53
  apiUrl,
52
54
  { type: 'orderStatus', user: params.address, oid },
53
- hlInfoOptions(client, options)
55
+ infoOpts
54
56
  )
55
57
 
56
58
  if (status.status !== 'order') {
57
59
  const history = await infoRequest<HlTwapHistoryEntry[]>(
58
60
  apiUrl,
59
61
  { type: 'twapHistory', user: params.address },
60
- hlInfoOptions(client, options)
62
+ infoOpts
61
63
  )
62
64
  const twap = history.find(
63
65
  (entry) =>
@@ -81,5 +83,10 @@ export const getOrder = async (
81
83
 
82
84
  const registry = getMarketRegistry(client, PROVIDER_KEY)
83
85
  await registry.sync()
84
- return mapOrder(status.order, registry.require(status.order.order.coin))
86
+ const order = mapOrder(
87
+ status.order,
88
+ registry.require(status.order.order.coin)
89
+ )
90
+ const [linked] = await withExplorerLinks([order], params.address, infoOpts)
91
+ return linked
85
92
  }
@@ -1,8 +1,6 @@
1
1
  import {
2
2
  ACTIVE_ORDER_STATUSES,
3
3
  createWarnOnce,
4
- ExplorerChainId,
5
- explorerTxUrl,
6
4
  getMarketRegistry,
7
5
  isActiveOrderStatus,
8
6
  type MarketRegistry,
@@ -14,34 +12,21 @@ import type { MarketDisplay, Order, OrdersResponse } from '@lifi/perps-types'
14
12
  import { PROVIDER_KEY } from '../constants.js'
15
13
  import type { HyperliquidContext } from '../context.js'
16
14
  import type {
17
- HlExplorerTx,
18
15
  HlFrontendOpenOrders,
19
16
  HlHistoricalOrders,
20
17
  HlTwapHistoryEntry,
21
18
  } from '../types/index.js'
22
- import {
23
- assetIsOutcome,
24
- fetchUserTransactions,
25
- mapOrder,
26
- matchOrderActionHash,
27
- perpsDexNames,
28
- } from '../utils/index.js'
29
- import {
30
- hlInfoOptions,
31
- type InfoRequestOptions,
32
- infoRequest,
33
- } from '../utils/infoClient.js'
19
+ import { withExplorerLinks } from '../utils/explorer.js'
20
+ import { assetIsOutcome, mapOrder, perpsDexNames } from '../utils/index.js'
21
+ import { hlInfoOptions, infoRequest } from '../utils/infoClient.js'
34
22
 
35
23
  /** Parameters for a lifecycle-filtered Hyperliquid order read. */
36
24
  export type GetOrdersParams = ProviderGetOrdersParams
37
25
 
38
26
  const warn = createWarnOnce()
39
27
 
40
- const warnOnce = (key: string, detail?: string): void => {
41
- warn(
42
- key,
43
- `[${PROVIDER_KEY}] ${detail === undefined ? key : `${key}: ${detail}`}`
44
- )
28
+ const warnOnce = (key: string): void => {
29
+ warn(key, `[${PROVIDER_KEY}] ${key}`)
45
30
  }
46
31
 
47
32
  /**
@@ -73,42 +58,6 @@ const mapRow = (
73
58
  }
74
59
  }
75
60
 
76
- /**
77
- * Set `explorerLink` on each row whose placement transaction the explorer
78
- * window still holds. One explorer read serves the whole page, and it is
79
- * skipped when no row carries a client order id to match on. The link is
80
- * supplementary, so an explorer failure warns and leaves the rows unlinked
81
- * instead of failing the order read.
82
- */
83
- const withExplorerLinks = async (
84
- orders: Order[],
85
- address: string,
86
- options: InfoRequestOptions
87
- ): Promise<Order[]> => {
88
- if (!orders.some((order) => order.clientOrderId !== undefined)) {
89
- return orders
90
- }
91
- let txs: HlExplorerTx[]
92
- try {
93
- txs = await fetchUserTransactions(address, options)
94
- } catch (error) {
95
- if (!(error instanceof PerpsError)) {
96
- throw error
97
- }
98
- // The message stays out of the key: a transport failure names hosts and
99
- // ports, so keying on it would let the dedupe set grow without bound.
100
- warnOnce('explorer link lookup failed', error.message)
101
- return orders
102
- }
103
- return orders.map((order) => {
104
- const explorerLink = explorerTxUrl(
105
- ExplorerChainId.HYPERLIQUID,
106
- matchOrderActionHash(txs, order)
107
- )
108
- return explorerLink === undefined ? order : { ...order, explorerLink }
109
- })
110
- }
111
-
112
61
  /** Read regular, trigger, and TWAP orders from the requested lifecycle feeds. */
113
62
  export const getOrders = async (
114
63
  { client, apiUrl }: HyperliquidContext,
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  getMarketRegistry,
3
+ getProviders,
3
4
  PerpsError,
4
5
  type ProviderGetWithdrawableBalancesParams,
5
6
  type ProviderWithdrawableBalance,
@@ -27,13 +28,15 @@ export type GetWithdrawableBalancesParams =
27
28
  /**
28
29
  * The `(asset, route)` pairs `params.address` can withdraw from Hyperliquid,
29
30
  * read from the venue's own `withdrawable` and spot `total`/`hold` figures.
31
+ * Hyperliquid charges its flat withdrawal fee in USDC, so the backend's
32
+ * `withdrawalFeeUsd` is the fee in quote-asset units on every USDC row.
30
33
  *
31
34
  * Only the main perps dex is read: a withdrawal to L1 draws on that account,
32
35
  * and a sub-dex balance reaches it through a transfer first.
33
36
  *
34
- * @throws {PerpsError} On Hyperliquid REST error, network, or parsing
35
- * failures, and `SDKError` when the market registry carries no main perps
36
- * market to name the quote asset.
37
+ * @throws {PerpsError} On Hyperliquid REST or backend `/providers` error,
38
+ * network, or parsing failures, and `SDKError` when the market registry
39
+ * carries no main perps market to name the quote asset.
37
40
  * @public
38
41
  */
39
42
  export const getWithdrawableBalances = async (
@@ -44,24 +47,26 @@ export const getWithdrawableBalances = async (
44
47
  const registry = getMarketRegistry(client, PROVIDER_KEY)
45
48
  const infoOpts = hlInfoOptions(client, options)
46
49
 
47
- const [markets, abstraction, state, spotState] = await Promise.all([
48
- registry.sync(),
49
- infoRequest<HlAbstractionMode | null>(
50
- apiUrl,
51
- { type: 'userAbstraction', user: params.address },
52
- infoOpts
53
- ),
54
- infoRequest<HlClearinghouseState>(
55
- apiUrl,
56
- { type: 'clearinghouseState', user: params.address },
57
- infoOpts
58
- ),
59
- infoRequest<HlSpotClearinghouseState>(
60
- apiUrl,
61
- { type: 'spotClearinghouseState', user: params.address },
62
- infoOpts
63
- ),
64
- ])
50
+ const [{ providers }, markets, abstraction, state, spotState] =
51
+ await Promise.all([
52
+ getProviders(client, options),
53
+ registry.sync(),
54
+ infoRequest<HlAbstractionMode | null>(
55
+ apiUrl,
56
+ { type: 'userAbstraction', user: params.address },
57
+ infoOpts
58
+ ),
59
+ infoRequest<HlClearinghouseState>(
60
+ apiUrl,
61
+ { type: 'clearinghouseState', user: params.address },
62
+ infoOpts
63
+ ),
64
+ infoRequest<HlSpotClearinghouseState>(
65
+ apiUrl,
66
+ { type: 'spotClearinghouseState', user: params.address },
67
+ infoOpts
68
+ ),
69
+ ])
65
70
 
66
71
  const quoteAsset = markets.find(
67
72
  (market) => market.categoryId === MAIN_MARKET_ID
@@ -75,10 +80,15 @@ export const getWithdrawableBalances = async (
75
80
  throw error
76
81
  }
77
82
 
83
+ const feeUsd = providers.find(
84
+ (provider) => provider.key === PROVIDER_KEY
85
+ )?.withdrawalFeeUsd
86
+
78
87
  return hyperliquidWithdrawableBalances(
79
88
  abstraction,
80
89
  state,
81
90
  spotState,
82
- quoteAsset.id
91
+ quoteAsset.id,
92
+ feeUsd === undefined ? undefined : String(feeUsd)
83
93
  )
84
94
  }
@@ -1,6 +1,12 @@
1
- import type { OrderBase } from '@lifi/perps-types'
1
+ import {
2
+ createWarnOnce,
3
+ ExplorerChainId,
4
+ explorerTxUrl,
5
+ PerpsError,
6
+ } from '@lifi/perps-sdk'
7
+ import type { Order, OrderBase } from '@lifi/perps-types'
2
8
  import { isHex } from 'viem'
3
- import { HYPERLIQUID_EXPLORER_RPC_URL } from '../constants.js'
9
+ import { HYPERLIQUID_EXPLORER_RPC_URL, PROVIDER_KEY } from '../constants.js'
4
10
  import { type HlExplorerTx, isHlExplorerTx } from '../types/explorer.js'
5
11
  import { hlPostJson, type InfoRequestOptions } from './infoClient.js'
6
12
 
@@ -91,3 +97,46 @@ export const matchOrderActionHash = (
91
97
  )
92
98
  return match?.hash
93
99
  }
100
+
101
+ const warn = createWarnOnce()
102
+
103
+ /**
104
+ * Set `explorerLink` on each order whose placement transaction the explorer
105
+ * window still holds. One explorer read serves every order, and it is skipped
106
+ * when no order carries a client order id to match on. The link is
107
+ * supplementary, so an explorer failure warns and leaves the orders unlinked
108
+ * instead of failing the order read. A caller cancellation still rejects.
109
+ */
110
+ export const withExplorerLinks = async (
111
+ orders: Order[],
112
+ address: string,
113
+ options: InfoRequestOptions
114
+ ): Promise<Order[]> => {
115
+ if (!orders.some((order) => order.clientOrderId !== undefined)) {
116
+ return orders
117
+ }
118
+ let txs: HlExplorerTx[]
119
+ try {
120
+ txs = await fetchUserTransactions(address, options)
121
+ } catch (error) {
122
+ // `hlPostJson` wraps a signal timeout as a `ServerError`, so the signal,
123
+ // not the error type, tells a caller cancellation from an explorer failure.
124
+ if (!(error instanceof PerpsError) || options.signal?.aborted) {
125
+ throw error
126
+ }
127
+ // The message stays out of the key: a transport failure names hosts and
128
+ // ports, so keying on it would let the dedupe set grow without bound.
129
+ warn(
130
+ 'explorer link lookup failed',
131
+ `[${PROVIDER_KEY}] explorer link lookup failed: ${error.message}`
132
+ )
133
+ return orders
134
+ }
135
+ return orders.map((order) => {
136
+ const explorerLink = explorerTxUrl(
137
+ ExplorerChainId.HYPERLIQUID,
138
+ matchOrderActionHash(txs, order)
139
+ )
140
+ return explorerLink === undefined ? order : { ...order, explorerLink }
141
+ })
142
+ }
@@ -56,7 +56,7 @@ export {
56
56
  spotPriceById,
57
57
  } from './spotBalance.js'
58
58
  export { partitionSpotBalances, type SpotPartition } from './spotCollateral.js'
59
- export { positionMarginConstraints } from './transferMargin.js'
59
+ export { positionRemovableMargin } from './transferMargin.js'
60
60
  export {
61
61
  type DexMarginSummary,
62
62
  perpsTotals,
@@ -2,24 +2,27 @@ import { PerpsError } from '@lifi/perps-sdk'
2
2
  import {
3
3
  PerpsErrorCode,
4
4
  type Position,
5
- type PositionMarginConstraints,
6
5
  positionSupportsMarginAdjustment,
6
+ positionSupportsMarginRemoval,
7
7
  } from '@lifi/perps-types'
8
8
  import Big from 'big.js'
9
9
 
10
10
  const NOTIONAL_FLOOR_RATIO = '0.1'
11
- const AMOUNT_INCREMENT = '0.000001'
11
+ const AMOUNT_DECIMALS = 6
12
12
 
13
- function positivePositionAmount(value: string, field: string): Big {
14
- let amount: Big
13
+ function positionAmount(value: string, field: string): Big {
15
14
  try {
16
- amount = new Big(value)
15
+ return new Big(value)
17
16
  } catch {
18
17
  throw new PerpsError(
19
18
  PerpsErrorCode.ValidationError,
20
19
  `Invalid decimal string on Position.${field}: '${value}'`
21
20
  )
22
21
  }
22
+ }
23
+
24
+ function positivePositionAmount(value: string, field: string): Big {
25
+ const amount = positionAmount(value, field)
23
26
  if (amount.lte(0)) {
24
27
  throw new PerpsError(
25
28
  PerpsErrorCode.ValidationError,
@@ -30,36 +33,44 @@ function positivePositionAmount(value: string, field: string): Big {
30
33
  }
31
34
 
32
35
  /**
33
- * Hyperliquid's exact documented transfer-margin requirement:
34
- * `max(initial_margin_required, 0.1 * total_position_value)`.
35
- *
36
- * Strict-isolated markets still return constraints because margin can be
37
- * added; their market capability prevents removal. Cross positions and
38
- * markets without individual margin adjustment return `undefined`.
36
+ * Hyperliquid removable isolated margin:
37
+ * `marginUsed − max(initial_margin_required, 0.1 × total_position_value)`.
38
+ * Hyperliquid isolated `marginUsed` already includes the unrealized PnL of
39
+ * the position, so the PnL is not added again, and it can reach zero or less
40
+ * before liquidation.
39
41
  *
42
+ * @returns `undefined` for cross positions and markets without individual
43
+ * margin adjustment; `'0'` for add-only strict-isolated markets and when
44
+ * nothing is removable.
45
+ * @throws {PerpsError} `ValidationError` when a required `Position` decimal
46
+ * is malformed, or when a size, price or requirement is not positive.
40
47
  * @see https://hyperliquid.gitbook.io/hyperliquid-docs/trading/margining
41
48
  * @public
42
49
  */
43
- export function positionMarginConstraints(
50
+ export function positionRemovableMargin(
44
51
  position: Position
45
- ): PositionMarginConstraints | undefined {
52
+ ): string | undefined {
46
53
  if (!positionSupportsMarginAdjustment(position)) {
47
54
  return undefined
48
55
  }
56
+ if (!positionSupportsMarginRemoval(position)) {
57
+ return '0'
58
+ }
59
+ const marginUsed = positionAmount(position.marginUsed, 'marginUsed')
49
60
  const initialMargin = positivePositionAmount(
50
61
  position.initialMarginRequirement,
51
62
  'initialMarginRequirement'
52
63
  )
53
- const notional = positivePositionAmount(position.size, 'size').times(
54
- positivePositionAmount(position.markPrice, 'markPrice')
55
- )
56
- const notionalFloor = notional.times(NOTIONAL_FLOOR_RATIO)
64
+ const notionalFloor = positivePositionAmount(position.size, 'size')
65
+ .times(positivePositionAmount(position.markPrice, 'markPrice'))
66
+ .times(NOTIONAL_FLOOR_RATIO)
57
67
  const minimumMargin = initialMargin.gt(notionalFloor)
58
68
  ? initialMargin
59
69
  : notionalFloor
60
70
 
61
- return {
62
- minimumMarginRequirement: minimumMargin.toFixed(),
63
- amountIncrement: AMOUNT_INCREMENT,
71
+ const removable = marginUsed.minus(minimumMargin)
72
+ if (removable.lte(0)) {
73
+ return '0'
64
74
  }
75
+ return removable.round(AMOUNT_DECIMALS, Big.roundDown).toFixed()
65
76
  }
@@ -16,15 +16,24 @@ import { toWireBig } from './decimal.js'
16
16
  * per-asset venue minimum.
17
17
  *
18
18
  * @param quoteAssetId - `Asset.id` the perps-route row is keyed by.
19
+ * @param withdrawalFee - Flat venue fee in quote-asset units, set on every
20
+ * quote-asset row. Absent leaves every row without a fee.
19
21
  * @public
20
22
  */
21
23
  export const hyperliquidWithdrawableBalances = (
22
24
  abstraction: HlAbstractionMode | null,
23
25
  state: HlClearinghouseState,
24
26
  spotState: HlSpotClearinghouseState,
25
- quoteAssetId: string
27
+ quoteAssetId: string,
28
+ withdrawalFee?: string
26
29
  ): ProviderWithdrawableBalance[] => {
27
30
  const rows: ProviderWithdrawableBalance[] = []
31
+ const fee =
32
+ withdrawalFee === undefined
33
+ ? undefined
34
+ : toWireBig(withdrawalFee, 'providers.withdrawalFeeUsd').toFixed()
35
+ const feeFor = (assetId: string) =>
36
+ fee === undefined || assetId !== quoteAssetId ? {} : { withdrawalFee: fee }
28
37
 
29
38
  if (isUnifiedAbstraction(abstraction)) {
30
39
  for (const balance of spotState.balances) {
@@ -35,10 +44,12 @@ export const hyperliquidWithdrawableBalances = (
35
44
  toWireBig(balance.hold, 'spotBalance.hold')
36
45
  )
37
46
  if (spot.gt(0)) {
47
+ const assetId = String(balance.token)
38
48
  rows.push({
39
- assetId: String(balance.token),
49
+ assetId,
40
50
  route: 'spot',
41
51
  available: spot.toFixed(),
52
+ ...feeFor(assetId),
42
53
  })
43
54
  }
44
55
  }
@@ -50,6 +61,7 @@ export const hyperliquidWithdrawableBalances = (
50
61
  assetId: quoteAssetId,
51
62
  route: 'perps',
52
63
  available: perps.toFixed(),
64
+ ...feeFor(quoteAssetId),
53
65
  })
54
66
  }
55
67