@lifi/perps-sdk-provider-hyperliquid 30.0.0 → 30.0.2

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 (56) hide show
  1. package/README.md +22 -7
  2. package/dist/cjs/services/getOrder.d.ts.map +1 -1
  3. package/dist/cjs/services/getOrder.js +7 -2
  4. package/dist/cjs/services/getOrder.js.map +1 -1
  5. package/dist/cjs/services/getOrders.d.ts +1 -1
  6. package/dist/cjs/services/getOrders.d.ts.map +1 -1
  7. package/dist/cjs/services/getOrders.js +8 -1
  8. package/dist/cjs/services/getOrders.js.map +1 -1
  9. package/dist/cjs/types/fill.d.ts +4 -0
  10. package/dist/cjs/types/fill.d.ts.map +1 -1
  11. package/dist/cjs/types/ledger.d.ts +1 -1
  12. package/dist/cjs/types/ledger.d.ts.map +1 -1
  13. package/dist/cjs/utils/mapOrder.d.ts.map +1 -1
  14. package/dist/cjs/utils/mapOrder.js +73 -18
  15. package/dist/cjs/utils/mapOrder.js.map +1 -1
  16. package/dist/cjs/utils/twapHistory.d.ts +4 -0
  17. package/dist/cjs/utils/twapHistory.d.ts.map +1 -0
  18. package/dist/cjs/utils/twapHistory.js +47 -0
  19. package/dist/cjs/utils/twapHistory.js.map +1 -0
  20. package/dist/esm/services/getOrder.d.ts.map +1 -1
  21. package/dist/esm/services/getOrder.js +7 -2
  22. package/dist/esm/services/getOrder.js.map +1 -1
  23. package/dist/esm/services/getOrders.d.ts +1 -1
  24. package/dist/esm/services/getOrders.d.ts.map +1 -1
  25. package/dist/esm/services/getOrders.js +8 -1
  26. package/dist/esm/services/getOrders.js.map +1 -1
  27. package/dist/esm/types/fill.d.ts +9 -0
  28. package/dist/esm/types/fill.d.ts.map +1 -1
  29. package/dist/esm/types/ledger.d.ts +1 -1
  30. package/dist/esm/types/ledger.d.ts.map +1 -1
  31. package/dist/esm/utils/mapOrder.d.ts +4 -1
  32. package/dist/esm/utils/mapOrder.d.ts.map +1 -1
  33. package/dist/esm/utils/mapOrder.js +78 -19
  34. package/dist/esm/utils/mapOrder.js.map +1 -1
  35. package/dist/esm/utils/twapHistory.d.ts +13 -0
  36. package/dist/esm/utils/twapHistory.d.ts.map +1 -0
  37. package/dist/esm/utils/twapHistory.js +51 -0
  38. package/dist/esm/utils/twapHistory.js.map +1 -0
  39. package/dist/types/services/getOrder.d.ts.map +1 -1
  40. package/dist/types/services/getOrders.d.ts +1 -1
  41. package/dist/types/services/getOrders.d.ts.map +1 -1
  42. package/dist/types/types/fill.d.ts +9 -0
  43. package/dist/types/types/fill.d.ts.map +1 -1
  44. package/dist/types/types/ledger.d.ts +1 -1
  45. package/dist/types/types/ledger.d.ts.map +1 -1
  46. package/dist/types/utils/mapOrder.d.ts +4 -1
  47. package/dist/types/utils/mapOrder.d.ts.map +1 -1
  48. package/dist/types/utils/twapHistory.d.ts +13 -0
  49. package/dist/types/utils/twapHistory.d.ts.map +1 -0
  50. package/package.json +3 -3
  51. package/src/services/getOrder.ts +16 -2
  52. package/src/services/getOrders.ts +20 -2
  53. package/src/types/fill.ts +10 -0
  54. package/src/types/ledger.ts +1 -1
  55. package/src/utils/mapOrder.ts +94 -19
  56. package/src/utils/twapHistory.ts +63 -0
package/src/types/fill.ts CHANGED
@@ -53,3 +53,13 @@ export type HlUserFills = HlUserFill[]
53
53
 
54
54
  /** Fills returned by the time-bounded `userFillsByTime` query. @public */
55
55
  export type HlUserFillsByTime = HlUserFill[]
56
+
57
+ /**
58
+ * One row of the `userTwapSliceFills` query: a fill that a TWAP slice made.
59
+ * The venue returns at most 2,000 recent rows, newest first.
60
+ * @public
61
+ */
62
+ export type HlTwapSliceFill = {
63
+ fill: HlUserFill
64
+ twapId: number
65
+ }
@@ -18,7 +18,7 @@ export type HlSpotTransferDelta = {
18
18
  destination: Address
19
19
  fee?: string
20
20
  nativeTokenFee?: string
21
- nonce?: number
21
+ nonce?: number | null
22
22
  }
23
23
 
24
24
  /**
@@ -58,13 +58,54 @@ export const mapOrderType = (
58
58
  }
59
59
  }
60
60
 
61
- /** Translate venue lifecycle statuses; unknown statuses are errors. */
62
- export const mapOrderStatus = (status: string): OrderStatus => {
61
+ const STATUS_REASONS = {
62
+ canceled: 'Cancelled.',
63
+ scheduledCancel: 'Cancelled: the scheduled cancel deadline passed.',
64
+ marginCanceled: 'Cancelled: not enough margin to fill the order.',
65
+ vaultWithdrawalCanceled: 'Cancelled: a user withdrew from the vault.',
66
+ openInterestCapCanceled:
67
+ 'Cancelled: the price was too aggressive at the open interest cap.',
68
+ selfTradeCanceled:
69
+ 'Cancelled: the order would have traded with your own order.',
70
+ reduceOnlyCanceled:
71
+ 'Cancelled: the reduce-only order would not reduce the position.',
72
+ siblingFilledCanceled: 'Cancelled: the linked TP/SL order filled.',
73
+ delistedCanceled: 'Cancelled: the market was delisted.',
74
+ liquidatedCanceled: 'Cancelled: the account was liquidated.',
75
+ rejected: 'Rejected.',
76
+ tickRejected: 'Rejected: the price does not match the tick size.',
77
+ minTradeNtlRejected: 'Rejected: the order value is below the minimum.',
78
+ perpMarginRejected: 'Rejected: not enough margin for this order.',
79
+ reduceOnlyRejected:
80
+ 'Rejected: the reduce-only order would not reduce the position.',
81
+ badAloPxRejected:
82
+ 'Rejected: the post-only order would have filled immediately.',
83
+ iocCancelRejected: 'Rejected: the immediate-or-cancel order could not fill.',
84
+ badTriggerPxRejected: 'Rejected: the TP/SL trigger price is not valid.',
85
+ marketOrderNoLiquidityRejected:
86
+ 'Rejected: not enough liquidity for the market order.',
87
+ positionIncreaseAtOpenInterestCapRejected:
88
+ 'Rejected: the market is at its open interest cap.',
89
+ positionFlipAtOpenInterestCapRejected:
90
+ 'Rejected: the market is at its open interest cap.',
91
+ tooAggressiveAtOpenInterestCapRejected:
92
+ 'Rejected: the price was too aggressive at the open interest cap.',
93
+ openInterestIncreaseRejected:
94
+ 'Rejected: the market is at its open interest cap.',
95
+ insufficientSpotBalanceRejected: 'Rejected: not enough spot balance.',
96
+ oracleRejected: 'Rejected: the price is too far from the oracle price.',
97
+ perpMaxPositionRejected:
98
+ 'Rejected: the position is larger than the limit for the current leverage.',
99
+ } as const
100
+
101
+ const mapOrderLifecycle = (
102
+ status: string
103
+ ): Pick<OrderBase, 'status' | 'statusReason'> => {
63
104
  switch (status) {
64
105
  case 'open':
65
- return OrderStatus.OPEN
106
+ return { status: OrderStatus.OPEN }
66
107
  case 'filled':
67
- return OrderStatus.FILLED
108
+ return { status: OrderStatus.FILLED }
68
109
  case 'canceled':
69
110
  case 'scheduledCancel':
70
111
  case 'marginCanceled':
@@ -75,7 +116,10 @@ export const mapOrderStatus = (status: string): OrderStatus => {
75
116
  case 'siblingFilledCanceled':
76
117
  case 'delistedCanceled':
77
118
  case 'liquidatedCanceled':
78
- return OrderStatus.CANCELLED
119
+ return {
120
+ status: OrderStatus.CANCELLED,
121
+ statusReason: STATUS_REASONS[status],
122
+ }
79
123
  case 'rejected':
80
124
  case 'tickRejected':
81
125
  case 'minTradeNtlRejected':
@@ -92,14 +136,37 @@ export const mapOrderStatus = (status: string): OrderStatus => {
92
136
  case 'insufficientSpotBalanceRejected':
93
137
  case 'oracleRejected':
94
138
  case 'perpMaxPositionRejected':
95
- return OrderStatus.REJECTED
139
+ return {
140
+ status: OrderStatus.REJECTED,
141
+ statusReason: STATUS_REASONS[status],
142
+ }
96
143
  case 'triggered':
97
- return OrderStatus.TRIGGERED
144
+ return { status: OrderStatus.TRIGGERED }
98
145
  default:
146
+ // Documented cancel and reject reasons use these suffixes; an undocumented one keeps the row.
147
+ if (status.endsWith('Canceled')) {
148
+ return {
149
+ status: OrderStatus.CANCELLED,
150
+ statusReason: 'Cancelled by the venue.',
151
+ }
152
+ }
153
+ if (status.endsWith('Rejected')) {
154
+ return {
155
+ status: OrderStatus.REJECTED,
156
+ statusReason: 'Rejected by the venue.',
157
+ }
158
+ }
99
159
  throw venueError(`Unknown Hyperliquid order status: ${status}`)
100
160
  }
101
161
  }
102
162
 
163
+ /**
164
+ * Translate venue lifecycle statuses. An unknown `...Canceled` or `...Rejected`
165
+ * status maps by its suffix; any other unknown status is an error.
166
+ */
167
+ export const mapOrderStatus = (status: string): OrderStatus =>
168
+ mapOrderLifecycle(status).status
169
+
103
170
  const skipOrder = (
104
171
  marketId: string,
105
172
  field: string,
@@ -172,13 +239,20 @@ export const mapOrder = (
172
239
  if (updatedAt === undefined) {
173
240
  return skipOrder(market.id, 'time', raw.time)
174
241
  }
242
+ const hasRemaining =
243
+ safeIsDecimalStringGreaterThan(remainingSize, '0') === true
175
244
  let status: OrderStatus
245
+ let statusReason: string | undefined
176
246
  switch (raw.status.status) {
177
247
  case 'activated':
178
- status =
179
- safeIsDecimalStringGreaterThan(state.executedSz, '0') === true
180
- ? OrderStatus.PARTIALLY_FILLED
181
- : OrderStatus.OPEN
248
+ if (!hasRemaining) {
249
+ status = OrderStatus.FILLED
250
+ } else {
251
+ status =
252
+ safeIsDecimalStringGreaterThan(state.executedSz, '0') === true
253
+ ? OrderStatus.PARTIALLY_FILLED
254
+ : OrderStatus.OPEN
255
+ }
182
256
  break
183
257
  case 'waitingForTrigger':
184
258
  status = OrderStatus.OPEN
@@ -189,9 +263,11 @@ export const mapOrder = (
189
263
  case 'terminated':
190
264
  case 'stopped':
191
265
  status = OrderStatus.CANCELLED
266
+ statusReason = STATUS_REASONS.canceled
192
267
  break
193
268
  case 'error':
194
269
  status = OrderStatus.REJECTED
270
+ statusReason = STATUS_REASONS.rejected
195
271
  break
196
272
  default:
197
273
  throw venueError(
@@ -208,14 +284,12 @@ export const mapOrder = (
208
284
  type: OrderType.TWAP,
209
285
  side: state.side === 'B' ? OrderSide.BUY : OrderSide.SELL,
210
286
  originalSize: state.sz,
211
- remainingSize,
287
+ remainingSize: hasRemaining ? remainingSize : '0',
212
288
  filledSize: state.executedSz,
213
289
  ...(averagePrice === undefined ? {} : { averagePrice }),
214
290
  reduceOnly: state.reduceOnly,
215
291
  status,
216
- ...(status === OrderStatus.CANCELLED || status === OrderStatus.REJECTED
217
- ? { statusReason: raw.status.description ?? raw.status.status }
218
- : {}),
292
+ ...(statusReason === undefined ? {} : { statusReason }),
219
293
  createdAt,
220
294
  updatedAt,
221
295
  startedAt: createdAt,
@@ -239,7 +313,8 @@ export const mapOrder = (
239
313
  if (updatedAt === undefined) {
240
314
  return skipOrder(market.id, 'statusTimestamp', statusTimestamp)
241
315
  }
242
- let status = mapOrderStatus(venueStatus)
316
+ const lifecycle = mapOrderLifecycle(venueStatus)
317
+ let status = lifecycle.status
243
318
  if (parentOrderId !== undefined && !('order' in raw)) {
244
319
  status = OrderStatus.PENDING
245
320
  } else if (
@@ -254,9 +329,9 @@ export const mapOrder = (
254
329
  ...(typeof o.cloid === 'string' ? { clientOrderId: o.cloid } : {}),
255
330
  side: o.side === 'B' ? OrderSide.BUY : OrderSide.SELL,
256
331
  status,
257
- ...(status === OrderStatus.CANCELLED || status === OrderStatus.REJECTED
258
- ? { statusReason: venueStatus }
259
- : {}),
332
+ ...(lifecycle.statusReason === undefined
333
+ ? {}
334
+ : { statusReason: lifecycle.statusReason }),
260
335
  originalSize: o.origSz,
261
336
  remainingSize: o.sz,
262
337
  filledSize,
@@ -0,0 +1,63 @@
1
+ import {
2
+ safeAddDecimalStrings,
3
+ safeCalculateNotionalValue,
4
+ } from '@lifi/perps-sdk'
5
+ import type { HlTwapHistoryEntry, HlTwapSliceFill } from '../types/index.js'
6
+
7
+ /**
8
+ * Keep the newest `twapHistory` row of each TWAP, in first-seen id order. The
9
+ * venue adds one row per status change; on equal `time` the earlier row wins.
10
+ */
11
+ export const resolveLatestTwapEntries = (
12
+ history: readonly HlTwapHistoryEntry[]
13
+ ): HlTwapHistoryEntry[] => {
14
+ const latest = new Map<number | undefined, HlTwapHistoryEntry>()
15
+ for (const entry of history) {
16
+ const kept = latest.get(entry.twapId)
17
+ if (kept === undefined || entry.time > kept.time) {
18
+ latest.set(entry.twapId, entry)
19
+ }
20
+ }
21
+ return [...latest.values()]
22
+ }
23
+
24
+ /**
25
+ * Take the executed size and notional of each `activated` row from the slice
26
+ * fills of its `twapId`. The venue writes that row once, at activation. A row
27
+ * with no slice fill, or with a slice fill that does not read, keeps its state.
28
+ */
29
+ export const resolveTwapExecution = (
30
+ entries: readonly HlTwapHistoryEntry[],
31
+ sliceFills: readonly HlTwapSliceFill[]
32
+ ): HlTwapHistoryEntry[] => {
33
+ const fillsByTwap = new Map<number, HlTwapSliceFill['fill'][]>()
34
+ for (const { twapId, fill } of sliceFills) {
35
+ const fills = fillsByTwap.get(twapId)
36
+ if (fills === undefined) {
37
+ fillsByTwap.set(twapId, [fill])
38
+ } else {
39
+ fills.push(fill)
40
+ }
41
+ }
42
+ return entries.map((entry) => {
43
+ const fills =
44
+ entry.status.status === 'activated' && entry.twapId !== undefined
45
+ ? fillsByTwap.get(entry.twapId)
46
+ : undefined
47
+ if (fills === undefined) {
48
+ return entry
49
+ }
50
+ const notionals = fills.map((fill) =>
51
+ safeCalculateNotionalValue(fill.sz, fill.px)
52
+ )
53
+ if (!notionals.every((notional) => notional !== undefined)) {
54
+ return entry
55
+ }
56
+ const executedSz = safeAddDecimalStrings(fills.map((fill) => fill.sz))
57
+ const executedNtl = safeAddDecimalStrings(notionals)
58
+ if (executedSz === undefined || executedNtl === undefined) {
59
+ return entry
60
+ }
61
+ return { ...entry, state: { ...entry.state, executedSz, executedNtl } }
62
+ })
63
+ }