@lifi/perps-sdk-provider-hyperliquid 31.0.1 → 31.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.
- package/README.md +20 -0
- package/dist/cjs/constants.d.ts +1 -0
- package/dist/cjs/constants.d.ts.map +1 -1
- package/dist/cjs/constants.js +2 -1
- package/dist/cjs/constants.js.map +1 -1
- package/dist/cjs/services/getFills.d.ts.map +1 -1
- package/dist/cjs/services/getFills.js +14 -11
- package/dist/cjs/services/getFills.js.map +1 -1
- package/dist/cjs/services/twapSliceFills.d.ts.map +1 -1
- package/dist/cjs/services/twapSliceFills.js +3 -3
- package/dist/cjs/services/twapSliceFills.js.map +1 -1
- package/dist/cjs/types/ws.d.ts +6 -1
- package/dist/cjs/types/ws.d.ts.map +1 -1
- package/dist/cjs/utils/mapFill.d.ts.map +1 -1
- package/dist/cjs/utils/mapFill.js +2 -1
- package/dist/cjs/utils/mapFill.js.map +1 -1
- package/dist/cjs/websocket/HyperliquidWsProvider.d.ts +4 -0
- package/dist/cjs/websocket/HyperliquidWsProvider.d.ts.map +1 -1
- package/dist/cjs/websocket/HyperliquidWsProvider.js +59 -31
- package/dist/cjs/websocket/HyperliquidWsProvider.js.map +1 -1
- package/dist/esm/constants.d.ts +6 -0
- package/dist/esm/constants.d.ts.map +1 -1
- package/dist/esm/constants.js +6 -0
- package/dist/esm/constants.js.map +1 -1
- package/dist/esm/services/getFills.d.ts +6 -3
- package/dist/esm/services/getFills.d.ts.map +1 -1
- package/dist/esm/services/getFills.js +19 -13
- package/dist/esm/services/getFills.js.map +1 -1
- package/dist/esm/services/twapSliceFills.d.ts.map +1 -1
- package/dist/esm/services/twapSliceFills.js +3 -4
- package/dist/esm/services/twapSliceFills.js.map +1 -1
- package/dist/esm/types/ws.d.ts +7 -1
- package/dist/esm/types/ws.d.ts.map +1 -1
- package/dist/esm/utils/mapFill.d.ts.map +1 -1
- package/dist/esm/utils/mapFill.js +3 -1
- package/dist/esm/utils/mapFill.js.map +1 -1
- package/dist/esm/websocket/HyperliquidWsProvider.d.ts +10 -0
- package/dist/esm/websocket/HyperliquidWsProvider.d.ts.map +1 -1
- package/dist/esm/websocket/HyperliquidWsProvider.js +68 -31
- package/dist/esm/websocket/HyperliquidWsProvider.js.map +1 -1
- package/dist/types/constants.d.ts +6 -0
- package/dist/types/constants.d.ts.map +1 -1
- package/dist/types/services/getFills.d.ts +6 -3
- package/dist/types/services/getFills.d.ts.map +1 -1
- package/dist/types/services/twapSliceFills.d.ts.map +1 -1
- package/dist/types/types/ws.d.ts +7 -1
- package/dist/types/types/ws.d.ts.map +1 -1
- package/dist/types/utils/mapFill.d.ts.map +1 -1
- package/dist/types/websocket/HyperliquidWsProvider.d.ts +10 -0
- package/dist/types/websocket/HyperliquidWsProvider.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/constants.ts +6 -0
- package/src/services/getFills.ts +37 -25
- package/src/services/twapSliceFills.ts +3 -5
- package/src/types/ws.ts +8 -1
- package/src/utils/mapFill.ts +6 -1
- package/src/websocket/HyperliquidWsProvider.ts +88 -32
package/src/services/getFills.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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 { PROVIDER_KEY } from '../constants.js'
|
|
4
|
+
import { FILL_ROWS_CAP, PROVIDER_KEY } from '../constants.js'
|
|
5
5
|
import type { HyperliquidContext } from '../context.js'
|
|
6
|
-
import type { HlUserFills } from '../types/index.js'
|
|
6
|
+
import type { HlTwapSliceFill, HlUserFills } from '../types/index.js'
|
|
7
7
|
import { assetIsOutcome, mapFill } from '../utils/index.js'
|
|
8
8
|
import { hlInfoOptions, infoRequest } from '../utils/infoClient.js'
|
|
9
9
|
import { requireAccountExists } from './getAccountExists.js'
|
|
@@ -20,13 +20,16 @@ export interface GetFillsParams {
|
|
|
20
20
|
limit?: number
|
|
21
21
|
/** Opaque cursor returned in the previous page's `pagination.cursor`. */
|
|
22
22
|
cursor?: string
|
|
23
|
-
/** Inclusive lower bound in milliseconds; selects `
|
|
23
|
+
/** Inclusive lower bound in milliseconds; selects the `...ByTime` feeds. */
|
|
24
24
|
startTime?: number
|
|
25
|
-
/** Inclusive upper bound in milliseconds; selects `
|
|
25
|
+
/** Inclusive upper bound in milliseconds; selects the `...ByTime` feeds. */
|
|
26
26
|
endTime?: number
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Fetch retained regular and TWAP slice fills newest-first, using opaque
|
|
31
|
+
* cursors across inclusive time ranges.
|
|
32
|
+
*/
|
|
30
33
|
export const getFills = async (
|
|
31
34
|
{ client, apiUrl }: HyperliquidContext,
|
|
32
35
|
params: GetFillsParams,
|
|
@@ -46,30 +49,39 @@ export const getFills = async (
|
|
|
46
49
|
params,
|
|
47
50
|
limit,
|
|
48
51
|
async (startTime, endTime) => {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
52
|
+
const user = params.address
|
|
53
|
+
const [fills, sliceFills] = await Promise.all([
|
|
54
|
+
infoRequest<HlUserFills>(
|
|
55
|
+
apiUrl,
|
|
56
|
+
useRecent
|
|
57
|
+
? { type: 'userFills', user }
|
|
58
|
+
: { type: 'userFillsByTime', user, startTime, endTime },
|
|
59
|
+
infoOpts
|
|
60
|
+
),
|
|
61
|
+
infoRequest<HlTwapSliceFill[]>(
|
|
62
|
+
apiUrl,
|
|
63
|
+
useRecent
|
|
64
|
+
? { type: 'userTwapSliceFills', user }
|
|
65
|
+
: { type: 'userTwapSliceFillsByTime', user, startTime, endTime },
|
|
66
|
+
infoOpts
|
|
67
|
+
),
|
|
68
|
+
])
|
|
61
69
|
useRecent = false
|
|
62
|
-
if (fills.length >=
|
|
70
|
+
if (fills.length >= FILL_ROWS_CAP || sliceFills.length >= FILL_ROWS_CAP) {
|
|
63
71
|
return { items: [], saturated: true }
|
|
64
72
|
}
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
const seen = new Set<number>()
|
|
74
|
+
const items = [...fills, ...sliceFills.map(({ fill }) => fill)].flatMap(
|
|
75
|
+
(fill): Fill[] => {
|
|
76
|
+
if (assetIsOutcome(fill.coin) || seen.has(fill.tid)) {
|
|
77
|
+
return []
|
|
78
|
+
}
|
|
79
|
+
seen.add(fill.tid)
|
|
80
|
+
const market = registry.get(fill.coin)
|
|
81
|
+
const mapped = market && mapFill(fill, market, params.address)
|
|
82
|
+
return mapped ? [mapped] : []
|
|
68
83
|
}
|
|
69
|
-
|
|
70
|
-
const mapped = market && mapFill(fill, market, params.address)
|
|
71
|
-
return mapped ? [mapped] : []
|
|
72
|
-
})
|
|
84
|
+
)
|
|
73
85
|
return { items, saturated: false }
|
|
74
86
|
},
|
|
75
87
|
(fill) => ({
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import type { Address } from 'viem'
|
|
2
|
+
import { FILL_ROWS_CAP } from '../constants.js'
|
|
2
3
|
import type { HlTwapHistoryEntry, HlTwapSliceFill } from '../types/index.js'
|
|
3
4
|
import { type InfoRequestOptions, infoRequest } from '../utils/infoClient.js'
|
|
4
5
|
import { historyPage } from './historyPagination.js'
|
|
5
6
|
|
|
6
|
-
/** Row cap of both `userTwapSliceFills` and `userTwapSliceFillsByTime`. */
|
|
7
|
-
const TWAP_SLICE_FILLS_CAP = 2000
|
|
8
|
-
|
|
9
7
|
/**
|
|
10
8
|
* Read the slice fills of each `activated` TWAP in `entries`, or none without
|
|
11
9
|
* a request when no entry is `activated`. `userTwapSliceFills` holds only the
|
|
@@ -31,7 +29,7 @@ export const fetchTwapSliceFills = async (
|
|
|
31
29
|
{ type: 'userTwapSliceFills', user },
|
|
32
30
|
options
|
|
33
31
|
)
|
|
34
|
-
if (window.length <
|
|
32
|
+
if (window.length < FILL_ROWS_CAP) {
|
|
35
33
|
return window
|
|
36
34
|
}
|
|
37
35
|
const oldest = Math.min(...window.map(({ fill }) => fill.time))
|
|
@@ -54,7 +52,7 @@ export const fetchTwapSliceFills = async (
|
|
|
54
52
|
},
|
|
55
53
|
options
|
|
56
54
|
)
|
|
57
|
-
return fills.length >=
|
|
55
|
+
return fills.length >= FILL_ROWS_CAP
|
|
58
56
|
? { items: [], saturated: true }
|
|
59
57
|
: { items: fills, saturated: false }
|
|
60
58
|
},
|
package/src/types/ws.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { DecimalString } from '@lifi/perps-types'
|
|
4
4
|
import type { HlClearinghouseState, HlSpotBalance } from './account.js'
|
|
5
5
|
import type { HlCandle, HlL2Book } from './asset.js'
|
|
6
|
-
import type { HlUserFill } from './fill.js'
|
|
6
|
+
import type { HlTwapSliceFill, HlUserFill } from './fill.js'
|
|
7
7
|
|
|
8
8
|
type HlWsNumberString = string | number
|
|
9
9
|
|
|
@@ -185,6 +185,13 @@ export type HlWsUserFillsData = {
|
|
|
185
185
|
fills: HlUserFill[]
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
/** TWAP slice fills event. A live frame can omit `isSnapshot`. @public */
|
|
189
|
+
export type HlWsUserTwapSliceFillsData = {
|
|
190
|
+
isSnapshot?: boolean
|
|
191
|
+
user: string
|
|
192
|
+
twapSliceFills: HlTwapSliceFill[]
|
|
193
|
+
}
|
|
194
|
+
|
|
188
195
|
/**
|
|
189
196
|
* One sub-dex slice of an `allDexsClearinghouseState` frame. `marginSummary`
|
|
190
197
|
* is absent on a frame that reports positions only, so a reader must treat it
|
package/src/utils/mapFill.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
LiquidityRole,
|
|
12
12
|
OrderSide,
|
|
13
13
|
} from '@lifi/perps-types'
|
|
14
|
+
import { zeroHash } from 'viem'
|
|
14
15
|
import { PROVIDER_KEY, SPOT_MARKET_ID } from '../constants.js'
|
|
15
16
|
import type { HlFillLiquidation, HlUserFill } from '../types/index.js'
|
|
16
17
|
import { rowTimestampToIsoStringOrUndefined } from './rowTimestamp.js'
|
|
@@ -95,7 +96,11 @@ export const mapFill = (
|
|
|
95
96
|
? undefined
|
|
96
97
|
: { amount: fill.builderFee, asset: feeAsset },
|
|
97
98
|
startPosition: fill.startPosition,
|
|
98
|
-
|
|
99
|
+
// A TWAP slice fill carries the zero hash: no venue transaction to link.
|
|
100
|
+
explorerLink: explorerTxUrl(
|
|
101
|
+
ExplorerChainId.HYPERLIQUID,
|
|
102
|
+
fill.hash === zeroHash ? undefined : fill.hash
|
|
103
|
+
),
|
|
99
104
|
classification:
|
|
100
105
|
market.categoryId === SPOT_MARKET_ID
|
|
101
106
|
? side === OrderSide.BUY
|
|
@@ -81,6 +81,7 @@ import type {
|
|
|
81
81
|
HlWsSpotStateData,
|
|
82
82
|
HlWsTrade,
|
|
83
83
|
HlWsUserFillsData,
|
|
84
|
+
HlWsUserTwapSliceFillsData,
|
|
84
85
|
} from '../types/index.js'
|
|
85
86
|
import { HlAbstractionMode } from '../types/index.js'
|
|
86
87
|
import {
|
|
@@ -361,37 +362,17 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
|
|
|
361
362
|
// Markets context aggregates slower asset-context feeds with fast mid/mark
|
|
362
363
|
// ticks from `fastAssetCtxs`.
|
|
363
364
|
if (sub.channel === 'marketsContext') {
|
|
364
|
-
|
|
365
|
-
const entries = this.getMarketsContextSubEntries()
|
|
366
|
-
let acquired = 0
|
|
367
|
-
const release = () => {
|
|
368
|
-
if (epoch !== this.spotWireEpoch) {
|
|
369
|
-
return
|
|
370
|
-
}
|
|
371
|
-
for (let index = 0; index < acquired; index++) {
|
|
372
|
-
const { subKey, payload } = entries[index]
|
|
373
|
-
this.unregisterSub(subKey)
|
|
374
|
-
this.rws.send(
|
|
375
|
-
JSON.stringify({ method: 'unsubscribe', subscription: payload })
|
|
376
|
-
)
|
|
377
|
-
}
|
|
365
|
+
return this.openWireEntries(this.getMarketsContextSubEntries(), () => {
|
|
378
366
|
this.perpCtxBySubDex.clear()
|
|
379
367
|
this.spotCtxByMarketId = {}
|
|
380
368
|
this.fastCtxByMarketId = {}
|
|
381
369
|
this.marketsContextByMarketId = {}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
await this.rws.ready()
|
|
390
|
-
return release
|
|
391
|
-
} catch (error) {
|
|
392
|
-
release()
|
|
393
|
-
throw error
|
|
394
|
-
}
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// `fills` merges the regular and the TWAP slice fill feeds of one address.
|
|
374
|
+
if (sub.channel === 'fills') {
|
|
375
|
+
return this.openWireEntries(this.getFillsSubEntries(sub.address))
|
|
395
376
|
}
|
|
396
377
|
|
|
397
378
|
// The spot wire is shared between the public spotBalances channel and
|
|
@@ -529,6 +510,58 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
|
|
|
529
510
|
]
|
|
530
511
|
}
|
|
531
512
|
|
|
513
|
+
/** Sub-key + payload pairs of the `fills` channel: regular and TWAP slice fills. */
|
|
514
|
+
private getFillsSubEntries(
|
|
515
|
+
address: string
|
|
516
|
+
): Array<{ subKey: string; payload: object }> {
|
|
517
|
+
const user = normalizeHlAddress(address)
|
|
518
|
+
return [
|
|
519
|
+
{ subKey: `userFills:${user}`, payload: { type: 'userFills', user } },
|
|
520
|
+
{
|
|
521
|
+
subKey: `userTwapSliceFills:${user}`,
|
|
522
|
+
payload: { type: 'userTwapSliceFills', user },
|
|
523
|
+
},
|
|
524
|
+
]
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Register each wire entry in order and return a release that unregisters
|
|
529
|
+
* the acquired ones. A failed register releases the acquired entries first.
|
|
530
|
+
* A release after `close()` sends no frame.
|
|
531
|
+
*/
|
|
532
|
+
private async openWireEntries(
|
|
533
|
+
entries: Array<{ subKey: string; payload: object }>,
|
|
534
|
+
onRelease?: () => void
|
|
535
|
+
): Promise<() => void> {
|
|
536
|
+
const epoch = this.spotWireEpoch
|
|
537
|
+
let acquired = 0
|
|
538
|
+
const release = () => {
|
|
539
|
+
if (epoch !== this.spotWireEpoch) {
|
|
540
|
+
return
|
|
541
|
+
}
|
|
542
|
+
for (let index = 0; index < acquired; index++) {
|
|
543
|
+
const { subKey, payload } = entries[index]
|
|
544
|
+
this.unregisterSub(subKey)
|
|
545
|
+
this.rws.send(
|
|
546
|
+
JSON.stringify({ method: 'unsubscribe', subscription: payload })
|
|
547
|
+
)
|
|
548
|
+
}
|
|
549
|
+
acquired = 0
|
|
550
|
+
onRelease?.()
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
for (const { subKey, payload } of entries) {
|
|
554
|
+
await this.registerSub(subKey, payload)
|
|
555
|
+
acquired++
|
|
556
|
+
}
|
|
557
|
+
await this.rws.ready()
|
|
558
|
+
return release
|
|
559
|
+
} catch (error) {
|
|
560
|
+
release()
|
|
561
|
+
throw error
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
532
565
|
private registerOrderbook(marketId: string, orderbookKey: string): void {
|
|
533
566
|
let activeKeys = this.orderbookKeysByMarketId.get(marketId)
|
|
534
567
|
if (activeKeys === undefined) {
|
|
@@ -603,7 +636,7 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
|
|
|
603
636
|
case 'orderUpdates':
|
|
604
637
|
return `orderUpdates:${sub.address.toLowerCase()}`
|
|
605
638
|
case 'fills':
|
|
606
|
-
return `
|
|
639
|
+
return `fills:${sub.address.toLowerCase()}`
|
|
607
640
|
case 'positions':
|
|
608
641
|
return `positions:${sub.address.toLowerCase()}`
|
|
609
642
|
case 'accountSummary':
|
|
@@ -683,6 +716,8 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
|
|
|
683
716
|
case 'orderUpdates':
|
|
684
717
|
return { type: 'orderUpdates', user: normalizeHlAddress(sub.address) }
|
|
685
718
|
case 'fills':
|
|
719
|
+
// Handled via getFillsSubEntries in openChannel; never reaches
|
|
720
|
+
// toHlPayload, but TS requires an exhaustive switch.
|
|
686
721
|
return { type: 'userFills', user: normalizeHlAddress(sub.address) }
|
|
687
722
|
case 'positions':
|
|
688
723
|
case 'accountSummary':
|
|
@@ -765,6 +800,9 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
|
|
|
765
800
|
case 'userFills':
|
|
766
801
|
this.handleUserFills(msg.data as HlWsUserFillsData)
|
|
767
802
|
break
|
|
803
|
+
case 'userTwapSliceFills':
|
|
804
|
+
this.handleUserTwapSliceFills(msg.data as HlWsUserTwapSliceFillsData)
|
|
805
|
+
break
|
|
768
806
|
case 'allDexsClearinghouseState':
|
|
769
807
|
this.handleAllDexsClearinghouseState(
|
|
770
808
|
msg.data as HlWsAllDexsClearinghouseStateData
|
|
@@ -1231,22 +1269,38 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
|
|
|
1231
1269
|
}
|
|
1232
1270
|
|
|
1233
1271
|
private handleUserFills(data: HlWsUserFillsData) {
|
|
1272
|
+
this.emitFills(data.user, data.isSnapshot, data.fills)
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
private handleUserTwapSliceFills(data: HlWsUserTwapSliceFillsData) {
|
|
1276
|
+
this.emitFills(
|
|
1277
|
+
data.user,
|
|
1278
|
+
data.isSnapshot,
|
|
1279
|
+
data.twapSliceFills.map(({ fill }) => fill)
|
|
1280
|
+
)
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
private emitFills(
|
|
1284
|
+
user: string,
|
|
1285
|
+
isSnapshot: boolean | undefined,
|
|
1286
|
+
fills: HlUserFill[]
|
|
1287
|
+
) {
|
|
1234
1288
|
// The snapshot frame carries up to ~2000 historical fills and is replayed
|
|
1235
1289
|
// on every reconnect via `replaySubs`; emitting them would surface old
|
|
1236
1290
|
// fills as live events on each network blip. Suppress it to an empty event
|
|
1237
1291
|
// (fill history is available via REST `getFills`) so only live fills are
|
|
1238
1292
|
// delivered as events.
|
|
1239
|
-
const items =
|
|
1293
|
+
const items = isSnapshot
|
|
1240
1294
|
? []
|
|
1241
|
-
:
|
|
1295
|
+
: fills.flatMap((f) => {
|
|
1242
1296
|
if (assetIsOutcome(f.coin)) {
|
|
1243
1297
|
return []
|
|
1244
1298
|
}
|
|
1245
1299
|
const market = this.registry?.get(f.coin)
|
|
1246
|
-
const mapped = market && mapFill(f
|
|
1300
|
+
const mapped = market && mapFill(f, market, user)
|
|
1247
1301
|
return mapped ? [mapped] : []
|
|
1248
1302
|
})
|
|
1249
|
-
this.emit(`
|
|
1303
|
+
this.emit(`fills:${user.toLowerCase()}`, {
|
|
1250
1304
|
channel: 'fills',
|
|
1251
1305
|
data: items,
|
|
1252
1306
|
})
|
|
@@ -1924,6 +1978,8 @@ function isValidHlFrame(channel: string, data: unknown): boolean {
|
|
|
1924
1978
|
return Array.isArray(data)
|
|
1925
1979
|
case 'userFills':
|
|
1926
1980
|
return typeof data.user === 'string' && Array.isArray(data.fills)
|
|
1981
|
+
case 'userTwapSliceFills':
|
|
1982
|
+
return typeof data.user === 'string' && Array.isArray(data.twapSliceFills)
|
|
1927
1983
|
case 'allDexsClearinghouseState':
|
|
1928
1984
|
return (
|
|
1929
1985
|
typeof data.user === 'string' && Array.isArray(data.clearinghouseStates)
|