@lifi/perps-sdk-provider-hyperliquid 26.0.0 → 27.0.1

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 (132) hide show
  1. package/dist/cjs/services/getAccount.d.ts.map +1 -1
  2. package/dist/cjs/services/getAccount.js +7 -6
  3. package/dist/cjs/services/getAccount.js.map +1 -1
  4. package/dist/cjs/services/getOrders.d.ts.map +1 -1
  5. package/dist/cjs/services/getOrders.js +5 -27
  6. package/dist/cjs/services/getOrders.js.map +1 -1
  7. package/dist/cjs/services/getPortfolioHistory.d.ts.map +1 -1
  8. package/dist/cjs/services/getPortfolioHistory.js +3 -1
  9. package/dist/cjs/services/getPortfolioHistory.js.map +1 -1
  10. package/dist/cjs/utils/index.d.ts +1 -1
  11. package/dist/cjs/utils/index.d.ts.map +1 -1
  12. package/dist/cjs/utils/index.js +3 -1
  13. package/dist/cjs/utils/index.js.map +1 -1
  14. package/dist/cjs/utils/liquidation.d.ts +4 -1
  15. package/dist/cjs/utils/liquidation.d.ts.map +1 -1
  16. package/dist/cjs/utils/liquidation.js +9 -8
  17. package/dist/cjs/utils/liquidation.js.map +1 -1
  18. package/dist/cjs/utils/mapActivity.d.ts.map +1 -1
  19. package/dist/cjs/utils/mapActivity.js +9 -3
  20. package/dist/cjs/utils/mapActivity.js.map +1 -1
  21. package/dist/cjs/utils/mapFill.d.ts.map +1 -1
  22. package/dist/cjs/utils/mapFill.js +4 -1
  23. package/dist/cjs/utils/mapFill.js.map +1 -1
  24. package/dist/cjs/utils/mapMarket.d.ts.map +1 -1
  25. package/dist/cjs/utils/mapMarket.js +1 -1
  26. package/dist/cjs/utils/mapMarket.js.map +1 -1
  27. package/dist/cjs/utils/mapOrder.d.ts +2 -0
  28. package/dist/cjs/utils/mapOrder.d.ts.map +1 -1
  29. package/dist/cjs/utils/mapOrder.js +31 -19
  30. package/dist/cjs/utils/mapOrder.js.map +1 -1
  31. package/dist/cjs/utils/mapPosition.d.ts.map +1 -1
  32. package/dist/cjs/utils/mapPosition.js +4 -4
  33. package/dist/cjs/utils/mapPosition.js.map +1 -1
  34. package/dist/cjs/utils/spotBalance.d.ts +1 -1
  35. package/dist/cjs/utils/spotBalance.d.ts.map +1 -1
  36. package/dist/cjs/utils/spotBalance.js +4 -1
  37. package/dist/cjs/utils/spotBalance.js.map +1 -1
  38. package/dist/cjs/utils/transferMargin.js +1 -1
  39. package/dist/cjs/utils/transferMargin.js.map +1 -1
  40. package/dist/cjs/utils/venueTotals.d.ts +0 -1
  41. package/dist/cjs/utils/venueTotals.d.ts.map +1 -1
  42. package/dist/cjs/utils/venueTotals.js +13 -4
  43. package/dist/cjs/utils/venueTotals.js.map +1 -1
  44. package/dist/cjs/websocket/HyperliquidWsProvider.d.ts +2 -0
  45. package/dist/cjs/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  46. package/dist/cjs/websocket/HyperliquidWsProvider.js +63 -51
  47. package/dist/cjs/websocket/HyperliquidWsProvider.js.map +1 -1
  48. package/dist/esm/services/getAccount.d.ts.map +1 -1
  49. package/dist/esm/services/getAccount.js +8 -7
  50. package/dist/esm/services/getAccount.js.map +1 -1
  51. package/dist/esm/services/getOrders.d.ts.map +1 -1
  52. package/dist/esm/services/getOrders.js +7 -35
  53. package/dist/esm/services/getOrders.js.map +1 -1
  54. package/dist/esm/services/getPortfolioHistory.d.ts.map +1 -1
  55. package/dist/esm/services/getPortfolioHistory.js +3 -1
  56. package/dist/esm/services/getPortfolioHistory.js.map +1 -1
  57. package/dist/esm/utils/index.d.ts +1 -1
  58. package/dist/esm/utils/index.d.ts.map +1 -1
  59. package/dist/esm/utils/index.js +1 -1
  60. package/dist/esm/utils/index.js.map +1 -1
  61. package/dist/esm/utils/liquidation.d.ts +24 -23
  62. package/dist/esm/utils/liquidation.d.ts.map +1 -1
  63. package/dist/esm/utils/liquidation.js +29 -31
  64. package/dist/esm/utils/liquidation.js.map +1 -1
  65. package/dist/esm/utils/mapActivity.d.ts.map +1 -1
  66. package/dist/esm/utils/mapActivity.js +9 -3
  67. package/dist/esm/utils/mapActivity.js.map +1 -1
  68. package/dist/esm/utils/mapFill.d.ts.map +1 -1
  69. package/dist/esm/utils/mapFill.js +4 -1
  70. package/dist/esm/utils/mapFill.js.map +1 -1
  71. package/dist/esm/utils/mapMarket.d.ts.map +1 -1
  72. package/dist/esm/utils/mapMarket.js +2 -2
  73. package/dist/esm/utils/mapMarket.js.map +1 -1
  74. package/dist/esm/utils/mapOrder.d.ts +7 -0
  75. package/dist/esm/utils/mapOrder.d.ts.map +1 -1
  76. package/dist/esm/utils/mapOrder.js +35 -19
  77. package/dist/esm/utils/mapOrder.js.map +1 -1
  78. package/dist/esm/utils/mapPosition.d.ts.map +1 -1
  79. package/dist/esm/utils/mapPosition.js +4 -4
  80. package/dist/esm/utils/mapPosition.js.map +1 -1
  81. package/dist/esm/utils/spotBalance.d.ts +2 -1
  82. package/dist/esm/utils/spotBalance.d.ts.map +1 -1
  83. package/dist/esm/utils/spotBalance.js +5 -1
  84. package/dist/esm/utils/spotBalance.js.map +1 -1
  85. package/dist/esm/utils/transferMargin.js +1 -1
  86. package/dist/esm/utils/transferMargin.js.map +1 -1
  87. package/dist/esm/utils/venueTotals.d.ts +2 -6
  88. package/dist/esm/utils/venueTotals.d.ts.map +1 -1
  89. package/dist/esm/utils/venueTotals.js +15 -8
  90. package/dist/esm/utils/venueTotals.js.map +1 -1
  91. package/dist/esm/utils/withdrawableBalances.d.ts +2 -2
  92. package/dist/esm/utils/withdrawableBalances.js +2 -2
  93. package/dist/esm/websocket/HyperliquidWsProvider.d.ts +3 -1
  94. package/dist/esm/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  95. package/dist/esm/websocket/HyperliquidWsProvider.js +66 -54
  96. package/dist/esm/websocket/HyperliquidWsProvider.js.map +1 -1
  97. package/dist/types/services/getAccount.d.ts.map +1 -1
  98. package/dist/types/services/getOrders.d.ts.map +1 -1
  99. package/dist/types/services/getPortfolioHistory.d.ts.map +1 -1
  100. package/dist/types/utils/index.d.ts +1 -1
  101. package/dist/types/utils/index.d.ts.map +1 -1
  102. package/dist/types/utils/liquidation.d.ts +24 -23
  103. package/dist/types/utils/liquidation.d.ts.map +1 -1
  104. package/dist/types/utils/mapActivity.d.ts.map +1 -1
  105. package/dist/types/utils/mapFill.d.ts.map +1 -1
  106. package/dist/types/utils/mapMarket.d.ts.map +1 -1
  107. package/dist/types/utils/mapOrder.d.ts +7 -0
  108. package/dist/types/utils/mapOrder.d.ts.map +1 -1
  109. package/dist/types/utils/mapPosition.d.ts.map +1 -1
  110. package/dist/types/utils/spotBalance.d.ts +2 -1
  111. package/dist/types/utils/spotBalance.d.ts.map +1 -1
  112. package/dist/types/utils/venueTotals.d.ts +2 -6
  113. package/dist/types/utils/venueTotals.d.ts.map +1 -1
  114. package/dist/types/utils/withdrawableBalances.d.ts +2 -2
  115. package/dist/types/websocket/HyperliquidWsProvider.d.ts +3 -1
  116. package/dist/types/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  117. package/package.json +3 -3
  118. package/src/services/getAccount.ts +16 -6
  119. package/src/services/getOrders.ts +9 -44
  120. package/src/services/getPortfolioHistory.ts +3 -7
  121. package/src/utils/index.ts +2 -0
  122. package/src/utils/liquidation.ts +42 -34
  123. package/src/utils/mapActivity.ts +9 -3
  124. package/src/utils/mapFill.ts +4 -1
  125. package/src/utils/mapMarket.ts +4 -2
  126. package/src/utils/mapOrder.ts +46 -20
  127. package/src/utils/mapPosition.ts +8 -4
  128. package/src/utils/spotBalance.ts +8 -1
  129. package/src/utils/transferMargin.ts +1 -1
  130. package/src/utils/venueTotals.ts +21 -28
  131. package/src/utils/withdrawableBalances.ts +2 -2
  132. package/src/websocket/HyperliquidWsProvider.ts +105 -69
@@ -1,13 +1,14 @@
1
1
  import {
2
+ createWarnOnce,
3
+ isDecimalString,
2
4
  isDecimalStringGreaterThan,
5
+ type MarketRegistry,
3
6
  PerpsError,
4
7
  safeDivideDecimalString,
5
8
  safeIsDecimalStringGreaterThan,
6
9
  safeIsDecimalStringZero,
7
10
  safeSubtractDecimalString,
8
- subtractDecimalString,
9
11
  triggerConditionFor,
10
- unknownToDecimalString,
11
12
  warnSkippedVenueRow,
12
13
  } from '@lifi/perps-sdk'
13
14
  import type {
@@ -29,6 +30,7 @@ import type {
29
30
  HlOrderDetail,
30
31
  HlTwapHistoryEntry,
31
32
  } from '../types/index.js'
33
+ import { assetIsOutcome } from './assetId.js'
32
34
  import { rowTimestampToIsoStringOrUndefined } from './rowTimestamp.js'
33
35
 
34
36
  /** Order payload shared by frontend, historical, and single-order reads. */
@@ -98,15 +100,42 @@ export const mapOrderStatus = (status: string): OrderStatus => {
98
100
  }
99
101
  }
100
102
 
101
- const skipOrder = (field: string, value: unknown): undefined => {
102
- warnSkippedVenueRow(PROVIDER_KEY, 'order', field, value)
103
+ const skipOrder = (
104
+ marketId: string,
105
+ field: string,
106
+ value: unknown
107
+ ): undefined => {
108
+ warnSkippedVenueRow(PROVIDER_KEY, 'order', field, value, { marketId })
103
109
  return undefined
104
110
  }
105
111
 
106
- const venueDecimal = (value: unknown, field: string): string | undefined => {
112
+ const warnDroppedRowOnce = createWarnOnce()
113
+
114
+ /**
115
+ * Map one venue row, or drop it. An outcome market identity, a coin the backend
116
+ * market list does not hold, and a row the mapper rejects each drop only their
117
+ * own row; each distinct mapper message warns once.
118
+ */
119
+ export const mapOrderRow = (
120
+ coin: string,
121
+ registry: MarketRegistry | undefined,
122
+ map: (market: MarketDisplay) => Order | undefined
123
+ ): Order | undefined => {
124
+ if (assetIsOutcome(coin)) {
125
+ return undefined
126
+ }
127
+ const market = registry?.get(coin)
128
+ if (market === undefined) {
129
+ return undefined
130
+ }
107
131
  try {
108
- return unknownToDecimalString(value, field, PROVIDER_KEY)
109
- } catch {
132
+ return map(market)
133
+ } catch (error) {
134
+ if (!(error instanceof PerpsError)) {
135
+ throw error
136
+ }
137
+ const message = `[${PROVIDER_KEY}] dropped order row: ${error.message}`
138
+ warnDroppedRowOnce(message, message)
110
139
  return undefined
111
140
  }
112
141
  }
@@ -133,15 +162,15 @@ export const mapOrder = (
133
162
  const { state } = raw
134
163
  const remainingSize = safeSubtractDecimalString(state.sz, state.executedSz)
135
164
  if (remainingSize === undefined) {
136
- return skipOrder('executedSz', state.executedSz)
165
+ return skipOrder(market.id, 'executedSz', state.executedSz)
137
166
  }
138
167
  const createdAt = rowTimestampToIsoStringOrUndefined(state.timestamp)
139
168
  if (createdAt === undefined) {
140
- return skipOrder('timestamp', state.timestamp)
169
+ return skipOrder(market.id, 'timestamp', state.timestamp)
141
170
  }
142
171
  const updatedAt = rowTimestampToIsoStringOrUndefined(raw.time * 1000)
143
172
  if (updatedAt === undefined) {
144
- return skipOrder('time', raw.time)
173
+ return skipOrder(market.id, 'time', raw.time)
145
174
  }
146
175
  let status: OrderStatus
147
176
  switch (raw.status.status) {
@@ -195,23 +224,20 @@ export const mapOrder = (
195
224
  }
196
225
  const o = 'order' in raw ? raw.order : raw
197
226
  const venueStatus = 'order' in raw ? raw.status : 'open'
198
- const originalSize = venueDecimal(o.origSz, 'origSz')
199
- if (originalSize === undefined) {
200
- return skipOrder('origSz', o.origSz)
201
- }
202
- const remainingSize = venueDecimal(o.sz, 'sz')
203
- if (remainingSize === undefined) {
204
- return skipOrder('sz', o.sz)
227
+ const filledSize = safeSubtractDecimalString(o.origSz, o.sz)
228
+ if (filledSize === undefined) {
229
+ return isDecimalString(o.origSz)
230
+ ? skipOrder(market.id, 'sz', o.sz)
231
+ : skipOrder(market.id, 'origSz', o.origSz)
205
232
  }
206
- const filledSize = subtractDecimalString(originalSize, remainingSize)
207
233
  const createdAt = rowTimestampToIsoStringOrUndefined(o.timestamp)
208
234
  if (createdAt === undefined) {
209
- return skipOrder('timestamp', o.timestamp)
235
+ return skipOrder(market.id, 'timestamp', o.timestamp)
210
236
  }
211
237
  const statusTimestamp = 'order' in raw ? raw.statusTimestamp : o.timestamp
212
238
  const updatedAt = rowTimestampToIsoStringOrUndefined(statusTimestamp)
213
239
  if (updatedAt === undefined) {
214
- return skipOrder('statusTimestamp', statusTimestamp)
240
+ return skipOrder(market.id, 'statusTimestamp', statusTimestamp)
215
241
  }
216
242
  let status = mapOrderStatus(venueStatus)
217
243
  if (parentOrderId !== undefined && !('order' in raw)) {
@@ -18,8 +18,12 @@ import type { HlAssetPosition } from '../types/index.js'
18
18
 
19
19
  const warnNonPositiveLeverageOnce = createWarnOnce()
20
20
 
21
- const skipPosition = (field: string, value: unknown): undefined => {
22
- warnSkippedVenueRow(PROVIDER_KEY, 'position', field, value)
21
+ const skipPosition = (
22
+ marketId: string,
23
+ field: string,
24
+ value: unknown
25
+ ): undefined => {
26
+ warnSkippedVenueRow(PROVIDER_KEY, 'position', field, value, { marketId })
23
27
  return undefined
24
28
  }
25
29
 
@@ -48,7 +52,7 @@ export const mapPosition = (
48
52
  ): Position | undefined => {
49
53
  const pos = ap.position
50
54
  if (!isDecimalString(pos.szi)) {
51
- return skipPosition('szi', pos.szi)
55
+ return skipPosition(market.id, 'szi', pos.szi)
52
56
  }
53
57
  const size = absDecimalString(pos.szi)
54
58
  const positionValue = safeAbsDecimalString(pos.positionValue)
@@ -89,7 +93,7 @@ export const mapPosition = (
89
93
  size,
90
94
  entryPrice: pos.entryPx ?? '0',
91
95
  ...(markPrice === undefined ? {} : { markPrice }),
92
- liquidationPrice: pos.liquidationPx ?? '0',
96
+ liquidationPrice: pos.liquidationPx || '0',
93
97
  unrealizedPnl: pos.unrealizedPnl,
94
98
  ...(accruedFunding === undefined ? {} : { accruedFunding }),
95
99
  ...(leverage === undefined ? {} : { leverage }),
@@ -38,12 +38,19 @@ export const spotPriceById = (
38
38
  * Convert a Hyperliquid spot balance's numeric token index into an SDK asset.
39
39
  * The balance payload has no `fullName`, so logo resolution uses the base
40
40
  * `_spot` URI rule rather than Unit-underlying lookup.
41
+ * @param registered - The registry `Asset` with the same `id`; only its `decimals` is copied.
41
42
  * @public
42
43
  */
43
- export const spotAssetFromToken = (b: HlSpotBalance): Asset => ({
44
+ export const spotAssetFromToken = (
45
+ b: HlSpotBalance,
46
+ registered?: Asset
47
+ ): Asset => ({
44
48
  ...coinAsset(b.coin),
45
49
  id: String(b.token),
46
50
  logoURI: spotLogoURI(b.coin),
51
+ ...(registered?.decimals === undefined
52
+ ? {}
53
+ : { decimals: registered.decimals }),
47
54
  })
48
55
 
49
56
  /** Assemble a typed spot {@link Balance}; `total` is native token units and its unit price and USD value use `priceById`. @public */
@@ -24,7 +24,7 @@ function requirePositionAmount(
24
24
  if (!isDecimalString(value)) {
25
25
  throw new PerpsError(
26
26
  PerpsErrorCode.ValidationError,
27
- `Invalid \`Position.${field}\`: '${value}' is not a decimal string.`
27
+ `Invalid \`Position.${field}\`: '${value}' does not match the decimal pattern.`
28
28
  )
29
29
  }
30
30
  return value
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  addDecimalString,
3
- safeAddDecimalString,
3
+ addDecimalStrings,
4
+ createWarnOnce,
5
+ isDecimalString,
4
6
  unknownToDecimalString,
5
7
  } from '@lifi/perps-sdk'
6
8
  import type {
@@ -55,35 +57,26 @@ export const perpsTotals = (
55
57
  return { accountValue, marginUsed }
56
58
  }
57
59
 
60
+ const warnSkippedTermOnce = createWarnOnce()
61
+
58
62
  /**
59
- * Sum the unrealized PnL of every position, in quote-asset units.
63
+ * Sum the unrealized PnL of every position, in quote-asset units. A term that
64
+ * does not match the decimal pattern adds nothing and warns once.
60
65
  * @public
61
66
  */
62
67
  export const sumUnrealizedPnl = (
63
68
  positions: readonly Position[]
64
- ): DecimalString =>
65
- positions.reduce(
66
- (sum, position) =>
67
- addDecimalString(
68
- sum,
69
- unknownToDecimalString(
70
- position.unrealizedPnl,
71
- 'position.unrealizedPnl',
72
- PROVIDER_KEY
73
- )
74
- ),
75
- '0'
76
- )
77
-
78
- /**
79
- * Sum every term, or `undefined` with a warning when any term is not a
80
- * decimal string. A bad term never gives a partial sum.
81
- */
82
- export const safeSumDecimalStrings = (
83
- terms: readonly string[]
84
- ): DecimalString | undefined =>
85
- terms.reduce<DecimalString | undefined>(
86
- (sum, term) =>
87
- sum === undefined ? undefined : safeAddDecimalString(sum, term),
88
- '0'
89
- )
69
+ ): DecimalString => {
70
+ const terms: DecimalString[] = []
71
+ for (const { unrealizedPnl } of positions) {
72
+ if (isDecimalString(unrealizedPnl)) {
73
+ terms.push(unrealizedPnl)
74
+ continue
75
+ }
76
+ warnSkippedTermOnce(
77
+ `${PROVIDER_KEY}|position.unrealizedPnl`,
78
+ `[${PROVIDER_KEY}] skipping a \`position.unrealizedPnl\` term that does not match the decimal pattern: '${String(unrealizedPnl).slice(0, 64)}'`
79
+ )
80
+ }
81
+ return addDecimalStrings(terms)
82
+ }
@@ -25,8 +25,8 @@ import { assetIsOutcome } from './assetId.js'
25
25
  * @param withdrawalFee - Flat venue fee in quote-asset units, set on every
26
26
  * quote-asset row. Hyperliquid deducts it from the requested amount. Absent
27
27
  * leaves every row without a fee.
28
- * @throws {PerpsError} `SDKError` when the fee or a venue figure is not a
29
- * decimal.
28
+ * @throws {PerpsError} `SDKError` when the fee or a venue figure does not match the
29
+ * decimal pattern.
30
30
  * @public
31
31
  */
32
32
  export const hyperliquidWithdrawableBalances = (
@@ -1,13 +1,14 @@
1
1
  import {
2
+ type AssetRegistry,
2
3
  addDecimalString,
3
4
  calculateTransferable,
4
5
  compareDecimalStrings,
5
6
  DecodeChain,
7
+ getAssetRegistry,
6
8
  getMarketRegistry,
7
9
  isActiveMarket,
8
10
  isActiveOrderStatus,
9
11
  isDecimalString,
10
- isDecimalStringGreaterThan,
11
12
  isDecimalStringZero,
12
13
  type MarketRegistry,
13
14
  PerpsError,
@@ -24,6 +25,7 @@ import {
24
25
  safeIsDecimalStringZero,
25
26
  safeMultiplyDecimalString,
26
27
  safeNumberToDecimalString,
28
+ safeSubtractDecimalString,
27
29
  subtractDecimalString,
28
30
  toAssetDisplay,
29
31
  toPerpsMarketDisplay,
@@ -100,6 +102,7 @@ import {
100
102
  spotPriceById,
101
103
  sumUnrealizedPnl,
102
104
  } from '../utils/index.js'
105
+ import { mapOrderRow } from '../utils/mapOrder.js'
103
106
 
104
107
  /** HL's compact `l2` snapshot carries 20 levels per side. */
105
108
  const HL_L2_BOOK_MAX_LEVELS_PER_SIDE = 20
@@ -153,6 +156,7 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
153
156
  private readonly clearinghouseRefs = new Map<string, number>()
154
157
  private readonly client: PerpsSDKClient | undefined
155
158
  private readonly registry: MarketRegistry | undefined
159
+ private readonly assetRegistry: AssetRegistry | undefined
156
160
  private readonly accountChecksByUser = new Map<string, Promise<void>>()
157
161
  // Spot-funded modes need both spot valuation and clearinghouse margin state.
158
162
  private readonly abstractionByUser = new Map<
@@ -241,6 +245,7 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
241
245
  )
242
246
  this.client = client
243
247
  this.registry = client && getMarketRegistry(client, providerKey)
248
+ this.assetRegistry = client && getAssetRegistry(client, providerKey)
244
249
  const orderApiUrl = new URL(wsUrl)
245
250
  orderApiUrl.protocol = orderApiUrl.protocol === 'wss:' ? 'https:' : 'http:'
246
251
  this.orderApiUrl = orderApiUrl.origin
@@ -973,7 +978,7 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
973
978
 
974
979
  /** Latest mid per `Market.id` across asset-context and fast feeds. */
975
980
  /** Latest mid per `Market.id` across asset-context and fast feeds; a mid
976
- * that is not a decimal string is left out. */
981
+ * that does not match the decimal pattern is left out. */
977
982
  private mergedMids(): Map<string, DecimalString> {
978
983
  const map = new Map<string, DecimalString>()
979
984
  const setMid = (id: string, mid: string | number | null | undefined) => {
@@ -1017,10 +1022,10 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
1017
1022
  provider: this.providerKey,
1018
1023
  marketId: data.coin,
1019
1024
  bids: data.levels[0]
1020
- .flatMap((l) => toOrderbookLevel(l.px, l.sz))
1025
+ .flatMap((l) => toOrderbookLevel(data.coin, l.px, l.sz))
1021
1026
  .slice(0, HL_L2_BOOK_MAX_LEVELS_PER_SIDE),
1022
1027
  asks: data.levels[1]
1023
- .flatMap((l) => toOrderbookLevel(l.px, l.sz))
1028
+ .flatMap((l) => toOrderbookLevel(data.coin, l.px, l.sz))
1024
1029
  .slice(0, HL_L2_BOOK_MAX_LEVELS_PER_SIDE),
1025
1030
  timestamp: data.time,
1026
1031
  }
@@ -1064,12 +1069,14 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
1064
1069
  provider: this.providerKey,
1065
1070
  marketId: delta.c,
1066
1071
  bids: applyCompressedL2Side(
1072
+ delta.c,
1067
1073
  previous.bids,
1068
1074
  delta.l[0],
1069
1075
  delta.r?.[0] ?? [],
1070
1076
  'bid'
1071
1077
  ),
1072
1078
  asks: applyCompressedL2Side(
1079
+ delta.c,
1073
1080
  previous.asks,
1074
1081
  delta.l[1],
1075
1082
  delta.r?.[1] ?? [],
@@ -1141,48 +1148,33 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
1141
1148
  if (epoch !== this.orderUpdatesEpoch || key !== this.orderUpdatesKey) {
1142
1149
  return
1143
1150
  }
1151
+ const user = key.slice('orderUpdates:'.length)
1144
1152
  const mapped = await Promise.all(
1145
1153
  data.map(async (update) => {
1146
1154
  const basic = update.order
1147
- if (assetIsOutcome(basic.coin)) {
1155
+ if (
1156
+ assetIsOutcome(basic.coin) ||
1157
+ this.registry?.get(basic.coin) === undefined
1158
+ ) {
1148
1159
  return undefined
1149
1160
  }
1150
- const market = this.registry?.get(basic.coin)
1151
- if (market === undefined) {
1161
+ let detail: HlOrderDetail
1162
+ try {
1163
+ detail = await this.readOrderDetail(user, client, update)
1164
+ } catch (error) {
1165
+ if (!(error instanceof PerpsError)) {
1166
+ throw error
1167
+ }
1168
+ wsLog.droppedRow(
1169
+ this.providerKey,
1170
+ 'order',
1171
+ `${basic.oid}: ${error.message}`
1172
+ )
1152
1173
  return undefined
1153
1174
  }
1154
- const response = await infoRequest<HlOrderStatusResponse>(
1155
- this.orderApiUrl,
1156
- {
1157
- type: 'orderStatus',
1158
- user: key.slice('orderUpdates:'.length),
1159
- oid: basic.oid,
1160
- },
1161
- hlInfoOptions(client)
1175
+ return mapOrderRow(basic.coin, this.registry, (resolved) =>
1176
+ mapOrder(detail, resolved)
1162
1177
  )
1163
- if (response.status !== 'order') {
1164
- throw new PerpsError(
1165
- PerpsErrorCode.OrderNotFound,
1166
- `Hyperliquid order metadata not found: ${basic.oid}`
1167
- )
1168
- }
1169
- // REST supplies execution metadata; the stream owns this event's lifecycle and quantities.
1170
- const detail: HlOrderDetail = {
1171
- status: update.status,
1172
- statusTimestamp: update.statusTimestamp,
1173
- order: {
1174
- ...response.order.order,
1175
- coin: basic.coin,
1176
- side: basic.side,
1177
- limitPx: basic.limitPx,
1178
- sz: basic.sz,
1179
- oid: basic.oid,
1180
- timestamp: basic.timestamp,
1181
- origSz: basic.origSz,
1182
- cloid: basic.cloid ?? response.order.order.cloid,
1183
- },
1184
- }
1185
- return mapOrder(detail, market)
1186
1178
  })
1187
1179
  )
1188
1180
  if (epoch !== this.orderUpdatesEpoch || key !== this.orderUpdatesKey) {
@@ -1203,6 +1195,41 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
1203
1195
  })
1204
1196
  }
1205
1197
 
1198
+ // REST supplies execution metadata; the stream owns this event's lifecycle and quantities.
1199
+ private async readOrderDetail(
1200
+ user: string,
1201
+ client: PerpsSDKClient,
1202
+ update: HlWsOrder
1203
+ ): Promise<HlOrderDetail> {
1204
+ const basic = update.order
1205
+ const response = await infoRequest<HlOrderStatusResponse>(
1206
+ this.orderApiUrl,
1207
+ { type: 'orderStatus', user, oid: basic.oid },
1208
+ hlInfoOptions(client)
1209
+ )
1210
+ if (response.status !== 'order') {
1211
+ throw new PerpsError(
1212
+ PerpsErrorCode.OrderNotFound,
1213
+ `Hyperliquid order metadata not found: ${basic.oid}`
1214
+ )
1215
+ }
1216
+ return {
1217
+ status: update.status,
1218
+ statusTimestamp: update.statusTimestamp,
1219
+ order: {
1220
+ ...response.order.order,
1221
+ coin: basic.coin,
1222
+ side: basic.side,
1223
+ limitPx: basic.limitPx,
1224
+ sz: basic.sz,
1225
+ oid: basic.oid,
1226
+ timestamp: basic.timestamp,
1227
+ origSz: basic.origSz,
1228
+ cloid: basic.cloid ?? response.order.order.cloid,
1229
+ },
1230
+ }
1231
+ }
1232
+
1206
1233
  private handleUserFills(data: HlWsUserFillsData) {
1207
1234
  // The snapshot frame carries up to ~2000 historical fills and is replayed
1208
1235
  // on every reconnect via `replaySubs`; emitting them would surface old
@@ -1308,6 +1335,7 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
1308
1335
  }
1309
1336
  }
1310
1337
  try {
1338
+ await this.assetRegistry?.sync()
1311
1339
  if (count === 0) {
1312
1340
  await this.registerSub(wireKey, payload)
1313
1341
  }
@@ -1552,15 +1580,10 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
1552
1580
  const priceById = spotPriceById(markets, this.mergedMids())
1553
1581
  const balances = data.spotState.balances
1554
1582
  .filter((balance) => !assetIsOutcome(balance.coin))
1555
- .map((balance) => ({
1556
- balance,
1557
- total: unknownToDecimalString(
1558
- balance.total,
1559
- 'spotState.balances.total',
1560
- this.providerKey
1561
- ),
1562
- }))
1563
- .filter(({ total }) => isDecimalStringGreaterThan(total, '0'))
1583
+ .filter(
1584
+ (balance) => safeIsDecimalStringGreaterThan(balance.total, '0') === true
1585
+ )
1586
+ .map((balance) => ({ balance, total: balance.total }))
1564
1587
  const pipeline = this.unifiedSummaryByUser.get(user)
1565
1588
  // Known spot markets await a price; unlisted tokens keep their unpriced balance.
1566
1589
  if (
@@ -1574,21 +1597,22 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
1574
1597
  }
1575
1598
  return
1576
1599
  }
1577
- const rows = balances.map(({ balance, total }) => ({
1578
- ...spotBalance(spotAssetFromToken(balance), total, priceById),
1579
- locked: balance.hold,
1580
- transferable: calculateTransferable(
1581
- subtractDecimalString(
1600
+ const rows = balances.map(({ balance, total }) => {
1601
+ const free = safeSubtractDecimalString(total, balance.hold)
1602
+ return {
1603
+ ...spotBalance(
1604
+ spotAssetFromToken(
1605
+ balance,
1606
+ this.assetRegistry?.get(String(balance.token))
1607
+ ),
1582
1608
  total,
1583
- unknownToDecimalString(
1584
- balance.hold,
1585
- 'spotState.balances.hold',
1586
- this.providerKey
1587
- )
1609
+ priceById
1588
1610
  ),
1589
- total
1590
- ),
1591
- }))
1611
+ locked: balance.hold,
1612
+ transferable:
1613
+ free === undefined ? '0' : calculateTransferable(free, total),
1614
+ }
1615
+ })
1592
1616
  this.emit(`spotState:${user}`, {
1593
1617
  channel: 'spotBalances',
1594
1618
  data: rows,
@@ -1776,27 +1800,38 @@ function mapSpotMarketContext(
1776
1800
  const BOOK_LEVEL_ROW = 'order book level'
1777
1801
 
1778
1802
  /** The venue price when it is a decimal string, or `undefined` after a logged skip. */
1779
- function bookLevelPrice(price: string): DecimalString | undefined {
1803
+ function bookLevelPrice(
1804
+ marketId: string,
1805
+ price: string
1806
+ ): DecimalString | undefined {
1780
1807
  if (!isDecimalString(price)) {
1781
- wsLog.skippedRow(PROVIDER_KEY, BOOK_LEVEL_ROW, 'price', price)
1808
+ wsLog.skippedRow(PROVIDER_KEY, BOOK_LEVEL_ROW, 'price', price, marketId)
1782
1809
  return undefined
1783
1810
  }
1784
1811
  return price
1785
1812
  }
1786
1813
 
1787
1814
  /** The venue size when it is a decimal string, or `undefined` after a logged skip. */
1788
- function bookLevelSize(size: string): DecimalString | undefined {
1815
+ function bookLevelSize(
1816
+ marketId: string,
1817
+ size: string
1818
+ ): DecimalString | undefined {
1789
1819
  if (!isDecimalString(size)) {
1790
- wsLog.skippedRow(PROVIDER_KEY, BOOK_LEVEL_ROW, 'size', size)
1820
+ wsLog.skippedRow(PROVIDER_KEY, BOOK_LEVEL_ROW, 'size', size, marketId)
1791
1821
  return undefined
1792
1822
  }
1793
1823
  return size
1794
1824
  }
1795
1825
 
1796
1826
  /** One order-book level, or none when the venue price or size is invalid. */
1797
- function toOrderbookLevel(price: string, size: string): OrderbookLevel[] {
1798
- const levelPrice = bookLevelPrice(price)
1799
- const levelSize = levelPrice === undefined ? undefined : bookLevelSize(size)
1827
+ function toOrderbookLevel(
1828
+ marketId: string,
1829
+ price: string,
1830
+ size: string
1831
+ ): OrderbookLevel[] {
1832
+ const levelPrice = bookLevelPrice(marketId, price)
1833
+ const levelSize =
1834
+ levelPrice === undefined ? undefined : bookLevelSize(marketId, size)
1800
1835
  return levelPrice === undefined || levelSize === undefined
1801
1836
  ? []
1802
1837
  : [{ price: levelPrice, size: levelSize }]
@@ -1813,6 +1848,7 @@ function compactRemovalPrice(
1813
1848
  }
1814
1849
 
1815
1850
  function applyCompressedL2Side(
1851
+ marketId: string,
1816
1852
  previous: OrderbookLevel[],
1817
1853
  updates: HlWsCompressedL2Data['l'][number],
1818
1854
  removals: NonNullable<HlWsCompressedL2Data['r']>[number],
@@ -1826,11 +1862,11 @@ function applyCompressedL2Side(
1826
1862
  }
1827
1863
  }
1828
1864
  for (const update of updates) {
1829
- const price = bookLevelPrice(update.p)
1865
+ const price = bookLevelPrice(marketId, update.p)
1830
1866
  if (price === undefined) {
1831
1867
  continue
1832
1868
  }
1833
- const size = bookLevelSize(update.s)
1869
+ const size = bookLevelSize(marketId, update.s)
1834
1870
  // A level whose new size is unknown must not keep its old size.
1835
1871
  if (size === undefined || safeIsDecimalStringZero(size) !== false) {
1836
1872
  byPrice.delete(price)