@lifi/perps-sdk-provider-hyperliquid 19.0.0 → 20.1.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.
- package/README.md +56 -0
- package/dist/cjs/HyperliquidProvider.js.map +1 -1
- package/dist/cjs/services/getAccount.d.ts.map +1 -1
- package/dist/cjs/services/getAccount.js +12 -8
- package/dist/cjs/services/getAccount.js.map +1 -1
- package/dist/cjs/services/getActivity.d.ts.map +1 -1
- package/dist/cjs/services/getActivity.js +71 -45
- package/dist/cjs/services/getActivity.js.map +1 -1
- package/dist/cjs/services/getFills.d.ts.map +1 -1
- package/dist/cjs/services/getFills.js +30 -38
- package/dist/cjs/services/getFills.js.map +1 -1
- package/dist/cjs/services/getMarketSettings.d.ts +2 -2
- package/dist/cjs/services/getMarketSettings.d.ts.map +1 -1
- package/dist/cjs/services/getMarketSettings.js +8 -3
- package/dist/cjs/services/getMarketSettings.js.map +1 -1
- package/dist/cjs/services/historyPagination.d.ts +22 -0
- package/dist/cjs/services/historyPagination.d.ts.map +1 -0
- package/dist/cjs/services/historyPagination.js +125 -0
- package/dist/cjs/services/historyPagination.js.map +1 -0
- package/dist/cjs/types/ledger.d.ts +17 -1
- package/dist/cjs/types/ledger.d.ts.map +1 -1
- package/dist/cjs/types/ledger.js +10 -1
- package/dist/cjs/types/ledger.js.map +1 -1
- package/dist/cjs/utils/mapActivity.d.ts.map +1 -1
- package/dist/cjs/utils/mapActivity.js +41 -0
- package/dist/cjs/utils/mapActivity.js.map +1 -1
- package/dist/cjs/utils/withdrawableBalances.d.ts.map +1 -1
- package/dist/cjs/utils/withdrawableBalances.js +3 -1
- package/dist/cjs/utils/withdrawableBalances.js.map +1 -1
- package/dist/cjs/websocket/HyperliquidWsProvider.d.ts +6 -0
- package/dist/cjs/websocket/HyperliquidWsProvider.d.ts.map +1 -1
- package/dist/cjs/websocket/HyperliquidWsProvider.js +129 -37
- package/dist/cjs/websocket/HyperliquidWsProvider.js.map +1 -1
- package/dist/esm/HyperliquidProvider.js.map +1 -1
- package/dist/esm/services/getAccount.d.ts.map +1 -1
- package/dist/esm/services/getAccount.js +10 -9
- package/dist/esm/services/getAccount.js.map +1 -1
- package/dist/esm/services/getActivity.d.ts +3 -13
- package/dist/esm/services/getActivity.d.ts.map +1 -1
- package/dist/esm/services/getActivity.js +78 -66
- package/dist/esm/services/getActivity.js.map +1 -1
- package/dist/esm/services/getFills.d.ts +2 -14
- package/dist/esm/services/getFills.d.ts.map +1 -1
- package/dist/esm/services/getFills.js +32 -58
- package/dist/esm/services/getFills.js.map +1 -1
- package/dist/esm/services/getMarketSettings.d.ts +5 -3
- package/dist/esm/services/getMarketSettings.d.ts.map +1 -1
- package/dist/esm/services/getMarketSettings.js +13 -7
- package/dist/esm/services/getMarketSettings.js.map +1 -1
- package/dist/esm/services/historyPagination.d.ts +22 -0
- package/dist/esm/services/historyPagination.d.ts.map +1 -0
- package/dist/esm/services/historyPagination.js +121 -0
- package/dist/esm/services/historyPagination.js.map +1 -0
- package/dist/esm/types/ledger.d.ts +42 -1
- package/dist/esm/types/ledger.d.ts.map +1 -1
- package/dist/esm/types/ledger.js +17 -0
- package/dist/esm/types/ledger.js.map +1 -1
- package/dist/esm/utils/mapActivity.d.ts +1 -0
- package/dist/esm/utils/mapActivity.d.ts.map +1 -1
- package/dist/esm/utils/mapActivity.js +49 -1
- package/dist/esm/utils/mapActivity.js.map +1 -1
- package/dist/esm/utils/withdrawableBalances.d.ts +2 -1
- package/dist/esm/utils/withdrawableBalances.d.ts.map +1 -1
- package/dist/esm/utils/withdrawableBalances.js +5 -2
- package/dist/esm/utils/withdrawableBalances.js.map +1 -1
- package/dist/esm/websocket/HyperliquidWsProvider.d.ts +6 -0
- package/dist/esm/websocket/HyperliquidWsProvider.d.ts.map +1 -1
- package/dist/esm/websocket/HyperliquidWsProvider.js +131 -49
- package/dist/esm/websocket/HyperliquidWsProvider.js.map +1 -1
- package/dist/types/services/getAccount.d.ts.map +1 -1
- package/dist/types/services/getActivity.d.ts +3 -13
- package/dist/types/services/getActivity.d.ts.map +1 -1
- package/dist/types/services/getFills.d.ts +2 -14
- package/dist/types/services/getFills.d.ts.map +1 -1
- package/dist/types/services/getMarketSettings.d.ts +5 -3
- package/dist/types/services/getMarketSettings.d.ts.map +1 -1
- package/dist/types/services/historyPagination.d.ts +22 -0
- package/dist/types/services/historyPagination.d.ts.map +1 -0
- package/dist/types/types/ledger.d.ts +42 -1
- package/dist/types/types/ledger.d.ts.map +1 -1
- package/dist/types/utils/mapActivity.d.ts +1 -0
- package/dist/types/utils/mapActivity.d.ts.map +1 -1
- package/dist/types/utils/withdrawableBalances.d.ts +2 -1
- package/dist/types/utils/withdrawableBalances.d.ts.map +1 -1
- package/dist/types/websocket/HyperliquidWsProvider.d.ts +6 -0
- package/dist/types/websocket/HyperliquidWsProvider.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/HyperliquidProvider.ts +1 -1
- package/src/services/getAccount.ts +22 -12
- package/src/services/getActivity.ts +138 -105
- package/src/services/getFills.ts +42 -95
- package/src/services/getMarketSettings.ts +20 -8
- package/src/services/historyPagination.ts +169 -0
- package/src/types/ledger.ts +61 -0
- package/src/utils/mapActivity.ts +56 -0
- package/src/utils/withdrawableBalances.ts +8 -3
- package/src/websocket/HyperliquidWsProvider.ts +141 -63
package/src/services/getFills.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
params.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { PerpsError, type SDKRequestOptions } from '@lifi/perps-sdk'
|
|
2
2
|
import {
|
|
3
3
|
MarginMode,
|
|
4
4
|
type MarketRef,
|
|
5
5
|
type MarketSettings,
|
|
6
|
+
PerpsErrorCode,
|
|
6
7
|
} from '@lifi/perps-types'
|
|
7
8
|
import type { Address } from 'viem'
|
|
8
|
-
import { SPOT_MARKET_ID } from '../constants.js'
|
|
9
|
+
import { PROVIDER_KEY, SPOT_MARKET_ID } from '../constants.js'
|
|
9
10
|
import type { HyperliquidContext } from '../context.js'
|
|
10
11
|
import { fetchActiveAssetData } from './activeAssetData.js'
|
|
11
12
|
|
|
@@ -22,17 +23,23 @@ export interface GetMarketSettingsParams {
|
|
|
22
23
|
/**
|
|
23
24
|
* The user's venue-stored margin mode and leverage for one perps market,
|
|
24
25
|
* read from `activeAssetData` — present whether or not a position is open.
|
|
25
|
-
* @throws {PerpsError}
|
|
26
|
+
* @throws {PerpsError} `ValidationError` for a spot market; `SDKError` when
|
|
27
|
+
* the response carries no positive leverage; any other `PerpsError` on
|
|
28
|
+
* Hyperliquid REST error, network, or parsing failures.
|
|
26
29
|
* @public
|
|
27
30
|
*/
|
|
28
31
|
export const getMarketSettings = async (
|
|
29
32
|
context: HyperliquidContext,
|
|
30
33
|
params: GetMarketSettingsParams,
|
|
31
34
|
options?: SDKRequestOptions
|
|
32
|
-
): Promise<MarketSettings
|
|
33
|
-
// Spot markets carry no leverage state on the venue.
|
|
35
|
+
): Promise<MarketSettings> => {
|
|
34
36
|
if (params.market.categoryId === SPOT_MARKET_ID) {
|
|
35
|
-
|
|
37
|
+
const error = new PerpsError(
|
|
38
|
+
PerpsErrorCode.ValidationError,
|
|
39
|
+
`Hyperliquid market '${params.market.marketId}' is a spot market and carries no leverage setting`
|
|
40
|
+
)
|
|
41
|
+
error.tool = PROVIDER_KEY
|
|
42
|
+
throw error
|
|
36
43
|
}
|
|
37
44
|
const data = await fetchActiveAssetData(
|
|
38
45
|
context,
|
|
@@ -41,8 +48,13 @@ export const getMarketSettings = async (
|
|
|
41
48
|
options
|
|
42
49
|
)
|
|
43
50
|
const leverage = data.leverage
|
|
44
|
-
if (!leverage) {
|
|
45
|
-
|
|
51
|
+
if (!leverage || !(leverage.value > 0)) {
|
|
52
|
+
const error = new PerpsError(
|
|
53
|
+
PerpsErrorCode.SDKError,
|
|
54
|
+
`Hyperliquid returned no leverage for market '${params.market.marketId}'`
|
|
55
|
+
)
|
|
56
|
+
error.tool = PROVIDER_KEY
|
|
57
|
+
throw error
|
|
46
58
|
}
|
|
47
59
|
return {
|
|
48
60
|
marginMode:
|
|
@@ -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
|
+
}
|
package/src/types/ledger.ts
CHANGED
|
@@ -130,6 +130,38 @@ export type HlLiquidationDelta = {
|
|
|
130
130
|
liquidatedPositions?: HlLiquidatedPosition[]
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Hyperliquid `cStakingTransfer` ledger delta. Moves `token` between the
|
|
135
|
+
* account's spot balance and its staking balance; `isDeposit` is `true` for a
|
|
136
|
+
* move into staking. `amount` is unsigned. A completed unstake row carries the
|
|
137
|
+
* zero hash.
|
|
138
|
+
* @public
|
|
139
|
+
*/
|
|
140
|
+
export type HlCStakingTransferDelta = {
|
|
141
|
+
type: 'cStakingTransfer'
|
|
142
|
+
token: string
|
|
143
|
+
amount: string
|
|
144
|
+
isDeposit: boolean
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Borrow-lend market action named by a `borrowLend` ledger delta. @public */
|
|
148
|
+
export type HlBorrowLendOperation = 'supply' | 'withdraw' | 'borrow' | 'repay'
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Hyperliquid `borrowLend` ledger delta. Moves `token` between the account's
|
|
152
|
+
* spot balance and the borrow-lend market; `operation` gives the direction and
|
|
153
|
+
* `amount` is unsigned. `interestAmount` is the accrued interest that
|
|
154
|
+
* `amount` includes on a `withdraw` or `repay`, and `"0.0"` otherwise.
|
|
155
|
+
* @public
|
|
156
|
+
*/
|
|
157
|
+
export type HlBorrowLendDelta = {
|
|
158
|
+
type: 'borrowLend'
|
|
159
|
+
token: string
|
|
160
|
+
operation: HlBorrowLendOperation
|
|
161
|
+
amount: string
|
|
162
|
+
interestAmount: string
|
|
163
|
+
}
|
|
164
|
+
|
|
133
165
|
/**
|
|
134
166
|
* Union of known Hyperliquid non-funding ledger deltas plus an open fallback
|
|
135
167
|
* for endpoint variants the provider does not map.
|
|
@@ -143,6 +175,8 @@ export type HlLedgerDelta =
|
|
|
143
175
|
| HlDepositDelta
|
|
144
176
|
| HlWithdrawDelta
|
|
145
177
|
| HlLiquidationDelta
|
|
178
|
+
| HlCStakingTransferDelta
|
|
179
|
+
| HlBorrowLendDelta
|
|
146
180
|
| {
|
|
147
181
|
type: string
|
|
148
182
|
usdc?: string
|
|
@@ -202,6 +236,33 @@ export const isLiquidationDelta = (
|
|
|
202
236
|
delta: HlLedgerDelta
|
|
203
237
|
): delta is HlLiquidationDelta => delta.type === 'liquidation'
|
|
204
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Type guard for `HlCStakingTransferDelta`. `isDeposit` is part of the guard:
|
|
241
|
+
* a row without a boolean `isDeposit` names no direction for the movement.
|
|
242
|
+
* @public
|
|
243
|
+
*/
|
|
244
|
+
export const isCStakingTransferDelta = (
|
|
245
|
+
delta: HlLedgerDelta
|
|
246
|
+
): delta is HlCStakingTransferDelta =>
|
|
247
|
+
delta.type === 'cStakingTransfer' &&
|
|
248
|
+
'isDeposit' in delta &&
|
|
249
|
+
typeof delta.isDeposit === 'boolean'
|
|
250
|
+
|
|
251
|
+
const HL_BORROW_LEND_OPERATIONS: ReadonlySet<unknown> =
|
|
252
|
+
new Set<HlBorrowLendOperation>(['supply', 'withdraw', 'borrow', 'repay'])
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Type guard for `HlBorrowLendDelta`. The operation is part of the guard: an
|
|
256
|
+
* unknown operation names no direction for the movement.
|
|
257
|
+
* @public
|
|
258
|
+
*/
|
|
259
|
+
export const isBorrowLendDelta = (
|
|
260
|
+
delta: HlLedgerDelta
|
|
261
|
+
): delta is HlBorrowLendDelta =>
|
|
262
|
+
delta.type === 'borrowLend' &&
|
|
263
|
+
'operation' in delta &&
|
|
264
|
+
HL_BORROW_LEND_OPERATIONS.has(delta.operation)
|
|
265
|
+
|
|
205
266
|
/**
|
|
206
267
|
* One timestamped non-funding ledger update. `time` is milliseconds since
|
|
207
268
|
* epoch and `hash` is the upstream transaction identifier.
|
package/src/utils/mapActivity.ts
CHANGED
|
@@ -17,13 +17,17 @@ import type {
|
|
|
17
17
|
} from '@lifi/perps-types'
|
|
18
18
|
import { ActivityType, PerpsErrorCode } from '@lifi/perps-types'
|
|
19
19
|
import Big from 'big.js'
|
|
20
|
+
import { zeroHash } from 'viem'
|
|
20
21
|
import type {
|
|
22
|
+
HlBorrowLendOperation,
|
|
21
23
|
HlFundingUpdate,
|
|
22
24
|
HlLedgerUpdate,
|
|
23
25
|
HlUserFill,
|
|
24
26
|
} from '../types/index.js'
|
|
25
27
|
import {
|
|
28
|
+
isBorrowLendDelta,
|
|
26
29
|
isCollateralTransferDelta,
|
|
30
|
+
isCStakingTransferDelta,
|
|
27
31
|
isDepositDelta,
|
|
28
32
|
isLiquidationDelta,
|
|
29
33
|
isSendAssetDelta,
|
|
@@ -47,6 +51,17 @@ const HL_NATIVE_TOKEN_SYMBOL = 'HYPE'
|
|
|
47
51
|
// Hyperliquid reserves spot token index 0 for USDC.
|
|
48
52
|
const HL_COLLATERAL_ASSET_ID = '0'
|
|
49
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Direction relative to the account's spot balance, the same frame as an
|
|
56
|
+
* outbound `vaultDeposit`.
|
|
57
|
+
*/
|
|
58
|
+
const BORROW_LEND_DIRECTION: Record<HlBorrowLendOperation, 'IN' | 'OUT'> = {
|
|
59
|
+
supply: 'OUT',
|
|
60
|
+
withdraw: 'IN',
|
|
61
|
+
borrow: 'IN',
|
|
62
|
+
repay: 'OUT',
|
|
63
|
+
}
|
|
64
|
+
|
|
50
65
|
/**
|
|
51
66
|
* A ledger row moves a spot asset — a perp contract cannot be sent, received
|
|
52
67
|
* or withdrawn — so the delta's token symbol resolves inside the spot asset
|
|
@@ -69,6 +84,7 @@ const resolveLedgerAsset = (symbol: string, registry: AssetRegistry): Asset => {
|
|
|
69
84
|
|
|
70
85
|
/**
|
|
71
86
|
* Map supported ledger entries; exclude same-account moves and unsupported types.
|
|
87
|
+
* Staking and borrow-lend moves map with the queried account as counterparty.
|
|
72
88
|
* Missing assets throw; unresolved liquidation markets omit only those positions.
|
|
73
89
|
* @public
|
|
74
90
|
*/
|
|
@@ -143,6 +159,46 @@ export const mapLedgerEntry = (
|
|
|
143
159
|
} satisfies TransferActivity
|
|
144
160
|
}
|
|
145
161
|
|
|
162
|
+
if (isCStakingTransferDelta(delta) || isBorrowLendDelta(delta)) {
|
|
163
|
+
// A completed unstake is a system event with the zero hash and no venue
|
|
164
|
+
// transaction, so the id is synthesized and no explorer link applies.
|
|
165
|
+
const hasTransaction = entry.hash !== zeroHash
|
|
166
|
+
const direction = isCStakingTransferDelta(delta)
|
|
167
|
+
? delta.isDeposit
|
|
168
|
+
? 'OUT'
|
|
169
|
+
: 'IN'
|
|
170
|
+
: BORROW_LEND_DIRECTION[delta.operation]
|
|
171
|
+
const action = isCStakingTransferDelta(delta) ? direction : delta.operation
|
|
172
|
+
return {
|
|
173
|
+
...base,
|
|
174
|
+
...(hasTransaction
|
|
175
|
+
? {}
|
|
176
|
+
: {
|
|
177
|
+
id: `${delta.type}:${delta.token}:${action}:${delta.amount}:${base.timestamp}`,
|
|
178
|
+
}),
|
|
179
|
+
type: ActivityType.TRANSFER,
|
|
180
|
+
direction,
|
|
181
|
+
counterpartyAddress: queriedAddress.toLowerCase(),
|
|
182
|
+
asset: resolveLedgerAsset(delta.token, assetRegistry),
|
|
183
|
+
amount: delta.amount,
|
|
184
|
+
meta: isCStakingTransferDelta(delta)
|
|
185
|
+
? { transferType: delta.type }
|
|
186
|
+
: {
|
|
187
|
+
transferType: delta.type,
|
|
188
|
+
operation: delta.operation,
|
|
189
|
+
interestAmount: delta.interestAmount,
|
|
190
|
+
},
|
|
191
|
+
...(hasTransaction
|
|
192
|
+
? {
|
|
193
|
+
explorerLink: explorerTxUrl(
|
|
194
|
+
ExplorerChainId.HYPERLIQUID,
|
|
195
|
+
entry.hash
|
|
196
|
+
),
|
|
197
|
+
}
|
|
198
|
+
: {}),
|
|
199
|
+
} satisfies TransferActivity
|
|
200
|
+
}
|
|
201
|
+
|
|
146
202
|
if (isVaultTransferDelta(delta)) {
|
|
147
203
|
const queried = queriedAddress.toLowerCase()
|
|
148
204
|
const vault = delta.vault.toLowerCase()
|
|
@@ -17,7 +17,8 @@ import { toWireBig } from './decimal.js'
|
|
|
17
17
|
*
|
|
18
18
|
* @param quoteAssetId - `Asset.id` the perps-route row is keyed by.
|
|
19
19
|
* @param withdrawalFee - Flat venue fee in quote-asset units, set on every
|
|
20
|
-
* quote-asset row.
|
|
20
|
+
* quote-asset row. Hyperliquid deducts it from the requested amount. Absent
|
|
21
|
+
* leaves every row without a fee.
|
|
21
22
|
* @public
|
|
22
23
|
*/
|
|
23
24
|
export const hyperliquidWithdrawableBalances = (
|
|
@@ -32,8 +33,12 @@ export const hyperliquidWithdrawableBalances = (
|
|
|
32
33
|
withdrawalFee === undefined
|
|
33
34
|
? undefined
|
|
34
35
|
: toWireBig(withdrawalFee, 'providers.withdrawalFeeUsd').toFixed()
|
|
35
|
-
const feeFor = (
|
|
36
|
-
|
|
36
|
+
const feeFor = (
|
|
37
|
+
assetId: string
|
|
38
|
+
): Pick<ProviderWithdrawableBalance, 'withdrawalFee' | 'isFeeDeducted'> =>
|
|
39
|
+
fee === undefined || assetId !== quoteAssetId
|
|
40
|
+
? {}
|
|
41
|
+
: { withdrawalFee: fee, isFeeDeducted: true }
|
|
37
42
|
|
|
38
43
|
if (isUnifiedAbstraction(abstraction)) {
|
|
39
44
|
for (const balance of spotState.balances) {
|