@lifi/perps-sdk-provider-hyperliquid 20.0.0 → 21.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 (111) hide show
  1. package/README.md +45 -0
  2. package/dist/cjs/HyperliquidProvider.d.ts.map +1 -1
  3. package/dist/cjs/HyperliquidProvider.js +2 -2
  4. package/dist/cjs/HyperliquidProvider.js.map +1 -1
  5. package/dist/cjs/services/getAccount.d.ts.map +1 -1
  6. package/dist/cjs/services/getAccount.js +5 -7
  7. package/dist/cjs/services/getAccount.js.map +1 -1
  8. package/dist/cjs/services/getActivity.d.ts.map +1 -1
  9. package/dist/cjs/services/getActivity.js +68 -44
  10. package/dist/cjs/services/getActivity.js.map +1 -1
  11. package/dist/cjs/services/getFills.d.ts.map +1 -1
  12. package/dist/cjs/services/getFills.js +30 -38
  13. package/dist/cjs/services/getFills.js.map +1 -1
  14. package/dist/cjs/services/getWithdrawableBalances.js +1 -1
  15. package/dist/cjs/services/getWithdrawableBalances.js.map +1 -1
  16. package/dist/cjs/services/historyPagination.d.ts +22 -0
  17. package/dist/cjs/services/historyPagination.d.ts.map +1 -0
  18. package/dist/cjs/services/historyPagination.js +125 -0
  19. package/dist/cjs/services/historyPagination.js.map +1 -0
  20. package/dist/cjs/utils/index.d.ts +1 -1
  21. package/dist/cjs/utils/index.d.ts.map +1 -1
  22. package/dist/cjs/utils/index.js +3 -3
  23. package/dist/cjs/utils/index.js.map +1 -1
  24. package/dist/cjs/utils/liquidation.js +1 -1
  25. package/dist/cjs/utils/liquidation.js.map +1 -1
  26. package/dist/cjs/utils/orderFormatting.d.ts +3 -2
  27. package/dist/cjs/utils/orderFormatting.d.ts.map +1 -1
  28. package/dist/cjs/utils/orderFormatting.js +4 -4
  29. package/dist/cjs/utils/orderFormatting.js.map +1 -1
  30. package/dist/cjs/utils/spotBalance.d.ts.map +1 -1
  31. package/dist/cjs/utils/spotBalance.js +2 -2
  32. package/dist/cjs/utils/spotBalance.js.map +1 -1
  33. package/dist/cjs/utils/withdrawableBalances.d.ts +1 -1
  34. package/dist/cjs/utils/withdrawableBalances.d.ts.map +1 -1
  35. package/dist/cjs/utils/withdrawableBalances.js +7 -4
  36. package/dist/cjs/utils/withdrawableBalances.js.map +1 -1
  37. package/dist/cjs/websocket/HyperliquidWsProvider.d.ts +6 -0
  38. package/dist/cjs/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  39. package/dist/cjs/websocket/HyperliquidWsProvider.js +135 -37
  40. package/dist/cjs/websocket/HyperliquidWsProvider.js.map +1 -1
  41. package/dist/esm/HyperliquidProvider.d.ts.map +1 -1
  42. package/dist/esm/HyperliquidProvider.js +3 -3
  43. package/dist/esm/HyperliquidProvider.js.map +1 -1
  44. package/dist/esm/services/getAccount.d.ts.map +1 -1
  45. package/dist/esm/services/getAccount.js +6 -5
  46. package/dist/esm/services/getAccount.js.map +1 -1
  47. package/dist/esm/services/getActivity.d.ts +3 -13
  48. package/dist/esm/services/getActivity.d.ts.map +1 -1
  49. package/dist/esm/services/getActivity.js +74 -64
  50. package/dist/esm/services/getActivity.js.map +1 -1
  51. package/dist/esm/services/getFills.d.ts +2 -14
  52. package/dist/esm/services/getFills.d.ts.map +1 -1
  53. package/dist/esm/services/getFills.js +32 -58
  54. package/dist/esm/services/getFills.js.map +1 -1
  55. package/dist/esm/services/getWithdrawableBalances.js +1 -1
  56. package/dist/esm/services/getWithdrawableBalances.js.map +1 -1
  57. package/dist/esm/services/historyPagination.d.ts +22 -0
  58. package/dist/esm/services/historyPagination.d.ts.map +1 -0
  59. package/dist/esm/services/historyPagination.js +121 -0
  60. package/dist/esm/services/historyPagination.js.map +1 -0
  61. package/dist/esm/utils/index.d.ts +1 -1
  62. package/dist/esm/utils/index.d.ts.map +1 -1
  63. package/dist/esm/utils/index.js +1 -1
  64. package/dist/esm/utils/index.js.map +1 -1
  65. package/dist/esm/utils/liquidation.js +2 -2
  66. package/dist/esm/utils/liquidation.js.map +1 -1
  67. package/dist/esm/utils/orderFormatting.d.ts +14 -12
  68. package/dist/esm/utils/orderFormatting.d.ts.map +1 -1
  69. package/dist/esm/utils/orderFormatting.js +14 -14
  70. package/dist/esm/utils/orderFormatting.js.map +1 -1
  71. package/dist/esm/utils/spotBalance.d.ts.map +1 -1
  72. package/dist/esm/utils/spotBalance.js +3 -3
  73. package/dist/esm/utils/spotBalance.js.map +1 -1
  74. package/dist/esm/utils/withdrawableBalances.d.ts +1 -1
  75. package/dist/esm/utils/withdrawableBalances.d.ts.map +1 -1
  76. package/dist/esm/utils/withdrawableBalances.js +7 -4
  77. package/dist/esm/utils/withdrawableBalances.js.map +1 -1
  78. package/dist/esm/websocket/HyperliquidWsProvider.d.ts +6 -0
  79. package/dist/esm/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  80. package/dist/esm/websocket/HyperliquidWsProvider.js +138 -50
  81. package/dist/esm/websocket/HyperliquidWsProvider.js.map +1 -1
  82. package/dist/types/HyperliquidProvider.d.ts.map +1 -1
  83. package/dist/types/services/getAccount.d.ts.map +1 -1
  84. package/dist/types/services/getActivity.d.ts +3 -13
  85. package/dist/types/services/getActivity.d.ts.map +1 -1
  86. package/dist/types/services/getFills.d.ts +2 -14
  87. package/dist/types/services/getFills.d.ts.map +1 -1
  88. package/dist/types/services/historyPagination.d.ts +22 -0
  89. package/dist/types/services/historyPagination.d.ts.map +1 -0
  90. package/dist/types/utils/index.d.ts +1 -1
  91. package/dist/types/utils/index.d.ts.map +1 -1
  92. package/dist/types/utils/orderFormatting.d.ts +14 -12
  93. package/dist/types/utils/orderFormatting.d.ts.map +1 -1
  94. package/dist/types/utils/spotBalance.d.ts.map +1 -1
  95. package/dist/types/utils/withdrawableBalances.d.ts +1 -1
  96. package/dist/types/utils/withdrawableBalances.d.ts.map +1 -1
  97. package/dist/types/websocket/HyperliquidWsProvider.d.ts +6 -0
  98. package/dist/types/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  99. package/package.json +3 -3
  100. package/src/HyperliquidProvider.ts +6 -5
  101. package/src/services/getAccount.ts +18 -18
  102. package/src/services/getActivity.ts +133 -104
  103. package/src/services/getFills.ts +42 -95
  104. package/src/services/getWithdrawableBalances.ts +1 -1
  105. package/src/services/historyPagination.ts +169 -0
  106. package/src/utils/index.ts +2 -2
  107. package/src/utils/liquidation.ts +2 -2
  108. package/src/utils/orderFormatting.ts +20 -16
  109. package/src/utils/spotBalance.ts +5 -3
  110. package/src/utils/withdrawableBalances.ts +12 -7
  111. package/src/websocket/HyperliquidWsProvider.ts +151 -63
@@ -1,19 +1,12 @@
1
1
  import { getMarketRegistry, type SDKRequestOptions } from '@lifi/perps-sdk'
2
2
  import type { Fill, FillsResponse } from '@lifi/perps-types'
3
3
  import type { Address } from 'viem'
4
- import {
5
- DEFAULT_HISTORY_LIMIT,
6
- MAX_HISTORY_LIMIT,
7
- PROVIDER_KEY,
8
- } from '../constants.js'
4
+ import { PROVIDER_KEY } from '../constants.js'
9
5
  import type { HyperliquidContext } from '../context.js'
10
- import type {
11
- HlUserFill,
12
- HlUserFills,
13
- HlUserFillsByTime,
14
- } from '../types/index.js'
6
+ import type { HlUserFills } from '../types/index.js'
15
7
  import { assetIsOutcome, mapFill } from '../utils/index.js'
16
8
  import { hlInfoOptions, infoRequest } from '../utils/infoClient.js'
9
+ import { getHistoryLimit, historyPage } from './historyPagination.js'
17
10
 
18
11
  /**
19
12
  * Parameters for {@link getFills}.
@@ -22,7 +15,7 @@ import { hlInfoOptions, infoRequest } from '../utils/infoClient.js'
22
15
  */
23
16
  export interface GetFillsParams {
24
17
  address: Address
25
- /** Maximum items returned; defaults to 50 and is capped at 200. */
18
+ /** Positive safe integer; defaults to 50 and is capped at 200. */
26
19
  limit?: number
27
20
  /** Opaque cursor returned in the previous page's `pagination.cursor`. */
28
21
  cursor?: string
@@ -32,101 +25,55 @@ export interface GetFillsParams {
32
25
  endTime?: number
33
26
  }
34
27
 
35
- interface FillCursor {
36
- time: number
37
- tid: number
38
- }
39
-
40
- // HL's docs specify neither newest-first ordering nor monotonic `tid` for
41
- // `userFills`/`userFillsByTime`, so pagination can't rely on upstream order —
42
- // it must impose its own (time desc, tid desc as tiebreaker for same-time fills).
43
- const compareFillsDesc = (a: FillCursor, b: FillCursor): number =>
44
- b.time - a.time || b.tid - a.tid
45
-
46
- const encodeCursor = (fill: FillCursor): string => `${fill.time}:${fill.tid}`
47
-
48
- const decodeCursor = (cursor: string): FillCursor => {
49
- const [time, tid] = cursor.split(':').map(Number)
50
- return { time, tid }
51
- }
52
-
53
- /**
54
- * Fetch the user's fills history. When `startTime` or `endTime` is provided,
55
- * Hyperliquid's `userFillsByTime` endpoint is used; otherwise the unbounded
56
- * `userFills` endpoint returns the full history. The backend's enriched asset
57
- * list supplies the display fields; only the fills endpoint is read direct
58
- * from HL.
59
- *
60
- * The response is sorted (time desc, `tid` as tiebreaker) before pagination —
61
- * see {@link compareFillsDesc}. The cursor encodes the last returned fill's
62
- * `(time, tid)`; the next page keeps fills that sort strictly after it.
63
- * @throws {PerpsError} On Hyperliquid REST error, network, or parsing failures.
64
- * @public
65
- */
28
+ /** Fetch retained fills newest-first, using opaque cursors across inclusive time ranges. */
66
29
  export const getFills = async (
67
30
  { client, apiUrl }: HyperliquidContext,
68
31
  params: GetFillsParams,
69
32
  options?: SDKRequestOptions
70
33
  ): Promise<FillsResponse> => {
34
+ const limit = getHistoryLimit(params.limit)
71
35
  const registry = getMarketRegistry(client, PROVIDER_KEY)
72
36
  await registry.sync()
73
37
  const infoOpts = hlInfoOptions(client, options)
74
38
 
75
- const limit = Math.min(
76
- params.limit ?? DEFAULT_HISTORY_LIMIT,
77
- MAX_HISTORY_LIMIT
78
- )
79
-
80
- const useByTime =
81
- params.startTime !== undefined || params.endTime !== undefined
82
-
83
- const allFills = useByTime
84
- ? await infoRequest<HlUserFillsByTime>(
85
- apiUrl,
86
- {
87
- type: 'userFillsByTime',
88
- user: params.address,
89
- startTime: params.startTime ?? 0,
90
- ...(params.endTime !== undefined ? { endTime: params.endTime } : {}),
91
- },
92
- infoOpts
93
- )
94
- : await infoRequest<HlUserFills>(
39
+ let useRecent =
40
+ params.cursor === undefined &&
41
+ params.startTime === undefined &&
42
+ params.endTime === undefined
43
+ const page = await historyPage(
44
+ params,
45
+ limit,
46
+ async (startTime, endTime) => {
47
+ const fills = await infoRequest<HlUserFills>(
95
48
  apiUrl,
96
- { type: 'userFills', user: params.address },
49
+ useRecent
50
+ ? { type: 'userFills', user: params.address }
51
+ : {
52
+ type: 'userFillsByTime',
53
+ user: params.address,
54
+ startTime,
55
+ endTime,
56
+ },
97
57
  infoOpts
98
58
  )
99
-
100
- const sorted = [...allFills].sort(compareFillsDesc)
101
-
102
- const filtered =
103
- params.cursor === undefined
104
- ? sorted
105
- : sorted.filter(
106
- (f) => compareFillsDesc(f, decodeCursor(params.cursor!)) > 0
107
- )
108
-
109
- const hasMore = filtered.length > limit
110
- const page = filtered.slice(0, limit)
111
- // `get`, not `require`: a coin the backend market list does not hold drops
112
- // only its own row instead of rejecting the whole page. The registry warns
113
- // once per unresolved id. A delisted market still resolves, so its rows stay.
114
- const items = page.flatMap((f): Fill[] => {
115
- if (assetIsOutcome(f.coin)) {
116
- return []
117
- }
118
- const market = registry.get(f.coin)
119
- return market === undefined ? [] : [mapFill(f, market)]
120
- })
121
- const lastPageFill: HlUserFill | undefined = page[page.length - 1]
122
-
123
- return {
124
- provider: PROVIDER_KEY,
125
- items,
126
- pagination: {
127
- limit,
128
- hasMore,
129
- cursor: lastPageFill ? encodeCursor(lastPageFill) : undefined,
59
+ useRecent = false
60
+ if (fills.length >= 2000) {
61
+ return { items: [], saturated: true }
62
+ }
63
+ const items = fills.flatMap((fill): Fill[] => {
64
+ if (assetIsOutcome(fill.coin)) {
65
+ return []
66
+ }
67
+ const market = registry.get(fill.coin)
68
+ return market === undefined ? [] : [mapFill(fill, market)]
69
+ })
70
+ return { items, saturated: false }
130
71
  },
131
- }
72
+ (fill) => ({
73
+ time: new Date(fill.createdAt).getTime(),
74
+ key: Number(fill.id),
75
+ }),
76
+ options?.signal
77
+ )
78
+ return { provider: PROVIDER_KEY, ...page }
132
79
  }
@@ -89,6 +89,6 @@ export const getWithdrawableBalances = async (
89
89
  state,
90
90
  spotState,
91
91
  quoteAsset.id,
92
- feeUsd === undefined ? undefined : String(feeUsd)
92
+ feeUsd
93
93
  )
94
94
  }
@@ -0,0 +1,169 @@
1
+ import { PerpsError } from '@lifi/perps-sdk'
2
+ import { PerpsErrorCode } from '@lifi/perps-types'
3
+ import { DEFAULT_HISTORY_LIMIT, MAX_HISTORY_LIMIT } from '../constants.js'
4
+
5
+ type HistoryKey = string | number
6
+ interface HistoryBoundary {
7
+ time: number
8
+ key: HistoryKey
9
+ startTime: number
10
+ endTime: number
11
+ }
12
+
13
+ const compareKeys = (a: HistoryKey, b: HistoryKey): number =>
14
+ typeof a === 'number' && typeof b === 'number'
15
+ ? b - a
16
+ : a < b
17
+ ? 1
18
+ : a > b
19
+ ? -1
20
+ : 0
21
+
22
+ export const getHistoryLimit = (limit = DEFAULT_HISTORY_LIMIT): number => {
23
+ if (!Number.isSafeInteger(limit) || limit < 1) {
24
+ throw new PerpsError(
25
+ PerpsErrorCode.ValidationError,
26
+ 'Hyperliquid history limit must be a positive safe integer'
27
+ )
28
+ }
29
+ return Math.min(limit, MAX_HISTORY_LIMIT)
30
+ }
31
+
32
+ const decodeCursor = (cursor: string): HistoryBoundary => {
33
+ let value: unknown
34
+ try {
35
+ value = JSON.parse(cursor)
36
+ } catch {
37
+ throw new PerpsError(
38
+ PerpsErrorCode.ValidationError,
39
+ 'Invalid Hyperliquid history cursor'
40
+ )
41
+ }
42
+ if (
43
+ !Array.isArray(value) ||
44
+ value.length !== 4 ||
45
+ !Number.isSafeInteger(value[0]) ||
46
+ (typeof value[1] !== 'string' && typeof value[1] !== 'number') ||
47
+ !Number.isSafeInteger(value[2]) ||
48
+ value[2] > value[0] ||
49
+ !Number.isSafeInteger(value[3]) ||
50
+ value[3] < value[0]
51
+ ) {
52
+ throw new PerpsError(
53
+ PerpsErrorCode.ValidationError,
54
+ 'Invalid Hyperliquid history cursor'
55
+ )
56
+ }
57
+ return {
58
+ time: value[0],
59
+ key: value[1],
60
+ startTime: value[2],
61
+ endTime: value[3],
62
+ }
63
+ }
64
+
65
+ export async function historyPage<T>(
66
+ params: { startTime?: number; endTime?: number; cursor?: string },
67
+ limit: number,
68
+ fetchWindow: (
69
+ startTime: number,
70
+ endTime: number,
71
+ snapshotEnd: number
72
+ ) => Promise<{ items: T[]; saturated: boolean }>,
73
+ identify: (item: T) => { time: number; key: HistoryKey },
74
+ signal?: AbortSignal
75
+ ): Promise<{
76
+ items: T[]
77
+ pagination: { limit: number; hasMore: boolean; cursor?: string }
78
+ }> {
79
+ const boundary =
80
+ params.cursor === undefined ? undefined : decodeCursor(params.cursor)
81
+ const lower = params.startTime ?? 0
82
+ const snapshotEnd = boundary?.endTime ?? params.endTime ?? Date.now()
83
+ const upper = Math.min(snapshotEnd, boundary?.time ?? Infinity)
84
+ const windows: { startTime: number; endTime: number }[] = []
85
+ if (boundary && boundary.startTime > lower) {
86
+ windows.push({ startTime: lower, endTime: boundary.startTime - 1 })
87
+ }
88
+ windows.push({
89
+ startTime: Math.max(lower, boundary?.startTime ?? lower),
90
+ endTime: upper,
91
+ })
92
+ const items: T[] = []
93
+ let last: HistoryBoundary | undefined
94
+ while (windows.length > 0) {
95
+ signal?.throwIfAborted()
96
+ const window = windows.pop()!
97
+ if (window.startTime > window.endTime) {
98
+ continue
99
+ }
100
+ const result = await fetchWindow(
101
+ window.startTime,
102
+ window.endTime,
103
+ snapshotEnd
104
+ )
105
+ signal?.throwIfAborted()
106
+ if (result.saturated) {
107
+ if (window.startTime === window.endTime) {
108
+ throw new PerpsError(
109
+ PerpsErrorCode.ThirdPartyError,
110
+ 'Hyperliquid history saturates a single millisecond; its time-only API cannot paginate this timestamp safely'
111
+ )
112
+ }
113
+ // Time APIs truncate from the start, so a capped response cannot supply a newest page.
114
+ const midpoint =
115
+ window.startTime + Math.floor((window.endTime - window.startTime) / 2)
116
+ windows.push({ startTime: window.startTime, endTime: midpoint })
117
+ windows.push({ startTime: midpoint + 1, endTime: window.endTime })
118
+ continue
119
+ }
120
+ const sorted = result.items.sort((a, b) => {
121
+ const left = identify(a)
122
+ const right = identify(b)
123
+ return right.time - left.time || compareKeys(left.key, right.key)
124
+ })
125
+ for (const item of sorted) {
126
+ const identity = identify(item)
127
+ if (identity.time < window.startTime || identity.time > window.endTime) {
128
+ continue
129
+ }
130
+ if (
131
+ boundary &&
132
+ (identity.time > boundary.time ||
133
+ (identity.time === boundary.time &&
134
+ compareKeys(identity.key, boundary.key) <= 0))
135
+ ) {
136
+ continue
137
+ }
138
+ if (items.length === limit) {
139
+ return {
140
+ items,
141
+ pagination: {
142
+ limit,
143
+ hasMore: true,
144
+ cursor:
145
+ last &&
146
+ JSON.stringify([
147
+ last.time,
148
+ last.key,
149
+ last.startTime,
150
+ last.endTime,
151
+ ]),
152
+ },
153
+ }
154
+ }
155
+ items.push(item)
156
+ last = { ...identity, startTime: window.startTime, endTime: snapshotEnd }
157
+ }
158
+ }
159
+ return {
160
+ items,
161
+ pagination: {
162
+ limit,
163
+ hasMore: false,
164
+ cursor:
165
+ last &&
166
+ JSON.stringify([last.time, last.key, last.startTime, last.endTime]),
167
+ },
168
+ }
169
+ }
@@ -45,9 +45,9 @@ export {
45
45
  export { isOpenAssetPosition, mapPosition } from './mapPosition.js'
46
46
  export { coinAsset, perpsDexNames } from './marketDisplay.js'
47
47
  export {
48
- formatOrderPrice,
49
- formatOrderSize,
50
48
  getMaxPriceDecimals,
49
+ snapOrderPrice,
50
+ snapOrderSize,
51
51
  } from './orderFormatting.js'
52
52
  export { priceStepToAggregation } from './priceStepToAggregation.js'
53
53
  export {
@@ -12,7 +12,7 @@
12
12
  * - margin_available = isolated_margin - maintenance_margin_required
13
13
  */
14
14
 
15
- import { estimateIsolatedLiquidationPrice } from '@lifi/perps-sdk'
15
+ import { estimateLiquidationPrice } from '@lifi/perps-sdk'
16
16
 
17
17
  /**
18
18
  * Calculate the maintenance margin fraction for a Hyperliquid asset.
@@ -65,7 +65,7 @@ export function calculateLiquidationPrice(
65
65
  if (mmr === undefined) {
66
66
  return undefined
67
67
  }
68
- return estimateIsolatedLiquidationPrice({
68
+ return estimateLiquidationPrice({
69
69
  entryPrice,
70
70
  leverage,
71
71
  isLong,
@@ -1,12 +1,13 @@
1
1
  /**
2
- * Hyperliquid order size and price formatting for exchange submission.
2
+ * Hyperliquid order size and price snapping for exchange submission.
3
3
  *
4
- * These encode Hyperliquid-specific order submission rules. Getting size/price
5
- * formatting wrong causes rejected orders.
4
+ * These encode Hyperliquid-specific order submission rules. Snapping a size or
5
+ * a price onto the wrong grid causes rejected orders.
6
6
  *
7
7
  * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/tick-and-lot-size
8
8
  */
9
9
 
10
+ import type { DecimalString } from '@lifi/perps-types'
10
11
  import Big from 'big.js'
11
12
 
12
13
  /**
@@ -36,28 +37,30 @@ export function getMaxPriceDecimals(
36
37
  }
37
38
 
38
39
  /**
39
- * Format a size value for order submission.
40
+ * Snap a size onto the asset's lot grid for order submission.
40
41
  *
41
42
  * Per Hyperliquid docs (tick-and-lot-size):
42
43
  * - Size must be rounded to the asset's szDecimals
43
44
  * - Trailing zeroes must be removed for signing
44
45
  *
45
- * @param size - The size value to format
46
+ * @param size - The size to snap, as a decimal string
46
47
  * @param szDecimals - Number of decimal places allowed for this asset (from meta)
47
- * @returns Size as a string with correct precision, no trailing zeros
48
+ * @returns Size as a decimal string with correct precision, no trailing zeros
48
49
  * @public
49
50
  */
50
- export function formatOrderSize(size: number, szDecimals: number): string {
51
+ export function snapOrderSize(
52
+ size: DecimalString,
53
+ szDecimals: number
54
+ ): DecimalString {
51
55
  // Big.roundDown truncates toward zero — never round a size up, it could
52
- // exceed available balance. Exact decimal arithmetic: flooring the float
53
- // product dropped a lot step (0.29 * 100 === 28.999999999999996).
56
+ // exceed available balance.
54
57
  const truncated = new Big(size).round(szDecimals, Big.roundDown)
55
58
  // toFixed() with no dp always emits plain notation; eq(0) guards '-0'
56
59
  return truncated.eq(0) ? '0' : truncated.toFixed()
57
60
  }
58
61
 
59
62
  /**
60
- * Format a price value for order submission.
63
+ * Snap a price onto the asset's tick grid for order submission.
61
64
  *
62
65
  * Per Hyperliquid docs (tick-and-lot-size):
63
66
  * - Maximum 5 significant figures
@@ -66,19 +69,20 @@ export function formatOrderSize(size: number, szDecimals: number): string {
66
69
  * - Trailing zeroes must be removed for signing
67
70
  *
68
71
  * Rounding is half-up (away from zero at exact halfway) on the true decimal
69
- * value, not on the binary float — (1.005).toFixed(2) would give '1.00'.
72
+ * value, which is why the price arrives as a string: `(1.005).toFixed(2)`
73
+ * would give `'1.00'`.
70
74
  *
71
- * @param price - The price value to format
75
+ * @param price - The price to snap, as a decimal string
72
76
  * @param szDecimals - The asset's szDecimals (affects max price decimals)
73
77
  * @param market - Optional market type (e.g. 'spot'). Defaults to perps rules.
74
- * @returns Price as a string with correct precision, no trailing zeros
78
+ * @returns Price as a decimal string with correct precision, no trailing zeros
75
79
  * @public
76
80
  */
77
- export function formatOrderPrice(
78
- price: number,
81
+ export function snapOrderPrice(
82
+ price: DecimalString,
79
83
  szDecimals: number,
80
84
  market?: string
81
- ): string {
85
+ ): DecimalString {
82
86
  const maxPriceDecimals = getMaxPriceDecimals(szDecimals, market)
83
87
 
84
88
  let rounded = new Big(price).round(maxPriceDecimals, Big.roundHalfUp)
@@ -1,4 +1,4 @@
1
- import { stringToFloat } from '@lifi/perps-sdk'
1
+ import { numberToDecimalString, parseDecimal } from '@lifi/perps-sdk'
2
2
  import type { Asset, Balance, Market } from '@lifi/perps-types'
3
3
  import { SPOT_MARKET_ID } from '../constants.js'
4
4
  import type { HlSpotBalance } from '../types/index.js'
@@ -52,8 +52,10 @@ export const spotBalance = (
52
52
  categoryId: SPOT_MARKET_ID,
53
53
  asset,
54
54
  units: total,
55
- valueUsd: (stringToFloat(total) * price).toString(),
55
+ valueUsd: numberToDecimalString(
56
+ (parseDecimal(total) ?? Number.NaN) * price
57
+ ),
56
58
  // A zero entry means the map holds no mark for the asset, not a free asset.
57
- ...(price > 0 ? { price: price.toString() } : {}),
59
+ ...(price > 0 ? { price: numberToDecimalString(price) } : {}),
58
60
  }
59
61
  }
@@ -1,4 +1,7 @@
1
- import type { ProviderWithdrawableBalance } from '@lifi/perps-sdk'
1
+ import {
2
+ calculateWithdrawMax,
3
+ type ProviderWithdrawableBalance,
4
+ } from '@lifi/perps-sdk'
2
5
  import type {
3
6
  HlAbstractionMode,
4
7
  HlClearinghouseState,
@@ -50,24 +53,26 @@ export const hyperliquidWithdrawableBalances = (
50
53
  )
51
54
  if (spot.gt(0)) {
52
55
  const assetId = String(balance.token)
53
- rows.push({
56
+ const row = {
54
57
  assetId,
55
- route: 'spot',
58
+ route: 'spot' as const,
56
59
  available: spot.toFixed(),
57
60
  ...feeFor(assetId),
58
- })
61
+ }
62
+ rows.push({ ...row, max: calculateWithdrawMax(row) })
59
63
  }
60
64
  }
61
65
  }
62
66
 
63
67
  const perps = toWireBig(state.withdrawable, 'clearinghouseState.withdrawable')
64
68
  if (perps.gt(0)) {
65
- rows.push({
69
+ const row = {
66
70
  assetId: quoteAssetId,
67
- route: 'perps',
71
+ route: 'perps' as const,
68
72
  available: perps.toFixed(),
69
73
  ...feeFor(quoteAssetId),
70
- })
74
+ }
75
+ rows.push({ ...row, max: calculateWithdrawMax(row) })
71
76
  }
72
77
 
73
78
  return rows