@tria-sdk/hyperliquid-core 0.1.0 → 6.44.0-beta

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/dist/api.js CHANGED
@@ -12,6 +12,64 @@ const CANDLE_LOOKBACK = 200;
12
12
  // ============================================================================
13
13
  // Helper Functions
14
14
  // ============================================================================
15
+ /**
16
+ * Maps a raw asset position entry to a typed HyperliquidPosition.
17
+ * Shared between fetchUserPositions and fetchUserState to avoid duplication.
18
+ */
19
+ const mapRawAssetPositionToPosition = (entry) => {
20
+ const pos = entry.position;
21
+ const posLeverage = pos.leverage;
22
+ const leverageObj = typeof posLeverage === "number"
23
+ ? { value: posLeverage, type: undefined }
24
+ : posLeverage || entry.leverage;
25
+ const leverageType = leverageObj?.type;
26
+ const isCross = leverageType === "cross" || (!leverageType && true);
27
+ return {
28
+ coin: pos.coin ?? "",
29
+ szi: pos.szi ?? "0",
30
+ entryPx: pos.entryPx || "0",
31
+ positionValue: pos.positionValue || "0",
32
+ unrealizedPnl: pos.unrealizedPnl || "0",
33
+ returnOnEquity: pos.returnOnEquity || "0",
34
+ liquidationPx: pos.liquidationPx || null,
35
+ marginUsed: pos.marginUsed || "0",
36
+ leverage: leverageObj?.value || 1,
37
+ isCross,
38
+ };
39
+ };
40
+ /**
41
+ * Maps a raw user fill entry to a typed HyperliquidUserFill.
42
+ * Shared between fetchUserFills and fetchUserFillsByTime to avoid duplication.
43
+ */
44
+ const mapRawUserFill = (fill) => ({
45
+ coin: String(fill.coin ?? ""),
46
+ px: String(fill.px ?? fill.price ?? "0"),
47
+ sz: String(fill.sz ?? fill.size ?? "0"),
48
+ side: String(fill.side ?? ""),
49
+ time: Number(fill.time ?? Date.now()),
50
+ fee: fill.fee ? String(fill.fee) : undefined,
51
+ feeToken: fill.feeToken ? String(fill.feeToken) : undefined,
52
+ hash: fill.hash ? String(fill.hash) : undefined,
53
+ oid: typeof fill.oid === "number" ? fill.oid : undefined,
54
+ tid: typeof fill.tid === "number" ? fill.tid : undefined,
55
+ cloid: fill.cloid ? String(fill.cloid) : undefined,
56
+ startPosition: fill.startPosition ? String(fill.startPosition) : undefined,
57
+ dir: fill.dir ? String(fill.dir) : undefined,
58
+ closedPnl: fill.closedPnl ? String(fill.closedPnl) : undefined,
59
+ crossed: typeof fill.crossed === "boolean" ? fill.crossed : undefined,
60
+ liquidation: fill.liquidation ?? undefined,
61
+ twapId: typeof fill.twapId === "number" ? fill.twapId : undefined,
62
+ });
63
+ /**
64
+ * Filters raw asset position entries to only include non-zero positions.
65
+ */
66
+ const filterNonZeroPositions = (entry) => {
67
+ const pos = entry?.position;
68
+ if (!pos)
69
+ return false;
70
+ const size = parseFloat(pos.szi || "0");
71
+ return size !== 0;
72
+ };
15
73
  const ensureSpotSymbol = (symbol) => {
16
74
  const trimmed = symbol?.trim();
17
75
  if (!trimmed) {
@@ -122,7 +180,7 @@ const pickValidPrice = (...values) => {
122
180
  return value;
123
181
  }
124
182
  }
125
- return "0";
183
+ return null;
126
184
  };
127
185
  // ============================================================================
128
186
  // Public API Functions
@@ -131,495 +189,494 @@ const pickValidPrice = (...values) => {
131
189
  * Retrieves raw spot metadata from Hyperliquid public API
132
190
  */
133
191
  export async function fetchSpotMeta(network = "testnet") {
134
- try {
135
- const client = getHyperliquidInfoClient(network);
136
- return await client.spotMeta();
137
- }
138
- catch (error) {
139
- throw error;
140
- }
192
+ const client = getHyperliquidInfoClient(network);
193
+ return client.spotMeta();
141
194
  }
142
195
  /**
143
196
  * Fetches token details for a specific token
144
197
  */
145
198
  export async function fetchTokenDetails(tokenId, network = "testnet") {
146
- try {
147
- const client = getHyperliquidInfoClient(network);
148
- const details = await client.tokenDetails({ tokenId });
149
- return {
150
- name: details.name,
151
- maxSupply: details.maxSupply,
152
- totalSupply: details.totalSupply,
153
- circulatingSupply: details.circulatingSupply,
154
- szDecimals: details.szDecimals,
155
- weiDecimals: details.weiDecimals,
156
- genesis: details.genesis,
157
- deployer: details.deployer || undefined,
158
- deployGas: details.deployGas || undefined,
159
- deployTime: details.deployTime || undefined,
160
- seededUsdc: details.seededUsdc,
161
- futureEmissions: details.futureEmissions,
162
- };
163
- }
164
- catch (error) {
165
- throw error;
166
- }
199
+ const client = getHyperliquidInfoClient(network);
200
+ const details = await client.tokenDetails({ tokenId });
201
+ return {
202
+ name: details.name,
203
+ maxSupply: details.maxSupply,
204
+ totalSupply: details.totalSupply,
205
+ circulatingSupply: details.circulatingSupply,
206
+ szDecimals: details.szDecimals,
207
+ weiDecimals: details.weiDecimals,
208
+ genesis: details.genesis,
209
+ deployer: details.deployer || undefined,
210
+ deployGas: details.deployGas || undefined,
211
+ deployTime: details.deployTime || undefined,
212
+ seededUsdc: details.seededUsdc,
213
+ futureEmissions: details.futureEmissions,
214
+ };
167
215
  }
168
216
  /**
169
217
  * Fetches all mid prices
170
218
  */
171
219
  export async function fetchAllMids(network = "testnet") {
172
- try {
173
- const client = getHyperliquidInfoClient(network);
174
- return await client.allMids();
175
- }
176
- catch (error) {
177
- throw error;
178
- }
220
+ const client = getHyperliquidInfoClient(network);
221
+ return client.allMids();
179
222
  }
180
223
  /**
181
224
  * Fetches spot metadata and asset contexts
182
225
  */
183
226
  export async function fetchSpotMetaAndAssetCtxs(network = "testnet") {
184
- try {
185
- const client = getHyperliquidInfoClient(network);
186
- return await client.spotMetaAndAssetCtxs();
227
+ const client = getHyperliquidInfoClient(network);
228
+ return client.spotMetaAndAssetCtxs();
229
+ }
230
+ /**
231
+ * Remaps token symbol for display purposes.
232
+ * Tokens with fullName starting with "Unit " are Unit tokens (e.g., UBTC → BTC)
233
+ * These are wrapped versions on HyperCore, but should display as the underlying asset.
234
+ */
235
+ function getDisplayName(symbol, fullName) {
236
+ // If fullName starts with "Unit ", this is a Unit token
237
+ // Remove the "U" prefix from the symbol for display (UBTC → BTC, UETH → ETH)
238
+ if (fullName?.startsWith("Unit ") && symbol.startsWith("U")) {
239
+ return symbol.slice(1);
187
240
  }
188
- catch (error) {
189
- throw error;
241
+ return symbol;
242
+ }
243
+ /**
244
+ * Normalizes quote token names for display.
245
+ * Hyperliquid uses versioned tokens (e.g., USDT0) but we want to display them as USDT.
246
+ */
247
+ function normalizeQuoteToken(symbol) {
248
+ // USDT0 → USDT (Hyperliquid's versioned USDT)
249
+ if (symbol === "USDT0") {
250
+ return "USDT";
190
251
  }
252
+ return symbol;
191
253
  }
192
254
  /**
193
255
  * Fetches spot tokens with market data
194
256
  */
195
257
  export async function fetchSpotTokensWithMarketData(network = "testnet") {
196
- try {
197
- const [[spotMeta, spotCtxs], allMids] = await Promise.all([
198
- fetchSpotMetaAndAssetCtxs(network),
199
- fetchAllMids(network),
200
- ]);
201
- if (!spotMeta?.tokens || !Array.isArray(spotCtxs)) {
202
- return [];
203
- }
204
- return spotMeta.tokens
205
- .map((token, index) => {
206
- const assetCtx = spotCtxs[index];
207
- const currentPrice = pickValidPrice(allMids[token.name], assetCtx?.midPx, assetCtx?.markPx);
208
- const lastPrice = pickValidPrice(assetCtx?.markPx, assetCtx?.midPx, assetCtx?.prevDayPx, allMids[token.name]);
209
- let change24h = "0.00";
210
- if (assetCtx?.prevDayPx) {
211
- const prevPx = parseFloat(assetCtx.prevDayPx);
212
- const nowPx = parseFloat(currentPrice);
213
- if (prevPx > 0 && Number.isFinite(nowPx)) {
214
- change24h = (((nowPx - prevPx) / prevPx) * 100).toFixed(2);
215
- }
216
- }
217
- return {
218
- name: token.name,
219
- szDecimals: token.szDecimals,
220
- fullName: token.fullName || undefined,
221
- price: currentPrice,
222
- lastPrice,
223
- midPrice: assetCtx?.midPx,
224
- markPrice: assetCtx?.markPx,
225
- change24h,
226
- volume24h: assetCtx?.dayBaseVlm || "0",
227
- circulatingSupply: assetCtx?.circulatingSupply || "0",
228
- };
229
- })
230
- .filter((token) => {
231
- const price = parseFloat(token.price || "0");
232
- const volume = parseFloat(token.volume24h || "0");
233
- return Number.isFinite(price) && price > 0 && volume > 0;
234
- });
258
+ const [[spotMeta, spotCtxs], allMids] = await Promise.all([
259
+ fetchSpotMetaAndAssetCtxs(network),
260
+ fetchAllMids(network),
261
+ ]);
262
+ if (!spotMeta?.tokens || !Array.isArray(spotCtxs)) {
263
+ return [];
235
264
  }
236
- catch (error) {
237
- throw error;
265
+ return spotMeta.tokens
266
+ .map((token, index) => {
267
+ const assetCtx = spotCtxs[index];
268
+ const currentPrice = pickValidPrice(allMids[token.name], assetCtx?.midPx, assetCtx?.markPx);
269
+ const lastPrice = pickValidPrice(assetCtx?.markPx, assetCtx?.midPx, assetCtx?.prevDayPx, allMids[token.name]);
270
+ let change24h = null;
271
+ if (assetCtx?.prevDayPx && currentPrice) {
272
+ const prevPx = parseFloat(assetCtx.prevDayPx);
273
+ const nowPx = parseFloat(currentPrice);
274
+ if (prevPx > 0 && Number.isFinite(nowPx) && nowPx > 0) {
275
+ change24h = (((nowPx - prevPx) / prevPx) * 100).toFixed(2);
276
+ }
277
+ }
278
+ return {
279
+ name: token.name,
280
+ szDecimals: token.szDecimals,
281
+ fullName: token.fullName || undefined,
282
+ price: currentPrice,
283
+ lastPrice,
284
+ midPrice: assetCtx?.midPx ?? null,
285
+ markPrice: assetCtx?.markPx ?? null,
286
+ change24h,
287
+ volume24h: assetCtx?.dayBaseVlm ?? null,
288
+ circulatingSupply: assetCtx?.circulatingSupply ?? null,
289
+ };
290
+ })
291
+ .filter((token) => {
292
+ const price = parseFloat(token.price || "0");
293
+ const volume = parseFloat(token.volume24h || "0");
294
+ return Number.isFinite(price) && price > 0 && volume > 0;
295
+ });
296
+ }
297
+ /**
298
+ * Fetches spot trading pairs with market data using universe pairs
299
+ * This properly aligns pair data with asset contexts using pair.index
300
+ *
301
+ * Important: spotCtxs is indexed by pair.index (from universe), NOT by array position
302
+ * Each pair in universe has an 'index' field that corresponds to spotCtxs[index]
303
+ */
304
+ export async function fetchSpotPairsWithMarketData(network = "testnet") {
305
+ const [spotMeta, spotCtxs] = await fetchSpotMetaAndAssetCtxs(network);
306
+ if (!spotMeta?.universe || !spotMeta?.tokens || !Array.isArray(spotCtxs)) {
307
+ return [];
238
308
  }
309
+ // Create a token lookup by token index
310
+ const tokensByIndex = new Map();
311
+ spotMeta.tokens.forEach((token) => {
312
+ if (token.index !== undefined) {
313
+ tokensByIndex.set(token.index, token);
314
+ }
315
+ });
316
+ return spotMeta.universe
317
+ .map((pair) => {
318
+ // CRITICAL: Use pair.index to access spotCtxs, NOT array position
319
+ const pairIndex = pair.index;
320
+ const assetCtx = spotCtxs[pairIndex];
321
+ // Extract base and quote token indices from the pair
322
+ // tokens array is [baseTokenIndex, quoteTokenIndex]
323
+ const [baseTokenIndex, quoteTokenIndex] = pair.tokens || [];
324
+ const baseToken = tokensByIndex.get(baseTokenIndex);
325
+ const quoteToken = tokensByIndex.get(quoteTokenIndex);
326
+ if (!baseToken) {
327
+ return null;
328
+ }
329
+ const baseTokenName = baseToken.name;
330
+ const rawQuoteTokenName = (quoteToken?.name || "USDC");
331
+ // Normalize quote token for display (e.g., USDT0 → USDT)
332
+ const quoteTokenName = normalizeQuoteToken(rawQuoteTokenName);
333
+ const baseFullName = baseToken.fullName;
334
+ // Get display name (remaps Unit tokens like UBTC → BTC)
335
+ const displayName = getDisplayName(baseTokenName, baseFullName);
336
+ // Use pair name from API (e.g., "PURR/USDC" or "@1" for non-canonical)
337
+ // This is the identifier needed for API calls like l2Book and trades
338
+ const pairName = pair.name;
339
+ // Construct the API coin identifier:
340
+ // - For PURR (canonical pair with USDC), use "PURR/USDC"
341
+ // - For all other spot tokens, use the @{index} format
342
+ // NOTE: Use raw quote token name for API calls (USDT0 not USDT)
343
+ const apiCoin = pairName.startsWith("@")
344
+ ? pairName
345
+ : `${baseTokenName}/${rawQuoteTokenName}`;
346
+ // For display, construct a readable pair name using remapped display name
347
+ const displayPairName = `${displayName}/${quoteTokenName}`;
348
+ // Price data comes directly from assetCtx which is aligned by pair.index
349
+ const midPrice = assetCtx?.midPx ?? null;
350
+ const markPrice = assetCtx?.markPx ?? null;
351
+ const prevDayPrice = assetCtx?.prevDayPx ?? null;
352
+ // Use assetCtx prices as primary source (they are properly aligned)
353
+ const currentPrice = pickValidPrice(midPrice, markPrice);
354
+ const lastPrice = pickValidPrice(markPrice, midPrice, prevDayPrice);
355
+ // Calculate 24h change only if we have valid prices
356
+ let change24h = null;
357
+ if (prevDayPrice && currentPrice) {
358
+ const prevPx = parseFloat(prevDayPrice);
359
+ const nowPx = parseFloat(currentPrice);
360
+ if (prevPx > 0 && Number.isFinite(nowPx) && nowPx > 0) {
361
+ change24h = (((nowPx - prevPx) / prevPx) * 100).toFixed(2);
362
+ }
363
+ }
364
+ // Volume - return null if no data
365
+ const volume24h = assetCtx?.dayNtlVlm ?? null;
366
+ // Market cap - requires circulating supply which isn't in bulk API
367
+ // Will be null until we have circulating supply data
368
+ const marketCap = null;
369
+ // Token ID (hex address) and index for explorer links
370
+ const tokenId = baseToken.tokenId;
371
+ const tokenIndex = baseToken.index;
372
+ return {
373
+ pairName: displayPairName,
374
+ apiCoin,
375
+ baseToken: baseTokenName,
376
+ displayName,
377
+ // Return raw quote token name for API calls (USDT0, not USDT)
378
+ quoteToken: rawQuoteTokenName,
379
+ szDecimals: baseToken.szDecimals,
380
+ fullName: baseFullName,
381
+ price: currentPrice,
382
+ lastPrice,
383
+ midPrice,
384
+ markPrice,
385
+ change24h,
386
+ volume24h,
387
+ marketCap,
388
+ isCanonical: pair.isCanonical,
389
+ tokenId,
390
+ index: tokenIndex,
391
+ };
392
+ })
393
+ .filter((pair) => {
394
+ return pair !== null;
395
+ });
239
396
  }
240
397
  /**
241
398
  * Fetches spot order book for a token
242
399
  */
243
400
  export async function fetchSpotOrderBook(tokenName, network = "testnet") {
244
- try {
245
- const client = getHyperliquidInfoClient(network);
246
- const coin = ensureSpotSymbol(tokenName);
247
- return await client.l2Book({ coin });
248
- }
249
- catch (error) {
250
- throw error;
251
- }
401
+ const client = getHyperliquidInfoClient(network);
402
+ const coin = ensureSpotSymbol(tokenName);
403
+ return client.l2Book({ coin });
252
404
  }
253
405
  /**
254
406
  * Fetches order book snapshot with normalized levels
255
407
  */
256
408
  export async function fetchOrderBook(symbol, depth = 25, network = "testnet") {
257
409
  const { coin, market } = parseSymbolForBook(symbol);
258
- try {
259
- const client = getHyperliquidInfoClient(network);
260
- const response = (await client.l2Book({ coin }));
261
- const levels = Array.isArray(response?.levels)
262
- ? response.levels
263
- : [];
264
- const [rawBids = [], rawAsks = []] = levels;
265
- const bids = normalizeOrderBookLevels(rawBids, depth);
266
- const asks = normalizeOrderBookLevels(rawAsks, depth);
267
- const bestBid = bids[0]?.price ?? null;
268
- const bestAsk = asks[0]?.price ?? null;
269
- const midPrice = bestBid !== null && bestAsk !== null
270
- ? (bestBid + bestAsk) / 2
271
- : bestBid ?? bestAsk ?? null;
272
- const spread = bestBid !== null && bestAsk !== null
273
- ? Math.max(bestAsk - bestBid, 0)
274
- : null;
275
- return {
276
- symbol,
277
- coin,
278
- market,
279
- bids,
280
- asks,
281
- bestBid,
282
- bestAsk,
283
- midPrice,
284
- spread,
285
- lastUpdated: typeof response?.time === "number" ? response.time : Date.now(),
286
- };
287
- }
288
- catch (error) {
289
- throw error;
290
- }
410
+ const client = getHyperliquidInfoClient(network);
411
+ const response = (await client.l2Book({ coin }));
412
+ const levels = Array.isArray(response?.levels)
413
+ ? response.levels
414
+ : [];
415
+ const [rawBids = [], rawAsks = []] = levels;
416
+ const bids = normalizeOrderBookLevels(rawBids, depth);
417
+ const asks = normalizeOrderBookLevels(rawAsks, depth);
418
+ const bestBid = bids[0]?.price ?? null;
419
+ const bestAsk = asks[0]?.price ?? null;
420
+ const midPrice = bestBid !== null && bestAsk !== null
421
+ ? (bestBid + bestAsk) / 2
422
+ : bestBid ?? bestAsk ?? null;
423
+ const spread = bestBid !== null && bestAsk !== null
424
+ ? Math.max(bestAsk - bestBid, 0)
425
+ : null;
426
+ return {
427
+ symbol,
428
+ coin,
429
+ market,
430
+ bids,
431
+ asks,
432
+ bestBid,
433
+ bestAsk,
434
+ midPrice,
435
+ spread,
436
+ lastUpdated: typeof response?.time === "number" ? response.time : Date.now(),
437
+ };
291
438
  }
292
439
  /**
293
440
  * Fetches recent trades for a coin
294
441
  */
295
442
  export async function fetchRecentTrades(coin, limit = 20, network = "testnet") {
296
- try {
297
- const client = getHyperliquidInfoClient(network);
298
- const anyClient = client;
299
- let trades = [];
300
- if (typeof anyClient.tradesSnapshot === "function") {
301
- trades = await anyClient.tradesSnapshot({ coin, limit });
302
- }
303
- else if (typeof anyClient.trades === "function") {
304
- trades = await anyClient.trades({ coin, limit });
305
- }
306
- else if (typeof anyClient.tradeHistory === "function") {
307
- trades = await anyClient.tradeHistory({ coin, limit });
308
- }
309
- else if (typeof anyClient.recentTrades === "function") {
310
- trades = await anyClient.recentTrades({ coin, limit });
311
- }
312
- return trades.map((trade) => {
313
- const price = trade.p ?? trade.px ?? "0";
314
- const size = trade.s ?? trade.sz ?? "0";
315
- const timestamp = trade.time ?? Date.now();
316
- return {
317
- price: String(price),
318
- size: String(size),
319
- side: typeof trade.side === "string" ? trade.side : undefined,
320
- time: typeof timestamp === "number"
321
- ? timestamp
322
- : Number(timestamp) || Date.now(),
323
- };
324
- });
325
- }
326
- catch (error) {
327
- return [];
443
+ const transport = getHyperliquidTransport(network);
444
+ const trades = await transport.request("info", {
445
+ type: "recentTrades",
446
+ coin,
447
+ limit,
448
+ });
449
+ if (!Array.isArray(trades)) {
450
+ throw new Error(`Unexpected API response for recentTrades: expected array, got ${typeof trades}`);
328
451
  }
452
+ return trades.map((trade) => {
453
+ const price = trade.px ?? trade.p ?? "0";
454
+ const size = trade.sz ?? trade.s ?? "0";
455
+ const timestamp = trade.time ?? Date.now();
456
+ return {
457
+ price: String(price),
458
+ size: String(size),
459
+ side: typeof trade.side === "string" ? trade.side : undefined,
460
+ time: typeof timestamp === "number"
461
+ ? timestamp
462
+ : Number(timestamp) || Date.now(),
463
+ };
464
+ });
329
465
  }
330
466
  /**
331
467
  * Fetches perp metadata
332
468
  */
333
469
  export async function fetchPerpMeta(network = "testnet") {
334
- try {
335
- const client = getHyperliquidInfoClient(network);
336
- return await client.meta();
337
- }
338
- catch (error) {
339
- throw error;
340
- }
470
+ const client = getHyperliquidInfoClient(network);
471
+ return client.meta();
341
472
  }
342
473
  /**
343
474
  * Fetches perp metadata and asset contexts
344
475
  */
345
476
  export async function fetchPerpMetaAndAssetCtxs(network = "testnet") {
346
- try {
347
- const client = getHyperliquidInfoClient(network);
348
- return await client.metaAndAssetCtxs();
349
- }
350
- catch (error) {
351
- throw error;
352
- }
477
+ const client = getHyperliquidInfoClient(network);
478
+ return client.metaAndAssetCtxs();
353
479
  }
354
480
  /**
355
481
  * Fetches funding history for a coin
356
482
  */
357
483
  export async function fetchFundingHistory(coin, startTime, network = "testnet") {
358
- try {
359
- const client = getHyperliquidInfoClient(network);
360
- return await client.fundingHistory({ coin, startTime });
361
- }
362
- catch (error) {
363
- throw error;
364
- }
484
+ const client = getHyperliquidInfoClient(network);
485
+ return client.fundingHistory({ coin, startTime });
365
486
  }
366
487
  /**
367
488
  * Fetches user fills (trade history)
368
489
  */
369
490
  export async function fetchUserFills(params) {
370
491
  const { user, aggregateByTime = false, limit = 100, network = "testnet", } = params;
371
- try {
372
- const client = getHyperliquidInfoClient(network);
373
- const payload = { user, aggregateByTime };
374
- let fills = [];
375
- if (typeof client.userFills === "function") {
376
- fills = await client.userFills(payload);
377
- }
378
- else if (typeof client.userFillsByTime === "function") {
379
- const now = Date.now();
380
- fills = await client.userFillsByTime({
381
- ...payload,
382
- startTime: now - 30 * 24 * 60 * 60 * 1000,
383
- endTime: now,
384
- });
385
- }
386
- return Array.isArray(fills)
387
- ? fills.slice(0, limit).map((fill) => ({
388
- coin: String(fill.coin ?? ""),
389
- px: String(fill.px ?? fill.price ?? "0"),
390
- sz: String(fill.sz ?? fill.size ?? "0"),
391
- side: fill.side ?? "",
392
- time: Number(fill.time ?? Date.now()),
393
- fee: fill.fee ? String(fill.fee) : undefined,
394
- feeToken: fill.feeToken ? String(fill.feeToken) : undefined,
395
- hash: fill.hash ? String(fill.hash) : undefined,
396
- oid: typeof fill.oid === "number" ? fill.oid : undefined,
397
- tid: typeof fill.tid === "number" ? fill.tid : undefined,
398
- cloid: fill.cloid ? String(fill.cloid) : undefined,
399
- startPosition: fill.startPosition
400
- ? String(fill.startPosition)
401
- : undefined,
402
- dir: fill.dir ? String(fill.dir) : undefined,
403
- closedPnl: fill.closedPnl ? String(fill.closedPnl) : undefined,
404
- crossed: typeof fill.crossed === "boolean" ? fill.crossed : undefined,
405
- liquidation: fill.liquidation,
406
- twapId: typeof fill.twapId === "number" ? fill.twapId : undefined,
407
- }))
408
- : [];
409
- }
410
- catch (error) {
411
- return [];
492
+ const transport = getHyperliquidTransport(network);
493
+ const now = Date.now();
494
+ const fills = await transport.request("info", {
495
+ type: "userFillsByTime",
496
+ user,
497
+ aggregateByTime,
498
+ startTime: now - 30 * 24 * 60 * 60 * 1000,
499
+ endTime: now,
500
+ });
501
+ if (!Array.isArray(fills)) {
502
+ throw new Error(`Unexpected API response for userFillsByTime: expected array, got ${typeof fills}`);
412
503
  }
504
+ return fills.slice(0, limit).map(mapRawUserFill);
413
505
  }
414
506
  /**
415
507
  * Fetches user fills within a time window (order history)
416
508
  */
417
509
  export async function fetchUserFillsByTime(params) {
418
510
  const { user, startTime, endTime = Date.now(), aggregateByTime = true, limit = 200, network = "testnet", } = params;
419
- try {
420
- const client = getHyperliquidInfoClient(network);
421
- let fills = [];
422
- if (typeof client.userFillsByTime === "function") {
423
- fills = await client.userFillsByTime({
424
- user,
425
- startTime,
426
- endTime,
427
- aggregateByTime,
428
- });
429
- }
430
- else if (typeof client.userFills === "function") {
431
- fills = await client.userFills({ user, aggregateByTime });
432
- }
433
- return Array.isArray(fills)
434
- ? fills
435
- .filter((fill) => {
436
- const t = Number(fill.time ?? 0);
437
- return t >= startTime && t <= endTime;
438
- })
439
- .slice(0, limit)
440
- .map((fill) => ({
441
- coin: String(fill.coin ?? ""),
442
- px: String(fill.px ?? fill.price ?? "0"),
443
- sz: String(fill.sz ?? fill.size ?? "0"),
444
- side: fill.side ?? "",
445
- time: Number(fill.time ?? Date.now()),
446
- fee: fill.fee ? String(fill.fee) : undefined,
447
- feeToken: fill.feeToken ? String(fill.feeToken) : undefined,
448
- hash: fill.hash ? String(fill.hash) : undefined,
449
- oid: typeof fill.oid === "number" ? fill.oid : undefined,
450
- tid: typeof fill.tid === "number" ? fill.tid : undefined,
451
- cloid: fill.cloid ? String(fill.cloid) : undefined,
452
- startPosition: fill.startPosition
453
- ? String(fill.startPosition)
454
- : undefined,
455
- dir: fill.dir ? String(fill.dir) : undefined,
456
- closedPnl: fill.closedPnl ? String(fill.closedPnl) : undefined,
457
- crossed: typeof fill.crossed === "boolean" ? fill.crossed : undefined,
458
- liquidation: fill.liquidation,
459
- twapId: typeof fill.twapId === "number" ? fill.twapId : undefined,
460
- }))
461
- : [];
462
- }
463
- catch (error) {
464
- return [];
465
- }
511
+ const transport = getHyperliquidTransport(network);
512
+ const fills = await transport.request("info", {
513
+ type: "userFillsByTime",
514
+ user,
515
+ startTime,
516
+ endTime,
517
+ aggregateByTime,
518
+ });
519
+ if (!Array.isArray(fills)) {
520
+ throw new Error(`Unexpected API response for userFillsByTime: expected array, got ${typeof fills}`);
521
+ }
522
+ return fills
523
+ .filter((fill) => {
524
+ const t = Number(fill.time ?? 0);
525
+ return t >= startTime && t <= endTime;
526
+ })
527
+ .slice(0, limit)
528
+ .map(mapRawUserFill);
466
529
  }
467
530
  /**
468
531
  * Fetches user funding ledger updates
469
532
  */
470
533
  export async function fetchUserFunding(params) {
471
534
  const { user, startTime, endTime = null, limit = 200, network = "testnet", } = params;
472
- try {
473
- const client = getHyperliquidInfoClient(network);
474
- let updates = [];
475
- if (typeof client.userFunding === "function") {
476
- updates = await client.userFunding({ user, startTime, endTime });
477
- }
478
- return Array.isArray(updates)
479
- ? updates.slice(0, limit).map((update) => ({
480
- time: Number(update.time ?? Date.now()),
481
- hash: update.hash ? String(update.hash) : undefined,
482
- delta: update.delta,
483
- }))
484
- : [];
485
- }
486
- catch (error) {
487
- return [];
488
- }
535
+ const transport = getHyperliquidTransport(network);
536
+ const updates = await transport.request("info", {
537
+ type: "userFunding",
538
+ user,
539
+ startTime,
540
+ endTime,
541
+ });
542
+ if (!Array.isArray(updates)) {
543
+ throw new Error(`Unexpected API response for userFunding: expected array, got ${typeof updates}`);
544
+ }
545
+ return updates.slice(0, limit).map((update) => ({
546
+ time: Number(update.time ?? Date.now()),
547
+ hash: update.hash ? String(update.hash) : undefined,
548
+ delta: {
549
+ type: "funding",
550
+ coin: update.delta?.coin ?? "",
551
+ usdc: update.delta?.usdc ?? "0",
552
+ szi: update.delta?.szi ?? "0",
553
+ fundingRate: update.delta?.fundingRate ?? "0",
554
+ nSamples: update.delta?.nSamples ?? 0,
555
+ },
556
+ }));
489
557
  }
490
558
  /**
491
559
  * Fetches user non-funding ledger updates (deposits, withdrawals, transfers)
492
560
  */
493
561
  export async function fetchUserNonFundingLedgerUpdates(params) {
494
562
  const { user, startTime, endTime = null, limit = 200, network = "testnet", } = params;
495
- try {
496
- const client = getHyperliquidInfoClient(network);
497
- let updates = [];
498
- if (typeof client.userNonFundingLedgerUpdates === "function") {
499
- updates = await client.userNonFundingLedgerUpdates({
500
- user,
501
- startTime,
502
- endTime,
503
- });
504
- }
505
- return Array.isArray(updates)
506
- ? updates.slice(0, limit).map((update) => ({
507
- time: Number(update.time ?? Date.now()),
508
- hash: update.hash ? String(update.hash) : undefined,
509
- delta: update.delta,
510
- }))
511
- : [];
512
- }
513
- catch (error) {
514
- return [];
515
- }
563
+ const transport = getHyperliquidTransport(network);
564
+ const updates = await transport.request("info", {
565
+ type: "userNonFundingLedgerUpdates",
566
+ user,
567
+ startTime,
568
+ endTime,
569
+ });
570
+ if (!Array.isArray(updates)) {
571
+ throw new Error(`Unexpected API response for userNonFundingLedgerUpdates: expected array, got ${typeof updates}`);
572
+ }
573
+ return updates.slice(0, limit).map((update) => ({
574
+ time: Number(update.time ?? Date.now()),
575
+ hash: update.hash ? String(update.hash) : undefined,
576
+ delta: update.delta ?? { type: "" },
577
+ }));
516
578
  }
517
579
  /**
518
580
  * Fetches perps with market data
519
581
  */
520
582
  export async function fetchPerpsWithMarketData(network = "testnet") {
521
- try {
522
- const [[perpMeta, perpCtxs], allMids] = await Promise.all([
523
- fetchPerpMetaAndAssetCtxs(network),
524
- fetchAllMids(network),
525
- ]);
526
- if (!perpMeta?.universe || !Array.isArray(perpCtxs)) {
527
- return [];
528
- }
529
- return perpMeta.universe.map((asset, index) => {
530
- const assetCtx = perpCtxs[index];
531
- const midPrice = allMids[asset.name] || "0";
532
- let change24h = "0.00";
533
- if (assetCtx?.prevDayPx && assetCtx?.midPx) {
534
- const prev = parseFloat(assetCtx.prevDayPx);
535
- const current = parseFloat(assetCtx.midPx);
536
- if (prev > 0) {
537
- change24h = (((current - prev) / prev) * 100).toFixed(2);
538
- }
539
- }
540
- const fundingRate = assetCtx?.funding
541
- ? (parseFloat(assetCtx.funding) * 8 * 100).toFixed(4)
542
- : "0.0000";
543
- const now = Date.now();
544
- const eightHours = 8 * 60 * 60 * 1000;
545
- const nextFunding = Math.ceil(now / eightHours) * eightHours;
546
- const hoursUntil = Math.max(0, Math.round((nextFunding - now) / (60 * 60 * 1000)));
547
- const anyAsset = asset;
548
- const anyCtx = assetCtx;
549
- const levFromMeta = typeof anyAsset?.maxLeverage === "number"
550
- ? anyAsset.maxLeverage
551
- : typeof anyAsset?.maxLev === "number"
552
- ? anyAsset.maxLev
553
- : undefined;
554
- const levFromCtx = typeof anyCtx?.risk?.maxLev === "number"
555
- ? anyCtx.risk.maxLev
556
- : typeof anyCtx?.maxLeverage === "number"
557
- ? anyCtx.maxLeverage
558
- : undefined;
559
- const maxLeverage = levFromMeta ?? levFromCtx ?? 10;
560
- const rawOi = parseFloat(assetCtx?.openInterest || "0");
561
- const markPx = assetCtx?.markPx ?? assetCtx?.midPx ?? midPrice;
562
- const markValue = parseFloat(String(markPx) || "0");
563
- const openInterest = Number.isFinite(rawOi) && Number.isFinite(markValue)
564
- ? (rawOi * markValue).toString()
565
- : assetCtx?.openInterest || "0";
566
- return {
567
- name: asset.name,
568
- price: midPrice,
569
- change24h,
570
- volume24h: assetCtx?.dayNtlVlm || "0",
571
- openInterest,
572
- fundingRate: `${fundingRate}%`,
573
- nextFunding: `${hoursUntil}h`,
574
- maxLeverage,
575
- };
576
- });
577
- }
578
- catch (error) {
579
- throw error;
583
+ const [[perpMeta, perpCtxs], allMids] = await Promise.all([
584
+ fetchPerpMetaAndAssetCtxs(network),
585
+ fetchAllMids(network),
586
+ ]);
587
+ if (!perpMeta?.universe || !Array.isArray(perpCtxs)) {
588
+ return [];
580
589
  }
590
+ return perpMeta.universe.map((asset, index) => {
591
+ const assetCtx = perpCtxs[index];
592
+ const midPrice = allMids[asset.name] || "0";
593
+ let change24h = "0.00";
594
+ if (assetCtx?.prevDayPx && assetCtx?.midPx) {
595
+ const prev = parseFloat(assetCtx.prevDayPx);
596
+ const current = parseFloat(assetCtx.midPx);
597
+ if (prev > 0) {
598
+ change24h = (((current - prev) / prev) * 100).toFixed(2);
599
+ }
600
+ }
601
+ // The funding field from the API is the 8-hour rate as a decimal.
602
+ // Multiply by 100 to convert to percentage (e.g., 0.000013 -> 0.0013%)
603
+ const fundingRate = assetCtx?.funding
604
+ ? (parseFloat(assetCtx.funding) * 100).toFixed(4)
605
+ : "0.0000";
606
+ const now = Date.now();
607
+ const eightHours = 8 * 60 * 60 * 1000;
608
+ const nextFunding = Math.ceil(now / eightHours) * eightHours;
609
+ const hoursUntil = Math.max(0, Math.round((nextFunding - now) / (60 * 60 * 1000)));
610
+ const levFromMeta = typeof asset?.maxLeverage === "number" ? asset.maxLeverage : undefined;
611
+ const levFromCtx = typeof assetCtx?.risk?.maxLev === "number"
612
+ ? assetCtx.risk.maxLev
613
+ : typeof assetCtx?.maxLeverage === "number"
614
+ ? assetCtx.maxLeverage
615
+ : undefined;
616
+ const maxLeverage = levFromMeta ?? levFromCtx ?? 10;
617
+ const rawOi = parseFloat(assetCtx?.openInterest || "0");
618
+ const markPx = assetCtx?.markPx ?? assetCtx?.midPx ?? midPrice;
619
+ const markValue = parseFloat(String(markPx) || "0");
620
+ const openInterest = Number.isFinite(rawOi) && Number.isFinite(markValue)
621
+ ? (rawOi * markValue).toString()
622
+ : assetCtx?.openInterest || "0";
623
+ return {
624
+ name: asset.name,
625
+ price: midPrice,
626
+ change24h,
627
+ volume24h: assetCtx?.dayNtlVlm || "0",
628
+ openInterest,
629
+ fundingRate: `${fundingRate}%`,
630
+ nextFunding: `${hoursUntil}h`,
631
+ maxLeverage,
632
+ szDecimals: asset.szDecimals ?? 0,
633
+ };
634
+ });
581
635
  }
582
636
  /**
583
637
  * Fetches candlestick data
584
638
  */
585
639
  export async function fetchCandlesticks(params) {
586
640
  const { coin, interval, startTime, endTime, network = "testnet" } = params;
587
- try {
588
- const client = getHyperliquidInfoClient(network);
589
- const now = Date.now();
590
- const fromTime = startTime ?? now - CANDLE_LOOKBACK * intervalToMs(interval);
591
- const toTime = endTime ?? now;
592
- const candles = await client.candleSnapshot({
593
- coin,
594
- interval,
595
- startTime: fromTime,
596
- endTime: toTime,
597
- });
598
- if (!Array.isArray(candles) || candles.length === 0) {
599
- throw new Error(`No candlestick data for ${coin}`);
600
- }
601
- return candles
602
- .sort((a, b) => a.t - b.t)
603
- .map((candle) => ({
604
- time: Math.floor(candle.t / 1000),
605
- open: parseFloat(candle.o),
606
- high: parseFloat(candle.h),
607
- low: parseFloat(candle.l),
608
- close: parseFloat(candle.c),
609
- volume: parseFloat(candle.v || "0"),
610
- }))
611
- .filter((candle, index, arr) => index === 0 ? true : candle.time > arr[index - 1].time);
612
- }
613
- catch (error) {
614
- throw error;
615
- }
641
+ const client = getHyperliquidInfoClient(network);
642
+ const now = Date.now();
643
+ const fromTime = startTime ?? now - CANDLE_LOOKBACK * intervalToMs(interval);
644
+ const toTime = endTime ?? now;
645
+ const candles = await client.candleSnapshot({
646
+ coin,
647
+ interval,
648
+ startTime: fromTime,
649
+ endTime: toTime,
650
+ });
651
+ if (!Array.isArray(candles) || candles.length === 0) {
652
+ throw new Error(`No candlestick data for ${coin}`);
653
+ }
654
+ return candles
655
+ .sort((a, b) => a.t - b.t)
656
+ .map((candle) => ({
657
+ time: Math.floor(candle.t / 1000),
658
+ open: parseFloat(candle.o),
659
+ high: parseFloat(candle.h),
660
+ low: parseFloat(candle.l),
661
+ close: parseFloat(candle.c),
662
+ volume: parseFloat(candle.v || "0"),
663
+ }))
664
+ .filter((candle, index, arr) => index === 0 ? true : candle.time > arr[index - 1].time);
616
665
  }
617
666
  /**
618
667
  * Fetches spot candles for a token
668
+ * tokenName can be:
669
+ * - "@{index}" format (e.g., "@107") - passed directly to API
670
+ * - "PURR/USDC" format - passed directly to API
671
+ * - "PURR" format - converted to "PURR/USDC"
619
672
  */
620
673
  export async function fetchSpotCandles(params) {
621
674
  const { tokenName, interval, startTime, endTime, network } = params;
622
- const coin = ensureSpotSymbol(tokenName);
675
+ // If tokenName starts with "@", it's already in API format - use directly
676
+ // Otherwise, ensure it has the /QUOTE suffix
677
+ const coin = tokenName.startsWith("@")
678
+ ? tokenName
679
+ : ensureSpotSymbol(tokenName);
623
680
  return fetchCandlesticks({
624
681
  coin,
625
682
  interval,
@@ -629,47 +686,24 @@ export async function fetchSpotCandles(params) {
629
686
  });
630
687
  }
631
688
  /**
632
- * Fetches latest price for a coin
689
+ * Fetches latest price for a coin.
690
+ * @remarks
691
+ * `high24h` and `low24h` are returned as null because the Hyperliquid API
692
+ * does not provide actual 24-hour high/low data in this endpoint.
633
693
  */
634
694
  export async function fetchLatestPrice(coin, network = "testnet") {
635
- try {
636
- const [[perpMeta, perpCtxs], allMids] = await Promise.all([
637
- fetchPerpMetaAndAssetCtxs(network),
638
- fetchAllMids(network),
639
- ]);
640
- const perpIndex = perpMeta?.universe?.findIndex((asset) => asset.name === coin);
641
- if (perpIndex !== undefined && perpIndex > -1) {
642
- const assetCtx = perpCtxs[perpIndex];
643
- const currentPrice = allMids[coin] || assetCtx?.midPx || assetCtx?.markPx || "0";
644
- let change24h = "0.00";
645
- if (assetCtx?.prevDayPx && assetCtx?.midPx) {
646
- const prev = parseFloat(assetCtx.prevDayPx);
647
- const current = parseFloat(assetCtx.midPx);
648
- if (prev > 0) {
649
- change24h = (((current - prev) / prev) * 100).toFixed(2);
650
- }
651
- }
652
- return {
653
- price: currentPrice,
654
- change24h,
655
- high24h: currentPrice,
656
- low24h: currentPrice,
657
- volume24h: assetCtx?.dayNtlVlm || "0",
658
- };
659
- }
660
- const [[spotMeta, spotCtxs]] = await Promise.all([
661
- fetchSpotMetaAndAssetCtxs(network),
662
- ]);
663
- const spotIndex = spotMeta?.tokens?.findIndex((token) => token.name === coin);
664
- if (spotIndex === undefined || spotIndex === -1) {
665
- throw new Error(`Coin ${coin} not found in Hyperliquid markets`);
666
- }
667
- const spotCtx = spotCtxs[spotIndex];
668
- const currentPrice = allMids[coin] || spotCtx?.midPx || spotCtx?.markPx || "0";
695
+ const [[perpMeta, perpCtxs], allMids] = await Promise.all([
696
+ fetchPerpMetaAndAssetCtxs(network),
697
+ fetchAllMids(network),
698
+ ]);
699
+ const perpIndex = perpMeta?.universe?.findIndex((asset) => asset.name === coin);
700
+ if (perpIndex !== undefined && perpIndex > -1) {
701
+ const assetCtx = perpCtxs[perpIndex];
702
+ const currentPrice = allMids[coin] || assetCtx?.midPx || assetCtx?.markPx || "0";
669
703
  let change24h = "0.00";
670
- if (spotCtx?.prevDayPx && spotCtx?.midPx) {
671
- const prev = parseFloat(spotCtx.prevDayPx);
672
- const current = parseFloat(spotCtx.midPx);
704
+ if (assetCtx?.prevDayPx && assetCtx?.midPx) {
705
+ const prev = parseFloat(assetCtx.prevDayPx);
706
+ const current = parseFloat(assetCtx.midPx);
673
707
  if (prev > 0) {
674
708
  change24h = (((current - prev) / prev) * 100).toFixed(2);
675
709
  }
@@ -677,17 +711,40 @@ export async function fetchLatestPrice(coin, network = "testnet") {
677
711
  return {
678
712
  price: currentPrice,
679
713
  change24h,
680
- high24h: currentPrice,
681
- low24h: currentPrice,
682
- volume24h: spotCtx?.dayBaseVlm || "0",
714
+ high24h: null,
715
+ low24h: null,
716
+ volume24h: assetCtx?.dayNtlVlm || "0",
683
717
  };
684
718
  }
685
- catch (error) {
686
- throw error;
719
+ const [[spotMeta, spotCtxs]] = await Promise.all([
720
+ fetchSpotMetaAndAssetCtxs(network),
721
+ ]);
722
+ const spotIndex = spotMeta?.tokens?.findIndex((token) => token.name === coin);
723
+ if (spotIndex === undefined || spotIndex === -1) {
724
+ throw new Error(`Coin ${coin} not found in Hyperliquid markets`);
725
+ }
726
+ const spotCtx = spotCtxs[spotIndex];
727
+ const currentPrice = allMids[coin] || spotCtx?.midPx || spotCtx?.markPx || "0";
728
+ let change24h = "0.00";
729
+ if (spotCtx?.prevDayPx && spotCtx?.midPx) {
730
+ const prev = parseFloat(spotCtx.prevDayPx);
731
+ const current = parseFloat(spotCtx.midPx);
732
+ if (prev > 0) {
733
+ change24h = (((current - prev) / prev) * 100).toFixed(2);
734
+ }
687
735
  }
736
+ return {
737
+ price: currentPrice,
738
+ change24h,
739
+ high24h: null,
740
+ low24h: null,
741
+ volume24h: spotCtx?.dayBaseVlm || "0",
742
+ };
688
743
  }
689
744
  /**
690
- * Fetches user leverage mode for a specific coin
745
+ * Fetches user leverage mode for a specific coin.
746
+ * @throws Error if the API call fails - callers should implement retry logic.
747
+ * @returns The leverage mode, or null if the user has no position for this coin.
691
748
  */
692
749
  export async function fetchUserLeverageMode(userAddress, coin, network = "testnet") {
693
750
  try {
@@ -705,8 +762,12 @@ export async function fetchUserLeverageMode(userAddress, coin, network = "testne
705
762
  }
706
763
  const isCross = pos?.isCross ?? entry?.isCross ?? true;
707
764
  let leverage;
708
- if (pos?.leverage != null) {
709
- leverage = Number(pos.leverage);
765
+ const posLeverage = pos?.leverage;
766
+ if (posLeverage != null) {
767
+ leverage =
768
+ typeof posLeverage === "number"
769
+ ? posLeverage
770
+ : Number(posLeverage.value ?? 1);
710
771
  }
711
772
  else if (pos?.szi &&
712
773
  pos?.entryPx &&
@@ -722,7 +783,11 @@ export async function fetchUserLeverageMode(userAddress, coin, network = "testne
722
783
  return null;
723
784
  }
724
785
  catch (error) {
725
- return null;
786
+ const maskedAddress = userAddress.length > 10
787
+ ? `${userAddress.slice(0, 6)}...${userAddress.slice(-4)}`
788
+ : "***";
789
+ console.debug(`[fetchUserLeverageMode] Error fetching leverage mode for coin "${coin}" and user "${maskedAddress}":`, error);
790
+ throw error;
726
791
  }
727
792
  }
728
793
  /**
@@ -730,28 +795,23 @@ export async function fetchUserLeverageMode(userAddress, coin, network = "testne
730
795
  */
731
796
  export async function fetchTopOfBook(symbol, network = "testnet") {
732
797
  const { coin } = parseSymbolForBook(symbol);
733
- try {
734
- const client = getHyperliquidInfoClient(network);
735
- const book = (await client.l2Book({ coin }));
736
- const levels = Array.isArray(book?.levels)
737
- ? book.levels
738
- : [];
739
- const [bids = [], asks = []] = levels;
740
- const bestBid = getLevelPrice(bids[0]);
741
- const bestAsk = getLevelPrice(asks[0]);
742
- const midPrice = bestBid !== null && bestAsk !== null
743
- ? (bestBid + bestAsk) / 2
744
- : bestBid ?? bestAsk ?? null;
745
- return {
746
- bestBid,
747
- bestAsk,
748
- midPrice,
749
- lastUpdated: Date.now(),
750
- };
751
- }
752
- catch (error) {
753
- throw error;
754
- }
798
+ const client = getHyperliquidInfoClient(network);
799
+ const book = (await client.l2Book({ coin }));
800
+ const levels = Array.isArray(book?.levels)
801
+ ? book.levels
802
+ : [];
803
+ const [bids = [], asks = []] = levels;
804
+ const bestBid = getLevelPrice(bids[0]);
805
+ const bestAsk = getLevelPrice(asks[0]);
806
+ const midPrice = bestBid !== null && bestAsk !== null
807
+ ? (bestBid + bestAsk) / 2
808
+ : bestBid ?? bestAsk ?? null;
809
+ return {
810
+ bestBid,
811
+ bestAsk,
812
+ midPrice,
813
+ lastUpdated: Date.now(),
814
+ };
755
815
  }
756
816
  // ============================================================================
757
817
  // User-Specific API Functions
@@ -760,133 +820,83 @@ export async function fetchTopOfBook(symbol, network = "testnet") {
760
820
  * Fetches user positions from clearinghouse state
761
821
  */
762
822
  export async function fetchUserPositions(userAddress, network = "testnet") {
763
- try {
764
- const client = getHyperliquidInfoClient(network);
765
- const state = await client.clearinghouseState({
766
- user: userAddress,
767
- });
768
- if (!state?.assetPositions || !Array.isArray(state.assetPositions)) {
769
- return [];
770
- }
771
- return state.assetPositions
772
- .filter((entry) => {
773
- const pos = entry?.position;
774
- if (!pos)
775
- return false;
776
- const size = parseFloat(pos.szi || "0");
777
- return size !== 0;
778
- })
779
- .map((entry) => {
780
- const pos = entry.position;
781
- return {
782
- coin: pos.coin,
783
- szi: pos.szi,
784
- entryPx: pos.entryPx || "0",
785
- positionValue: pos.positionValue || "0",
786
- unrealizedPnl: pos.unrealizedPnl || "0",
787
- returnOnEquity: pos.returnOnEquity || "0",
788
- liquidationPx: pos.liquidationPx || null,
789
- marginUsed: pos.marginUsed || "0",
790
- leverage: entry.leverage?.value || pos.leverage?.value || 1,
791
- isCross: pos.isCross ?? entry.isCross ?? true,
792
- };
793
- });
794
- }
795
- catch (error) {
796
- throw error;
823
+ const client = getHyperliquidInfoClient(network);
824
+ const state = await client.clearinghouseState({
825
+ user: userAddress,
826
+ });
827
+ if (!state?.assetPositions || !Array.isArray(state.assetPositions)) {
828
+ return [];
797
829
  }
830
+ return state.assetPositions
831
+ .filter(filterNonZeroPositions)
832
+ .map(mapRawAssetPositionToPosition);
798
833
  }
799
834
  /**
800
835
  * Fetches user's clearinghouse state (positions + margin summary)
801
836
  */
802
837
  export async function fetchUserState(userAddress, network = "testnet") {
803
- try {
804
- const client = getHyperliquidInfoClient(network);
805
- const state = await client.clearinghouseState({
806
- user: userAddress,
807
- });
808
- if (!state) {
809
- return null;
810
- }
811
- const positions = (state.assetPositions || [])
812
- .filter((entry) => {
813
- const pos = entry?.position;
814
- if (!pos)
815
- return false;
816
- const size = parseFloat(pos.szi || "0");
817
- return size !== 0;
818
- })
819
- .map((entry) => {
820
- const pos = entry.position;
821
- return {
822
- coin: pos.coin,
823
- szi: pos.szi,
824
- entryPx: pos.entryPx || "0",
825
- positionValue: pos.positionValue || "0",
826
- unrealizedPnl: pos.unrealizedPnl || "0",
827
- returnOnEquity: pos.returnOnEquity || "0",
828
- liquidationPx: pos.liquidationPx || null,
829
- marginUsed: pos.marginUsed || "0",
830
- leverage: entry.leverage?.value || pos.leverage?.value || 1,
831
- isCross: pos.isCross ?? entry.isCross ?? true,
832
- };
833
- });
834
- const marginSummary = state.marginSummary || {};
835
- const crossMarginSummary = state.crossMarginSummary;
836
- return {
837
- assetPositions: positions,
838
- marginSummary: {
839
- accountValue: marginSummary.accountValue || "0",
840
- totalNtlPos: marginSummary.totalNtlPos || "0",
841
- totalRawUsd: marginSummary.totalRawUsd || "0",
842
- totalMarginUsed: marginSummary.totalMarginUsed || "0",
843
- withdrawable: marginSummary.withdrawable || "0",
844
- },
845
- crossMarginSummary: crossMarginSummary
846
- ? {
847
- accountValue: crossMarginSummary.accountValue || "0",
848
- totalNtlPos: crossMarginSummary.totalNtlPos || "0",
849
- totalRawUsd: crossMarginSummary.totalRawUsd || "0",
850
- totalMarginUsed: crossMarginSummary.totalMarginUsed || "0",
851
- withdrawable: crossMarginSummary.withdrawable || "0",
852
- }
853
- : undefined,
854
- };
855
- }
856
- catch (error) {
857
- throw error;
838
+ const client = getHyperliquidInfoClient(network);
839
+ const state = await client.clearinghouseState({
840
+ user: userAddress,
841
+ });
842
+ if (!state) {
843
+ return null;
858
844
  }
845
+ const positions = (state.assetPositions || [])
846
+ .filter(filterNonZeroPositions)
847
+ .map(mapRawAssetPositionToPosition);
848
+ const marginSummary = state.marginSummary || {};
849
+ const crossMarginSummary = state.crossMarginSummary;
850
+ const crossMaintenanceMarginUsed = state.crossMaintenanceMarginUsed;
851
+ return {
852
+ assetPositions: positions,
853
+ marginSummary: {
854
+ accountValue: marginSummary.accountValue || "0",
855
+ totalNtlPos: marginSummary.totalNtlPos || "0",
856
+ totalRawUsd: marginSummary.totalRawUsd || "0",
857
+ totalMarginUsed: marginSummary.totalMarginUsed || "0",
858
+ withdrawable: marginSummary.withdrawable || "0",
859
+ },
860
+ crossMarginSummary: crossMarginSummary
861
+ ? {
862
+ accountValue: crossMarginSummary.accountValue || "0",
863
+ totalNtlPos: crossMarginSummary.totalNtlPos || "0",
864
+ totalRawUsd: crossMarginSummary.totalRawUsd || "0",
865
+ totalMarginUsed: crossMarginSummary.totalMarginUsed || "0",
866
+ withdrawable: crossMarginSummary.withdrawable || "0",
867
+ }
868
+ : undefined,
869
+ crossMaintenanceMarginUsed: crossMaintenanceMarginUsed || "0",
870
+ };
859
871
  }
860
872
  /**
861
- * Fetches user's open orders
873
+ * Fetches user's open orders with frontend info (includes trigger prices for TP/SL)
862
874
  */
863
875
  export async function fetchOpenOrders(userAddress, network = "testnet") {
864
- try {
865
- const client = getHyperliquidInfoClient(network);
866
- const orders = await client.openOrders({
867
- user: userAddress,
868
- });
869
- if (!Array.isArray(orders)) {
870
- return [];
871
- }
872
- return orders.map((order) => ({
873
- coin: order.coin,
874
- oid: order.oid,
875
- side: order.side,
876
- limitPx: order.limitPx,
877
- sz: order.sz,
878
- origSz: order.origSz,
879
- timestamp: order.timestamp,
880
- reduceOnly: order.reduceOnly ?? false,
881
- orderType: order.orderType || "limit",
882
- triggerPx: order.triggerPx || undefined,
883
- isPositionTpsl: order.isPositionTpsl || false,
884
- cloid: order.cloid || undefined,
885
- }));
886
- }
887
- catch (error) {
888
- throw error;
876
+ const client = getHyperliquidInfoClient(network);
877
+ // Use frontendOpenOrders to get complete order info including triggerPx for TP/SL orders
878
+ const orders = await client.frontendOpenOrders({
879
+ user: userAddress,
880
+ });
881
+ if (!Array.isArray(orders)) {
882
+ return [];
889
883
  }
884
+ return orders.map((order) => ({
885
+ coin: String(order.coin ?? ""),
886
+ oid: order.oid ?? 0,
887
+ side: order.side === "B" || order.side === "A" ? order.side : "B",
888
+ limitPx: String(order.limitPx ?? "0"),
889
+ sz: String(order.sz ?? "0"),
890
+ origSz: String(order.origSz ?? order.sz ?? "0"),
891
+ timestamp: order.timestamp ?? Date.now(),
892
+ reduceOnly: order.reduceOnly ?? false,
893
+ orderType: order.orderType || "Limit",
894
+ triggerPx: order.triggerPx || undefined,
895
+ isTrigger: order.isTrigger ?? false,
896
+ triggerCondition: order.triggerCondition || "",
897
+ isPositionTpsl: order.isPositionTpsl || false,
898
+ cloid: order.cloid || undefined,
899
+ }));
890
900
  }
891
901
  // ============================================================================
892
902
  // Utility Functions
@@ -911,4 +921,114 @@ export async function resolveAssetIndex(coin, network = "testnet") {
911
921
  }
912
922
  return assetId;
913
923
  }
924
+ /**
925
+ * Fetches user fee rates from Hyperliquid
926
+ * Returns taker/maker rates for perps and spot
927
+ */
928
+ export async function fetchUserFees(params) {
929
+ const { userAddress, network = "testnet" } = params;
930
+ if (!userAddress) {
931
+ throw new Error("User address is required to fetch fees");
932
+ }
933
+ const client = getHyperliquidInfoClient(network);
934
+ const response = await client.userFees({
935
+ user: userAddress,
936
+ });
937
+ return {
938
+ userCrossRate: response.userCrossRate || "0",
939
+ userAddRate: response.userAddRate || "0",
940
+ userSpotCrossRate: response.userSpotCrossRate || "0",
941
+ userSpotAddRate: response.userSpotAddRate || "0",
942
+ activeReferralDiscount: response.activeReferralDiscount || "0",
943
+ };
944
+ }
945
+ /**
946
+ * Fetches user historical orders from Hyperliquid
947
+ * Returns orders with their current status (open, filled, canceled, etc.)
948
+ */
949
+ export async function fetchHistoricalOrders(params) {
950
+ const { user, limit = 200, network = "testnet" } = params;
951
+ if (!user) {
952
+ throw new Error("User address is required to fetch historical orders");
953
+ }
954
+ const transport = getHyperliquidTransport(network);
955
+ const response = await transport.request("info", {
956
+ type: "historicalOrders",
957
+ user,
958
+ });
959
+ if (!Array.isArray(response)) {
960
+ return [];
961
+ }
962
+ return response.slice(0, limit).map((item) => {
963
+ const order = item.order ?? {};
964
+ return {
965
+ order: {
966
+ coin: String(order.coin ?? ""),
967
+ side: order.side === "B" || order.side === "A" ? order.side : "B",
968
+ limitPx: String(order.limitPx ?? "0"),
969
+ sz: String(order.sz ?? "0"),
970
+ oid: typeof order.oid === "number" ? order.oid : 0,
971
+ timestamp: typeof order.timestamp === "number" ? order.timestamp : Date.now(),
972
+ origSz: String(order.origSz ?? order.sz ?? "0"),
973
+ triggerCondition: order.triggerCondition
974
+ ? String(order.triggerCondition)
975
+ : undefined,
976
+ isTrigger: typeof order.isTrigger === "boolean" ? order.isTrigger : false,
977
+ triggerPx: order.triggerPx ? String(order.triggerPx) : undefined,
978
+ children: Array.isArray(order.children) ? order.children : [],
979
+ isPositionTpsl: typeof order.isPositionTpsl === "boolean"
980
+ ? order.isPositionTpsl
981
+ : false,
982
+ reduceOnly: typeof order.reduceOnly === "boolean" ? order.reduceOnly : false,
983
+ orderType: String(order.orderType ?? "Limit"),
984
+ tif: String(order.tif ?? "Gtc"),
985
+ cloid: order.cloid ? String(order.cloid) : undefined,
986
+ },
987
+ status: (item.status ?? "filled"),
988
+ statusTimestamp: typeof item.statusTimestamp === "number"
989
+ ? item.statusTimestamp
990
+ : Date.now(),
991
+ };
992
+ });
993
+ }
994
+ /**
995
+ * Fetches user frontend open orders from Hyperliquid
996
+ * Returns open orders with additional display information (trigger conditions, TP/SL, etc.)
997
+ */
998
+ export async function fetchFrontendOpenOrders(params) {
999
+ const { user, network = "testnet" } = params;
1000
+ if (!user) {
1001
+ throw new Error("User address is required to fetch open orders");
1002
+ }
1003
+ const transport = getHyperliquidTransport(network);
1004
+ const response = await transport.request("info", {
1005
+ type: "frontendOpenOrders",
1006
+ user,
1007
+ });
1008
+ if (!Array.isArray(response)) {
1009
+ return [];
1010
+ }
1011
+ return response.map((order) => ({
1012
+ coin: String(order.coin ?? ""),
1013
+ side: order.side === "B" || order.side === "A" ? order.side : "B",
1014
+ limitPx: String(order.limitPx ?? "0"),
1015
+ sz: String(order.sz ?? "0"),
1016
+ oid: typeof order.oid === "number" ? order.oid : 0,
1017
+ timestamp: typeof order.timestamp === "number" ? order.timestamp : Date.now(),
1018
+ origSz: String(order.origSz ?? order.sz ?? "0"),
1019
+ triggerCondition: order.triggerCondition
1020
+ ? String(order.triggerCondition)
1021
+ : undefined,
1022
+ isTrigger: typeof order.isTrigger === "boolean" ? order.isTrigger : false,
1023
+ triggerPx: order.triggerPx ? String(order.triggerPx) : undefined,
1024
+ children: (Array.isArray(order.children)
1025
+ ? order.children
1026
+ : []),
1027
+ isPositionTpsl: typeof order.isPositionTpsl === "boolean" ? order.isPositionTpsl : false,
1028
+ reduceOnly: typeof order.reduceOnly === "boolean" ? order.reduceOnly : false,
1029
+ orderType: String(order.orderType ?? "Limit"),
1030
+ tif: String(order.tif ?? "Gtc"),
1031
+ cloid: order.cloid ? String(order.cloid) : undefined,
1032
+ }));
1033
+ }
914
1034
  //# sourceMappingURL=api.js.map