@lifi/perps-sdk-provider-hyperliquid 20.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.
Files changed (44) hide show
  1. package/README.md +45 -0
  2. package/dist/cjs/services/getActivity.d.ts.map +1 -1
  3. package/dist/cjs/services/getActivity.js +68 -44
  4. package/dist/cjs/services/getActivity.js.map +1 -1
  5. package/dist/cjs/services/getFills.d.ts.map +1 -1
  6. package/dist/cjs/services/getFills.js +30 -38
  7. package/dist/cjs/services/getFills.js.map +1 -1
  8. package/dist/cjs/services/historyPagination.d.ts +22 -0
  9. package/dist/cjs/services/historyPagination.d.ts.map +1 -0
  10. package/dist/cjs/services/historyPagination.js +125 -0
  11. package/dist/cjs/services/historyPagination.js.map +1 -0
  12. package/dist/cjs/websocket/HyperliquidWsProvider.d.ts +6 -0
  13. package/dist/cjs/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  14. package/dist/cjs/websocket/HyperliquidWsProvider.js +129 -37
  15. package/dist/cjs/websocket/HyperliquidWsProvider.js.map +1 -1
  16. package/dist/esm/services/getActivity.d.ts +3 -13
  17. package/dist/esm/services/getActivity.d.ts.map +1 -1
  18. package/dist/esm/services/getActivity.js +74 -64
  19. package/dist/esm/services/getActivity.js.map +1 -1
  20. package/dist/esm/services/getFills.d.ts +2 -14
  21. package/dist/esm/services/getFills.d.ts.map +1 -1
  22. package/dist/esm/services/getFills.js +32 -58
  23. package/dist/esm/services/getFills.js.map +1 -1
  24. package/dist/esm/services/historyPagination.d.ts +22 -0
  25. package/dist/esm/services/historyPagination.d.ts.map +1 -0
  26. package/dist/esm/services/historyPagination.js +121 -0
  27. package/dist/esm/services/historyPagination.js.map +1 -0
  28. package/dist/esm/websocket/HyperliquidWsProvider.d.ts +6 -0
  29. package/dist/esm/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  30. package/dist/esm/websocket/HyperliquidWsProvider.js +131 -49
  31. package/dist/esm/websocket/HyperliquidWsProvider.js.map +1 -1
  32. package/dist/types/services/getActivity.d.ts +3 -13
  33. package/dist/types/services/getActivity.d.ts.map +1 -1
  34. package/dist/types/services/getFills.d.ts +2 -14
  35. package/dist/types/services/getFills.d.ts.map +1 -1
  36. package/dist/types/services/historyPagination.d.ts +22 -0
  37. package/dist/types/services/historyPagination.d.ts.map +1 -0
  38. package/dist/types/websocket/HyperliquidWsProvider.d.ts +6 -0
  39. package/dist/types/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  40. package/package.json +2 -2
  41. package/src/services/getActivity.ts +133 -104
  42. package/src/services/getFills.ts +42 -95
  43. package/src/services/historyPagination.ts +169 -0
  44. package/src/websocket/HyperliquidWsProvider.ts +141 -63
@@ -32,7 +32,12 @@ export declare class HyperliquidWsProvider extends WsProviderBase<object> {
32
32
  private readonly client;
33
33
  private readonly registry;
34
34
  private readonly abstractionByUser;
35
+ private readonly summaryUsers;
36
+ private readonly abstractionReadsByUser;
35
37
  private readonly spotRefs;
38
+ private spotWireEpoch;
39
+ private readonly latestSpotByUser;
40
+ private readonly revalueSpotBalances;
36
41
  private readonly unifiedSummaryByUser;
37
42
  private readonly latestPerpsByUser;
38
43
  private readonly heldSummaryByUser;
@@ -136,6 +141,7 @@ export declare class HyperliquidWsProvider extends WsProviderBase<object> {
136
141
  private fetchAbstractionMode;
137
142
  private handleActiveAssetData;
138
143
  private handleSpotState;
144
+ private emitSpotBalances;
139
145
  /** Read the venue buying power over REST when the spot frame omits it. */
140
146
  private fetchAvailableAfterMaintenance;
141
147
  }
@@ -1 +1 @@
1
- {"version":3,"file":"HyperliquidWsProvider.d.ts","sourceRoot":"","sources":["../../../src/websocket/HyperliquidWsProvider.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"HyperliquidWsProvider.d.ts","sourceRoot":"","sources":["../../../src/websocket/HyperliquidWsProvider.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifi/perps-sdk-provider-hyperliquid",
3
- "version": "20.0.0",
3
+ "version": "20.1.0",
4
4
  "description": "Hyperliquid provider plugin for @lifi/perps-sdk",
5
5
  "homepage": "https://github.com/lifinance/perps-sdk",
6
6
  "bugs": {
@@ -52,7 +52,7 @@
52
52
  "@lifi/perps-types": "^18.0.0"
53
53
  },
54
54
  "peerDependencies": {
55
- "@lifi/perps-sdk": "^20.0.0"
55
+ "@lifi/perps-sdk": "^20.1.0"
56
56
  },
57
57
  "publishConfig": {
58
58
  "access": "public"
@@ -2,6 +2,7 @@ import {
2
2
  type AssetRegistry,
3
3
  getAssetRegistry,
4
4
  getMarketRegistry,
5
+ PerpsError,
5
6
  type SDKRequestOptions,
6
7
  } from '@lifi/perps-sdk'
7
8
  import type {
@@ -10,13 +11,9 @@ import type {
10
11
  FundingActivity,
11
12
  MarketDisplay,
12
13
  } from '@lifi/perps-types'
13
- import { ActivityType } from '@lifi/perps-types'
14
+ import { ActivityType, PerpsErrorCode } from '@lifi/perps-types'
14
15
  import type { Address } from 'viem'
15
- import {
16
- DEFAULT_HISTORY_LIMIT,
17
- MAX_HISTORY_LIMIT,
18
- PROVIDER_KEY,
19
- } from '../constants.js'
16
+ import { PROVIDER_KEY } from '../constants.js'
20
17
  import type { HyperliquidContext } from '../context.js'
21
18
  import type {
22
19
  HlUserFills,
@@ -44,6 +41,7 @@ import {
44
41
  type InfoRequestOptions,
45
42
  infoRequest,
46
43
  } from '../utils/infoClient.js'
44
+ import { getHistoryLimit, historyPage } from './historyPagination.js'
47
45
 
48
46
  /**
49
47
  * Parameters for {@link getActivity}.
@@ -52,9 +50,9 @@ import {
52
50
  */
53
51
  export interface GetActivityParams {
54
52
  address: Address
55
- /** Maximum items returned; defaults to 50 and is capped at 200. */
53
+ /** Positive safe integer; defaults to 50 and is capped at 200. */
56
54
  limit?: number
57
- /** Millisecond timestamp cursor; rows strictly older than it are returned. */
55
+ /** Opaque cursor returned in the previous page's `pagination.cursor`. */
58
56
  cursor?: string
59
57
  /** Inclusive lower bound in milliseconds since epoch. */
60
58
  startTime?: number
@@ -78,23 +76,100 @@ const ASSET_BEARING_TYPES: ReadonlySet<ActivityType> = new Set([
78
76
  const needsMarkets = (typeFilter: ActivityType[] | undefined): boolean =>
79
77
  !typeFilter || typeFilter.some((t) => MARKET_BEARING_TYPES.has(t))
80
78
 
79
+ const fetchLiquidationHistory = async (
80
+ apiUrl: string,
81
+ user: Address,
82
+ startTime: number,
83
+ endTime: number,
84
+ resolveMarket: (coin: string) => MarketDisplay | undefined,
85
+ options?: InfoRequestOptions
86
+ ): Promise<ActivityItem[]> => {
87
+ const retained = new Map<number, HlUserFills[number]>()
88
+ while (startTime <= endTime) {
89
+ options?.signal?.throwIfAborted()
90
+ const fills = await infoRequest<HlUserFills>(
91
+ apiUrl,
92
+ { type: 'userFillsByTime', user, startTime, endTime },
93
+ options
94
+ )
95
+ options?.signal?.throwIfAborted()
96
+ let latest = startTime
97
+ for (const fill of fills) {
98
+ retained.set(fill.tid, fill)
99
+ latest = Math.max(latest, fill.time)
100
+ }
101
+ if (fills.length < 2000 || retained.size >= 10000) {
102
+ break
103
+ }
104
+ if (latest === startTime) {
105
+ throw new PerpsError(
106
+ PerpsErrorCode.ThirdPartyError,
107
+ 'Hyperliquid fills saturate a single millisecond; liquidation history cannot be paginated safely'
108
+ )
109
+ }
110
+ // Inclusive overlap retains all same-time fills at the venue page boundary.
111
+ startTime = latest
112
+ }
113
+ const queried = user.toLowerCase()
114
+ const fills = [...retained.values()].filter(
115
+ (fill) => fill.liquidation?.liquidatedUser.toLowerCase() === queried
116
+ )
117
+ if (fills.length === 0) {
118
+ return []
119
+ }
120
+ const hashes = new Set<string>()
121
+ let firstTime = Infinity
122
+ let lastTime = 0
123
+ for (const fill of fills) {
124
+ if (fill.hash !== undefined) {
125
+ hashes.add(fill.hash)
126
+ }
127
+ firstTime = Math.min(firstTime, fill.time)
128
+ lastTime = Math.max(lastTime, fill.time)
129
+ }
130
+ // Exclusions must cover the entire order, not the current SDK page's ledger window.
131
+ const ledger = await historyPage(
132
+ { startTime: firstTime, endTime: lastTime },
133
+ Number.MAX_SAFE_INTEGER,
134
+ async (startTime, endTime) => {
135
+ const updates = await infoRequest<HlUserNonFundingLedgerUpdates>(
136
+ apiUrl,
137
+ { type: 'userNonFundingLedgerUpdates', user, startTime, endTime },
138
+ options
139
+ )
140
+ return {
141
+ items: updates.filter(
142
+ (entry) => isLiquidationDelta(entry.delta) && hashes.has(entry.hash)
143
+ ),
144
+ saturated: updates.length >= 500,
145
+ }
146
+ },
147
+ (entry) => ({ time: entry.time, key: entry.hash }),
148
+ options?.signal
149
+ )
150
+ return mapLiquidationFills(
151
+ fills,
152
+ PROVIDER_KEY,
153
+ user,
154
+ resolveMarket,
155
+ new Set(ledger.items.map((entry) => entry.hash))
156
+ )
157
+ }
158
+
81
159
  const fetchActivityData = async (
82
160
  apiUrl: string,
83
161
  typeFilter: ActivityType[] | undefined,
84
- timeParams: { user: Address; startTime?: number; endTime?: number },
162
+ timeParams: { user: Address; startTime: number; endTime: number },
85
163
  assetRegistry: AssetRegistry,
86
164
  resolveMarket: (coin: string) => MarketDisplay | undefined,
165
+ liquidationHistory: Promise<ActivityItem[]> | undefined,
87
166
  options?: InfoRequestOptions
88
- ): Promise<ActivityItem[]> => {
167
+ ): Promise<{ items: ActivityItem[]; saturated: boolean }> => {
89
168
  const needLedger =
90
169
  !typeFilter || typeFilter.some((t) => t !== ActivityType.FUNDING)
91
170
  const needFunding = !typeFilter || typeFilter.includes(ActivityType.FUNDING)
92
- // A liquidation executed as a market order reaches the account as fills
93
- // with a `liquidation` field and never as a ledger `liquidation` delta.
94
- const needLiquidationFills =
95
- !typeFilter || typeFilter.includes(ActivityType.LIQUIDATION)
96
171
 
97
- const [ledgerUpdates, fundingUpdates, fills] = await Promise.all([
172
+ const [ledgerUpdates, fundingUpdates, liquidations] = await Promise.all([
98
173
  needLedger
99
174
  ? infoRequest<HlUserNonFundingLedgerUpdates>(
100
175
  apiUrl,
@@ -109,16 +184,11 @@ const fetchActivityData = async (
109
184
  options
110
185
  )
111
186
  : Promise.resolve([] as HlUserFunding),
112
- needLiquidationFills
113
- ? infoRequest<HlUserFills>(
114
- apiUrl,
115
- timeParams.startTime === undefined
116
- ? { type: 'userFills', user: timeParams.user }
117
- : { type: 'userFillsByTime', ...timeParams },
118
- options
119
- )
120
- : Promise.resolve([] as HlUserFills),
187
+ liquidationHistory,
121
188
  ])
189
+ if (ledgerUpdates.length >= 500 || fundingUpdates.length >= 500) {
190
+ return { items: [], saturated: true }
191
+ }
122
192
 
123
193
  const ledgerItems: ActivityItem[] = ledgerUpdates.flatMap(
124
194
  (entry): ActivityItem[] => {
@@ -156,46 +226,20 @@ const fetchActivityData = async (
156
226
  }
157
227
  )
158
228
 
159
- const ledgerLiquidationHashes = new Set(
160
- ledgerUpdates
161
- .filter((entry) => isLiquidationDelta(entry.delta))
162
- .map((entry) => entry.hash)
163
- )
164
- const liquidationItems = mapLiquidationFills(
165
- fills,
166
- PROVIDER_KEY,
167
- timeParams.user,
168
- resolveMarket,
169
- ledgerLiquidationHashes
170
- )
171
-
172
- const merged = [...ledgerItems, ...fundingItems, ...liquidationItems].sort(
173
- (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
174
- )
175
-
176
- if (!typeFilter) {
177
- return merged
178
- }
179
- const typeSet = new Set(typeFilter)
180
- return merged.filter((item) => typeSet.has(item.type))
229
+ const merged = [...ledgerItems, ...fundingItems, ...(liquidations ?? [])]
230
+ const items = typeFilter
231
+ ? merged.filter((item) => typeFilter.includes(item.type))
232
+ : merged
233
+ return { items, saturated: false }
181
234
  }
182
235
 
183
- /**
184
- * Fetch a chronological activity feed (deposits, withdrawals, transfers,
185
- * liquidations, funding) for `address`. Combines results from Hyperliquid's
186
- * `userNonFundingLedgerUpdates`, `userFunding`, and `userFills` endpoints,
187
- * sorted newest-first.
188
- *
189
- * Cursor-based pagination uses the ms-since-epoch timestamp of the last item
190
- * on the current page.
191
- * @throws {PerpsError} On Hyperliquid REST error, network, or parsing failures.
192
- * @public
193
- */
236
+ /** Fetch retained activity newest-first with opaque cursors across inclusive time ranges. */
194
237
  export const getActivity = async (
195
238
  { client, apiUrl }: HyperliquidContext,
196
239
  params: GetActivityParams,
197
240
  options?: SDKRequestOptions
198
241
  ): Promise<ActivitiesResponse> => {
242
+ const limit = getHistoryLimit(params.limit)
199
243
  const registry = getMarketRegistry(client, PROVIDER_KEY)
200
244
  // Only funding and liquidation rows carry a market, so a Ledger-only
201
245
  // request must not pull the market list.
@@ -211,53 +255,38 @@ export const getActivity = async (
211
255
  }
212
256
  const infoOpts = hlInfoOptions(client, options)
213
257
 
214
- const limit = Math.min(
215
- params.limit ?? DEFAULT_HISTORY_LIMIT,
216
- MAX_HISTORY_LIMIT
217
- )
218
- const timeParams = {
219
- user: params.address,
220
- ...(params.startTime !== undefined || params.endTime !== undefined
221
- ? { startTime: params.startTime ?? 0 }
222
- : {}),
223
- ...(params.endTime === undefined ? {} : { endTime: params.endTime }),
224
- }
225
-
226
- // `get`, not `require`: a coin the backend market list does not hold drops
227
- // only its own row instead of rejecting the whole feed. The registry warns
228
- // once per unresolved id. A delisted market still resolves, so its rows stay.
229
- const merged = await fetchActivityData(
230
- apiUrl,
231
- params.type,
232
- timeParams,
233
- assetRegistry,
234
- (coin) => registry.get(coin),
235
- infoOpts
236
- )
237
-
238
- // Drop boundary items so the next page does not return the cursor row twice.
239
- const filtered =
240
- params.cursor === undefined
241
- ? merged
242
- : merged.filter(
243
- (item) =>
244
- new Date(item.timestamp).getTime() <
245
- Number.parseInt(params.cursor!, 10)
258
+ const needLiquidationFills =
259
+ !params.type || params.type.includes(ActivityType.LIQUIDATION)
260
+ let liquidationHistory: Promise<ActivityItem[]> | undefined
261
+ const page = await historyPage(
262
+ params,
263
+ limit,
264
+ (startTime, endTime, snapshotEnd) => {
265
+ if (needLiquidationFills && liquidationHistory === undefined) {
266
+ liquidationHistory = fetchLiquidationHistory(
267
+ apiUrl,
268
+ params.address,
269
+ params.startTime ?? 0,
270
+ snapshotEnd,
271
+ (coin) => registry.get(coin),
272
+ infoOpts
246
273
  )
247
-
248
- const hasMore = filtered.length > limit
249
- const items = filtered.slice(0, limit)
250
-
251
- return {
252
- provider: PROVIDER_KEY,
253
- items,
254
- pagination: {
255
- limit,
256
- hasMore,
257
- cursor:
258
- items.length > 0
259
- ? String(new Date(items[items.length - 1].timestamp).getTime())
260
- : undefined,
274
+ }
275
+ return fetchActivityData(
276
+ apiUrl,
277
+ params.type,
278
+ { user: params.address, startTime, endTime },
279
+ assetRegistry,
280
+ (coin) => registry.get(coin),
281
+ liquidationHistory,
282
+ infoOpts
283
+ )
261
284
  },
262
- }
285
+ (item) => ({
286
+ time: new Date(item.timestamp).getTime(),
287
+ key: `${item.type}:${item.id}`,
288
+ }),
289
+ options?.signal
290
+ )
291
+ return { provider: PROVIDER_KEY, ...page }
263
292
  }
@@ -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
  }