perp-cli 0.4.0 → 0.4.8

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 (79) hide show
  1. package/README.md +2 -0
  2. package/dist/arb/sizing.d.ts +12 -1
  3. package/dist/arb/sizing.js +38 -5
  4. package/dist/arb/state.d.ts +6 -0
  5. package/dist/arb/utils.d.ts +2 -0
  6. package/dist/arb/utils.js +1 -1
  7. package/dist/cli-spec.d.ts +0 -4
  8. package/dist/cli-spec.js +8 -1
  9. package/dist/commands/agent.js +12 -9
  10. package/dist/commands/arb/index.js +18 -3
  11. package/dist/commands/arb-auto.js +885 -67
  12. package/dist/commands/dex.js +7 -1
  13. package/dist/commands/funding.d.ts +2 -1
  14. package/dist/commands/funding.js +211 -4
  15. package/dist/commands/manage.js +1 -1
  16. package/dist/commands/market.js +1 -1
  17. package/dist/commands/multileg.d.ts +16 -0
  18. package/dist/commands/multileg.js +174 -0
  19. package/dist/commands/pnl.d.ts +12 -0
  20. package/dist/commands/pnl.js +243 -0
  21. package/dist/commands/settings.d.ts +2 -1
  22. package/dist/commands/settings.js +133 -2
  23. package/dist/commands/stream.js +132 -55
  24. package/dist/commands/trade.js +118 -43
  25. package/dist/commands/wallet.js +1 -1
  26. package/dist/config.d.ts +5 -0
  27. package/dist/config.js +12 -0
  28. package/dist/constants.d.ts +14 -0
  29. package/dist/constants.js +40 -0
  30. package/dist/dashboard/ws-feeds.js +1 -1
  31. package/dist/equity-tracker.d.ts +57 -0
  32. package/dist/equity-tracker.js +209 -0
  33. package/dist/exchanges/hyperliquid-spot.d.ts +68 -0
  34. package/dist/exchanges/hyperliquid-spot.js +317 -0
  35. package/dist/exchanges/hyperliquid.d.ts +26 -5
  36. package/dist/exchanges/hyperliquid.js +181 -92
  37. package/dist/exchanges/interface.d.ts +1 -1
  38. package/dist/exchanges/lighter-spot.d.ts +55 -0
  39. package/dist/exchanges/lighter-spot.js +417 -0
  40. package/dist/exchanges/lighter.d.ts +10 -2
  41. package/dist/exchanges/lighter.js +132 -37
  42. package/dist/exchanges/pacifica.d.ts +11 -4
  43. package/dist/exchanges/pacifica.js +52 -37
  44. package/dist/exchanges/spot-interface.d.ts +45 -0
  45. package/dist/exchanges/spot-interface.js +22 -0
  46. package/dist/execution-log.d.ts +2 -2
  47. package/dist/funding/rates.d.ts +36 -0
  48. package/dist/funding/rates.js +146 -0
  49. package/dist/index.js +148 -99
  50. package/dist/liquidity.d.ts +12 -0
  51. package/dist/liquidity.js +43 -0
  52. package/dist/settings.d.ts +6 -0
  53. package/dist/settings.js +18 -2
  54. package/dist/signer/evm-local.d.ts +12 -0
  55. package/dist/signer/evm-local.js +19 -0
  56. package/dist/signer/index.d.ts +3 -0
  57. package/dist/signer/index.js +2 -0
  58. package/dist/signer/interface.d.ts +31 -0
  59. package/dist/signer/interface.js +7 -0
  60. package/dist/signer/solana-local.d.ts +13 -0
  61. package/dist/signer/solana-local.js +19 -0
  62. package/dist/smart-order.d.ts +44 -0
  63. package/dist/smart-order.js +145 -0
  64. package/dist/trade-validator.js +2 -1
  65. package/dist/utils.js +7 -1
  66. package/dist/ws/market-feed.d.ts +74 -0
  67. package/dist/ws/market-feed.js +227 -0
  68. package/package.json +1 -1
  69. package/skills/perp-cli/SKILL.md +102 -4
  70. package/skills/perp-cli/references/commands.md +16 -1
  71. package/skills/perp-cli/references/strategies.md +1 -1
  72. package/skills/perp-cli/scripts/arb-monitor.sh +74 -0
  73. package/skills/perp-cli/scripts/funding-analysis.sh +85 -0
  74. package/skills/perp-cli/scripts/preflight.sh +98 -0
  75. package/skills/perp-cli/scripts/spot-perp-scan.sh +83 -0
  76. package/skills/perp-cli/scripts/validate-arb.sh +112 -0
  77. package/skills/perp-cli/templates/arb-scan.sh +0 -15
  78. package/skills/perp-cli/templates/open-position.sh +0 -38
  79. package/skills/perp-cli/templates/wallet-setup.sh +0 -17
@@ -1,23 +1,30 @@
1
1
  import { Hyperliquid } from "hyperliquid";
2
+ import { LocalEvmSigner } from "../signer/evm-local.js";
2
3
  export class HyperliquidAdapter {
3
4
  name = "hyperliquid";
4
5
  sdk;
5
6
  _address;
6
7
  _privateKey;
7
8
  _testnet;
9
+ _evmSigner;
8
10
  _assetMap = new Map();
9
11
  _assetMapReverse = new Map();
12
+ _szDecimalsMap = new Map(); // symbol → szDecimals
10
13
  /** HIP-3 deployed perp dex name. Empty string = native (validator) perps. */
11
14
  _dex = "";
12
15
  // In-memory cache removed — using file-based cache (src/cache.ts) for cross-process dedup
13
16
  constructor(privateKey, testnet = false) {
17
+ // Disable WebSocket in SDK — we use REST for all info calls and raw
18
+ // fetch for /exchange. This avoids the false "Please install ws" warning
19
+ // caused by the SDK's require('ws') failing in ESM context.
14
20
  this.sdk = new Hyperliquid({
15
21
  privateKey,
16
22
  testnet,
17
23
  walletAddress: undefined,
24
+ enableWs: false,
18
25
  });
19
26
  this._address = "";
20
- this._privateKey = privateKey;
27
+ this._privateKey = privateKey ?? "";
21
28
  this._testnet = testnet;
22
29
  }
23
30
  /** Set the HIP-3 deployed perp dex to query/trade on. */
@@ -39,6 +46,10 @@ export class HyperliquidAdapter {
39
46
  get isTestnet() {
40
47
  return this._testnet;
41
48
  }
49
+ /** Inject an external EVM signer. Call before init() to skip LocalEvmSigner creation. */
50
+ setSigner(signer) {
51
+ this._evmSigner = signer;
52
+ }
42
53
  async init() {
43
54
  // Suppress "WebSocket connected" noise from hyperliquid SDK
44
55
  const origLog = console.log;
@@ -49,14 +60,10 @@ export class HyperliquidAdapter {
49
60
  finally {
50
61
  console.log = origLog;
51
62
  }
52
- // Derive address from SDK or from private key directly
53
- const wallet = this.sdk.wallet;
54
- if (wallet && typeof wallet === "object" && "address" in wallet) {
55
- this._address = String(wallet.address);
56
- }
57
- if (!this._address) {
58
- const { ethers } = await import("ethers");
59
- this._address = new ethers.Wallet(this._privateKey).address;
63
+ // Initialize EVM signer if not externally injected (skip if no key — read-only mode)
64
+ if (!this._evmSigner && this._privateKey) {
65
+ this._evmSigner = await LocalEvmSigner.create(this._privateKey);
66
+ this._address = this._evmSigner.getAddress();
60
67
  }
61
68
  // Build asset index map
62
69
  await this._loadAssetMap();
@@ -85,12 +92,15 @@ export class HyperliquidAdapter {
85
92
  meta.universe.forEach((asset, idx) => {
86
93
  this._assetMap.set(asset.name, idx);
87
94
  this._assetMapReverse.set(idx, asset.name);
95
+ if (asset.szDecimals !== undefined) {
96
+ this._szDecimalsMap.set(asset.name, asset.szDecimals);
97
+ }
88
98
  });
89
99
  }
90
100
  }
91
101
  }
92
- catch {
93
- // non-critical
102
+ catch (e) {
103
+ console.error("[hyperliquid] Failed to load asset map:", e instanceof Error ? e.message : e);
94
104
  }
95
105
  }
96
106
  /**
@@ -115,7 +125,24 @@ export class HyperliquidAdapter {
115
125
  }
116
126
  return sym; // return as-is, let downstream error
117
127
  }
118
- getAssetIndex(symbol) {
128
+ /** Get szDecimals for a symbol (cached from init/getMeta) */
129
+ getSzDecimals(symbol) {
130
+ const resolved = this.resolveSymbol(symbol.toUpperCase());
131
+ return this._szDecimalsMap.get(resolved) ?? 2; // 2 is safe middle ground
132
+ }
133
+ /** HL official price rounding: 5 significant figures */
134
+ _hlRoundPrice(px, szDecimals, maxDecimals = 6) {
135
+ if (px > 100_000)
136
+ return String(Math.round(px));
137
+ const sig5 = Number(px.toPrecision(5));
138
+ const priceDec = Math.max(0, maxDecimals - szDecimals);
139
+ return Number(sig5.toFixed(priceDec)).toString();
140
+ }
141
+ async getAssetIndex(symbol) {
142
+ // Retry once if asset map is empty (e.g. init failed silently)
143
+ if (this._assetMap.size === 0) {
144
+ await this._loadAssetMap();
145
+ }
119
146
  const resolved = this.resolveSymbol(symbol);
120
147
  const idx = this._assetMap.get(resolved);
121
148
  if (idx !== undefined)
@@ -177,8 +204,14 @@ export class HyperliquidAdapter {
177
204
  ]),
178
205
  };
179
206
  }
207
+ ensureAddress() {
208
+ if (!this._address) {
209
+ throw new Error("No private key configured — account data unavailable. Run: perp init");
210
+ }
211
+ }
180
212
  /** Always fetches live clearinghouseState, writes result to shared cache for dashboard */
181
213
  async _getClearinghouseState() {
214
+ this.ensureAddress();
182
215
  const { fetchAndCache, TTL_ACCOUNT } = await import("../cache.js");
183
216
  const key = this._dex ? `acct:hl:chs:${this._address}:${this._dex}` : `acct:hl:chs:${this._address}`;
184
217
  return fetchAndCache(key, TTL_ACCOUNT, async () => {
@@ -210,10 +243,10 @@ export class HyperliquidAdapter {
210
243
  const spotState = await this.sdk.info.spot.getSpotClearinghouseState(this._address);
211
244
  const balances = spotState?.balances ?? [];
212
245
  const usdc = balances.find((b) => String(b.coin).startsWith("USDC"));
213
- const spotTotal = Number(usdc?.total ?? 0);
246
+ const spotTotal = usdc?.total !== undefined ? Number(usdc.total) : NaN;
214
247
  const spotHold = Number(usdc?.hold ?? 0);
215
- equity = spotTotal > 0 ? spotTotal : Number(margin.accountValue ?? cross.accountValue ?? 0);
216
- available = spotTotal > 0 ? spotTotal - spotHold : Number(s?.withdrawable ?? 0);
248
+ equity = !isNaN(spotTotal) ? spotTotal : Number(margin.accountValue ?? cross.accountValue ?? 0);
249
+ available = !isNaN(spotTotal) ? spotTotal - spotHold : Number(s?.withdrawable ?? 0);
217
250
  }
218
251
  catch {
219
252
  // Spot API failed — fall back to perp-only values
@@ -249,7 +282,7 @@ export class HyperliquidAdapter {
249
282
  side: szi > 0 ? "long" : "short",
250
283
  size: String(Math.abs(szi)),
251
284
  entryPrice: String(pos.entryPx ?? "0"),
252
- markPrice: String(pos.positionValue ? (Number(pos.positionValue) / Math.abs(szi)).toFixed(2) : "0"),
285
+ markPrice: String(pos.positionValue ? Number(pos.positionValue) / Math.abs(szi) : "0"),
253
286
  liquidationPrice: String(pos.liquidationPx ?? "N/A"),
254
287
  unrealizedPnl: String(pos.unrealizedPnl ?? "0"),
255
288
  leverage: Number(pos.leverage?.value ?? 1),
@@ -257,11 +290,13 @@ export class HyperliquidAdapter {
257
290
  });
258
291
  }
259
292
  async getOpenOrders() {
293
+ this.ensureAddress();
260
294
  const orders = await this.sdk.info.getUserOpenOrders(this._address);
261
295
  return (orders ?? []).map((o) => ({
262
296
  orderId: String(o.oid ?? ""),
263
297
  symbol: String(o.coin ?? ""),
264
- side: o.side === "B" ? "buy" : "sell",
298
+ // SDK convertSymbolsInObject already converts "B"→"buy", "A"→"sell"
299
+ side: String(o.side) === "B" ? "buy" : String(o.side) === "A" ? "sell" : o.side,
265
300
  price: String(o.limitPx ?? "0"),
266
301
  size: String(o.sz ?? "0"),
267
302
  filled: "0",
@@ -277,6 +312,7 @@ export class HyperliquidAdapter {
277
312
  catch { /* ignore */ }
278
313
  }
279
314
  async marketOrder(symbol, side, size) {
315
+ this.ensureSigner();
280
316
  if (this._dex) {
281
317
  const r = await this._dexMarketOrder(symbol, side, size);
282
318
  await this._invalidateAccountCache();
@@ -300,8 +336,8 @@ export class HyperliquidAdapter {
300
336
  * and constructs + signs the order action directly.
301
337
  */
302
338
  async _dexMarketOrder(symbol, side, size) {
303
- const assetIndex = this.getAssetIndex(symbol.toUpperCase());
304
- // Get current mark price from dex meta
339
+ const assetIndex = await this.getAssetIndex(symbol.toUpperCase());
340
+ // Get current mark price + szDecimals from dex meta
305
341
  const meta = await this._infoPost({
306
342
  type: "metaAndAssetCtxs",
307
343
  dex: this._dex,
@@ -310,57 +346,41 @@ export class HyperliquidAdapter {
310
346
  const midPrice = Number(ctx?.markPx ?? 0);
311
347
  if (midPrice <= 0)
312
348
  throw new Error(`Cannot get price for ${symbol} on dex ${this._dex}`);
313
- // Apply 5% slippage (same as SDK default)
349
+ const szDec = meta[0]?.universe?.[assetIndex]?.szDecimals ?? 2;
350
+ // HL official rounding: 5 significant figures, max 6 decimals for perps
351
+ const MAX_DECIMALS_PERP = 6;
314
352
  const slippage = 0.05;
315
353
  const isBuy = side === "buy";
316
354
  const slippagePrice = isBuy ? midPrice * (1 + slippage) : midPrice * (1 - slippage);
317
- const decimals = midPrice.toString().split(".")[1]?.length || 0;
318
- const limitPrice = slippagePrice.toFixed(Math.max(0, decimals - 1));
355
+ const limitPrice = slippagePrice > 100_000
356
+ ? String(Math.round(slippagePrice))
357
+ : Number(Number(slippagePrice.toPrecision(5)).toFixed(Math.max(0, MAX_DECIMALS_PERP - szDec))).toString();
319
358
  return this._rawPlaceOrder({
320
359
  assetIndex,
321
360
  isBuy,
322
361
  price: limitPrice,
323
- size,
362
+ size: Number(size).toFixed(szDec),
324
363
  orderType: { limit: { tif: "Ioc" } },
325
364
  reduceOnly: false,
326
365
  });
327
366
  }
328
367
  /**
329
- * Place an order via raw exchange API with EIP-712 signing.
330
- * This bypasses the SDK's symbolConversion and supports dex-specific orders.
368
+ * Sign and send any exchange action via raw EIP-712 signing.
369
+ * Bypasses the SDK entirely — works for order, batchModify, twapOrder, etc.
331
370
  */
332
- async _rawPlaceOrder(opts) {
371
+ ensureSigner() {
372
+ if (!this._evmSigner) {
373
+ throw new Error("No private key configured. Run: perp init");
374
+ }
375
+ }
376
+ async _signAndSendAction(action) {
377
+ this.ensureSigner();
333
378
  const { encode } = await import("@msgpack/msgpack");
334
379
  const { ethers, keccak256 } = await import("ethers");
335
- const wallet = new ethers.Wallet(this._privateKey);
336
380
  const isMainnet = !this._testnet;
337
381
  const baseUrl = isMainnet
338
382
  ? "https://api.hyperliquid.xyz"
339
383
  : "https://api.hyperliquid-testnet.xyz";
340
- // Remove trailing zeros (HL API requirement)
341
- const trimZeros = (s) => {
342
- if (!s.includes("."))
343
- return s;
344
- const n = s.replace(/\.?0+$/, "");
345
- return n === "-0" ? "0" : n;
346
- };
347
- const orderWire = {
348
- a: opts.assetIndex,
349
- b: opts.isBuy,
350
- p: trimZeros(opts.price),
351
- s: trimZeros(opts.size),
352
- r: opts.reduceOnly,
353
- t: opts.orderType,
354
- };
355
- const action = {
356
- type: "order",
357
- orders: [orderWire],
358
- grouping: "na",
359
- };
360
- // Add dex field for HIP-3 deployed perps
361
- if (this._dex) {
362
- action.dex = this._dex;
363
- }
364
384
  // Sign L1 action (replicates SDK's signL1Action)
365
385
  const nonce = Date.now();
366
386
  const msgPackBytes = encode(action);
@@ -386,7 +406,7 @@ export class HyperliquidAdapter {
386
406
  source: isMainnet ? "a" : "b",
387
407
  connectionId: hash,
388
408
  };
389
- const sig = await wallet.signTypedData(phantomDomain, agentTypes, phantomAgent);
409
+ const sig = await this._evmSigner.signTypedData(phantomDomain, agentTypes, phantomAgent);
390
410
  const parsed = ethers.Signature.from(sig);
391
411
  const payload = {
392
412
  action,
@@ -405,45 +425,78 @@ export class HyperliquidAdapter {
405
425
  }
406
426
  return result;
407
427
  }
408
- async limitOrder(symbol, side, price, size, opts) {
409
- const tif = (opts?.tif ?? "Gtc");
410
- const reduceOnly = opts?.reduceOnly ?? false;
411
- let result;
428
+ /**
429
+ * Place an order via raw exchange API with EIP-712 signing.
430
+ * This bypasses the SDK's symbolConversion and supports dex-specific orders.
431
+ */
432
+ async _rawPlaceOrder(opts) {
433
+ // Remove trailing zeros (HL API requirement)
434
+ const trimZeros = (s) => {
435
+ if (!s.includes("."))
436
+ return s;
437
+ const n = s.replace(/\.?0+$/, "");
438
+ return n === "-0" ? "0" : n;
439
+ };
440
+ const orderWire = {
441
+ a: opts.assetIndex,
442
+ b: opts.isBuy,
443
+ p: trimZeros(opts.price),
444
+ s: trimZeros(opts.size),
445
+ r: opts.reduceOnly,
446
+ t: opts.orderType,
447
+ };
448
+ const action = {
449
+ type: "order",
450
+ orders: [orderWire],
451
+ grouping: "na",
452
+ };
453
+ // Add dex field for HIP-3 deployed perps
412
454
  if (this._dex) {
413
- result = await this._rawPlaceOrder({
414
- assetIndex: this.getAssetIndex(symbol.toUpperCase()),
415
- isBuy: side === "buy",
416
- price,
417
- size,
418
- orderType: { limit: { tif } },
419
- reduceOnly,
420
- });
421
- }
422
- else {
423
- result = await this.sdk.exchange.placeOrder({
424
- coin: symbol.toUpperCase(),
425
- is_buy: side === "buy",
426
- sz: parseFloat(size),
427
- limit_px: parseFloat(price),
428
- order_type: { limit: { tif } },
429
- reduce_only: reduceOnly,
430
- });
455
+ action.dex = this._dex;
431
456
  }
457
+ return this._signAndSendAction(action);
458
+ }
459
+ async limitOrder(symbol, side, price, size, opts) {
460
+ // Normalize TIF: accept "IOC"/"GTC"/"ALO" (uppercase) or native "Ioc"/"Gtc"/"Alo"
461
+ const rawTif = opts?.tif ?? "Gtc";
462
+ const tifMap = { IOC: "Ioc", GTC: "Gtc", ALO: "Alo" };
463
+ const tif = (tifMap[rawTif.toUpperCase()] ?? rawTif);
464
+ const reduceOnly = opts?.reduceOnly ?? false;
465
+ // Round price/size to HL-supported decimals
466
+ const szDec = this.getSzDecimals(symbol);
467
+ const roundedPrice = this._hlRoundPrice(Number(price), szDec);
468
+ const roundedSize = Number(size).toFixed(szDec);
469
+ // Use _rawPlaceOrder for both DEX and non-DEX (SDK exchange.placeOrder is unavailable)
470
+ const result = await this._rawPlaceOrder({
471
+ assetIndex: await this.getAssetIndex(symbol.toUpperCase()),
472
+ isBuy: side === "buy",
473
+ price: roundedPrice,
474
+ size: roundedSize,
475
+ orderType: { limit: { tif } },
476
+ reduceOnly,
477
+ });
432
478
  await this._invalidateAccountCache();
433
479
  return result;
434
480
  }
435
481
  async cancelOrder(symbol, orderId) {
482
+ this.ensureSigner();
483
+ // HL SDK expects coin in "ETH-PERP" format (internal symbol convention)
484
+ const resolved = this.resolveSymbol(symbol);
436
485
  const result = await this.sdk.exchange.cancelOrder({
437
- coin: symbol.toUpperCase(),
486
+ coin: resolved,
438
487
  o: parseInt(orderId),
439
488
  });
440
489
  await this._invalidateAccountCache();
441
490
  return result;
442
491
  }
443
492
  async cancelAllOrders(symbol) {
493
+ this.ensureSigner();
444
494
  const orders = await this.getOpenOrders();
445
495
  const toCancel = symbol
446
- ? orders.filter((o) => o.symbol.toUpperCase() === symbol.toUpperCase())
496
+ ? orders.filter((o) => {
497
+ const norm = (s) => s.toUpperCase().replace(/-PERP$/, "");
498
+ return norm(o.symbol) === norm(symbol);
499
+ })
447
500
  : orders;
448
501
  const results = [];
449
502
  for (const o of toCancel) {
@@ -456,7 +509,11 @@ export class HyperliquidAdapter {
456
509
  }
457
510
  // ── Interface methods ──
458
511
  async editOrder(symbol, orderId, price, size) {
459
- return this.modifyOrder(symbol, parseInt(orderId), "buy", price, size);
512
+ // Look up the existing order's side to preserve it (avoid flipping buy↔sell)
513
+ const openOrders = await this.getOpenOrders();
514
+ const existing = openOrders.find(o => o.orderId === orderId);
515
+ const side = existing?.side ?? "buy";
516
+ return this.modifyOrder(symbol, parseInt(orderId), side, price, size);
460
517
  }
461
518
  async setLeverage(symbol, leverage, marginMode = "cross") {
462
519
  return this.updateLeverage(symbol, leverage, marginMode === "cross");
@@ -493,11 +550,13 @@ export class HyperliquidAdapter {
493
550
  }));
494
551
  }
495
552
  async getOrderHistory(limit = 30) {
553
+ this.ensureAddress();
496
554
  const fills = await this.client.info.getUserFills(this._address);
497
555
  return (fills ?? []).slice(0, limit).map((f) => ({
498
556
  orderId: String(f.oid ?? ""),
499
557
  symbol: String(f.coin ?? ""),
500
- side: String(f.side) === "B" ? "buy" : "sell",
558
+ // SDK convertSymbolsInObject already converts "B"→"buy", "A"→"sell"
559
+ side: String(f.side) === "B" ? "buy" : String(f.side) === "A" ? "sell" : f.side,
501
560
  price: String(f.px ?? "0"),
502
561
  size: String(f.sz ?? ""),
503
562
  filled: String(f.sz ?? ""),
@@ -506,17 +565,20 @@ export class HyperliquidAdapter {
506
565
  }));
507
566
  }
508
567
  async getTradeHistory(limit = 30) {
568
+ this.ensureAddress();
509
569
  const fills = await this.client.info.getUserFills(this._address);
510
570
  return (fills ?? []).slice(0, limit).map((f) => ({
511
571
  time: Number(f.time ?? 0),
512
572
  symbol: String(f.coin ?? ""),
513
- side: String(f.side) === "B" ? "buy" : "sell",
573
+ // SDK convertSymbolsInObject already converts "B"→"buy", "A"→"sell"
574
+ side: String(f.side) === "B" ? "buy" : String(f.side) === "A" ? "sell" : f.side,
514
575
  price: String(f.px ?? "0"),
515
576
  size: String(f.sz ?? ""),
516
577
  fee: String(f.fee ?? "0"),
517
578
  }));
518
579
  }
519
580
  async getFundingPayments(limit = 30) {
581
+ this.ensureAddress();
520
582
  const now = Date.now();
521
583
  const history = await this.client.info.perpetuals.getUserFunding(this._address, now - 7 * 24 * 60 * 60 * 1000);
522
584
  return (history ?? []).slice(0, limit).map((h) => {
@@ -534,7 +596,7 @@ export class HyperliquidAdapter {
534
596
  return (history ?? []).slice(-limit).map((h) => ({
535
597
  time: Number(h.time ?? 0),
536
598
  rate: String(h.fundingRate ?? "0"),
537
- price: "-",
599
+ price: null,
538
600
  }));
539
601
  }
540
602
  // ────────────────────────────────────────────────────────────
@@ -545,6 +607,7 @@ export class HyperliquidAdapter {
545
607
  * Python SDK: order() with trigger type {"trigger": {triggerPx, isMarket, tpsl: "tp"|"sl"}}
546
608
  */
547
609
  async triggerOrder(symbol, side, size, triggerPrice, tpsl, opts) {
610
+ this.ensureSigner();
548
611
  const orderParams = {
549
612
  coin: symbol.toUpperCase(),
550
613
  is_buy: side === "buy",
@@ -568,7 +631,7 @@ export class HyperliquidAdapter {
568
631
  * nktkas TS SDK: twapOrder({twap: {a, b, s, r, m, t}})
569
632
  */
570
633
  async twapOrder(symbol, side, size, durationMinutes, opts) {
571
- const assetIndex = this.getAssetIndex(symbol);
634
+ const assetIndex = await this.getAssetIndex(symbol);
572
635
  const action = {
573
636
  type: "twapOrder",
574
637
  twap: {
@@ -586,7 +649,7 @@ export class HyperliquidAdapter {
586
649
  * Cancel a TWAP order.
587
650
  */
588
651
  async twapCancel(symbol, twapId) {
589
- const assetIndex = this.getAssetIndex(symbol);
652
+ const assetIndex = await this.getAssetIndex(symbol);
590
653
  const action = {
591
654
  type: "twapCancel",
592
655
  a: assetIndex,
@@ -599,6 +662,7 @@ export class HyperliquidAdapter {
599
662
  * Uses SDK's built-in updateLeverage method.
600
663
  */
601
664
  async updateLeverage(symbol, leverage, isCross = true) {
665
+ this.ensureSigner();
602
666
  return this.sdk.exchange.updateLeverage(this.resolveSymbol(symbol), isCross ? "cross" : "isolated", leverage);
603
667
  }
604
668
  /**
@@ -606,6 +670,7 @@ export class HyperliquidAdapter {
606
670
  * amount > 0 to add margin, amount < 0 to remove
607
671
  */
608
672
  async updateIsolatedMargin(symbol, amount) {
673
+ this.ensureSigner();
609
674
  return this.sdk.exchange.updateIsolatedMargin(this.resolveSymbol(symbol), amount > 0, Math.round(Math.abs(amount) * 1e6));
610
675
  }
611
676
  /**
@@ -613,6 +678,7 @@ export class HyperliquidAdapter {
613
678
  * Python SDK: withdraw_from_bridge(amount, destination) → action type "withdraw3"
614
679
  */
615
680
  async withdraw(amount, destination) {
681
+ this.ensureSigner();
616
682
  try {
617
683
  return await this.sdk.exchange.initiateWithdrawal(destination, parseFloat(amount));
618
684
  }
@@ -634,6 +700,7 @@ export class HyperliquidAdapter {
634
700
  * Python SDK: usd_transfer(amount, destination)
635
701
  */
636
702
  async usdTransfer(amount, destination) {
703
+ this.ensureSigner();
637
704
  return this.sdk.exchange.usdTransfer(destination, amount);
638
705
  }
639
706
  /**
@@ -662,7 +729,15 @@ export class HyperliquidAdapter {
662
729
  * Modify an existing order.
663
730
  */
664
731
  async modifyOrder(symbol, orderId, newSide, newPrice, newSize, opts) {
665
- const assetIndex = this.getAssetIndex(symbol);
732
+ const assetIndex = await this.getAssetIndex(symbol);
733
+ const szDec = this.getSzDecimals(symbol);
734
+ // Remove trailing zeros (HL API requirement)
735
+ const trimZeros = (s) => {
736
+ if (!s.includes("."))
737
+ return s;
738
+ const n = s.replace(/\.?0+$/, "");
739
+ return n === "-0" ? "0" : n;
740
+ };
666
741
  const action = {
667
742
  type: "batchModify",
668
743
  modifies: [{
@@ -670,14 +745,14 @@ export class HyperliquidAdapter {
670
745
  order: {
671
746
  a: assetIndex,
672
747
  b: newSide === "buy",
673
- p: newPrice,
674
- s: newSize,
748
+ p: trimZeros(this._hlRoundPrice(Number(newPrice), szDec)),
749
+ s: trimZeros(Number(newSize).toFixed(szDec)),
675
750
  r: opts?.reduceOnly ?? false,
676
751
  t: { limit: { tif: "Gtc" } },
677
752
  },
678
753
  }],
679
754
  };
680
- return this._sendExchangeAction(action);
755
+ return this._signAndSendAction(action);
681
756
  }
682
757
  /**
683
758
  * Schedule cancel: cancel all orders at a future time.
@@ -731,6 +806,7 @@ export class HyperliquidAdapter {
731
806
  * Get user trade fills.
732
807
  */
733
808
  async getUserFills(startTime, endTime) {
809
+ this.ensureAddress();
734
810
  const baseUrl = this._testnet
735
811
  ? "https://api.hyperliquid-testnet.xyz"
736
812
  : "https://api.hyperliquid.xyz";
@@ -753,6 +829,7 @@ export class HyperliquidAdapter {
753
829
  * Get user portfolio analytics.
754
830
  */
755
831
  async getPortfolio() {
832
+ this.ensureAddress();
756
833
  const baseUrl = this._testnet
757
834
  ? "https://api.hyperliquid-testnet.xyz"
758
835
  : "https://api.hyperliquid.xyz";
@@ -767,6 +844,7 @@ export class HyperliquidAdapter {
767
844
  * Query a specific order by OID.
768
845
  */
769
846
  async queryOrder(orderId) {
847
+ this.ensureAddress();
770
848
  const baseUrl = this._testnet
771
849
  ? "https://api.hyperliquid-testnet.xyz"
772
850
  : "https://api.hyperliquid.xyz";
@@ -856,42 +934,49 @@ export class HyperliquidAdapter {
856
934
  * Get referral info.
857
935
  */
858
936
  async getReferralInfo() {
937
+ this.ensureAddress();
859
938
  return this._infoPost({ type: "referral", user: this._address });
860
939
  }
861
940
  /**
862
941
  * Get fee info.
863
942
  */
864
943
  async getUserFees() {
944
+ this.ensureAddress();
865
945
  return this._infoPost({ type: "userFees", user: this._address });
866
946
  }
867
947
  /**
868
948
  * Get sub-accounts.
869
949
  */
870
950
  async getSubAccounts() {
951
+ this.ensureAddress();
871
952
  return this._infoPost({ type: "subAccounts", user: this._address });
872
953
  }
873
954
  /**
874
955
  * Get historical orders (up to 2000).
875
956
  */
876
957
  async getHistoricalOrders() {
958
+ this.ensureAddress();
877
959
  return this._infoPost({ type: "historicalOrders", user: this._address });
878
960
  }
879
961
  /**
880
962
  * Get approved builders.
881
963
  */
882
964
  async getApprovedBuilders() {
965
+ this.ensureAddress();
883
966
  return this._infoPost({ type: "approvedBuilders", user: this._address });
884
967
  }
885
968
  /**
886
969
  * Get vault details.
887
970
  */
888
971
  async getVaultDetails(vaultAddress) {
972
+ this.ensureAddress();
889
973
  return this._infoPost({ type: "vaultDetails", vaultAddress, user: this._address });
890
974
  }
891
975
  /**
892
976
  * Get delegations (staking).
893
977
  */
894
978
  async getDelegations() {
979
+ this.ensureAddress();
895
980
  return this._infoPost({ type: "delegations", user: this._address });
896
981
  }
897
982
  /**
@@ -908,20 +993,24 @@ export class HyperliquidAdapter {
908
993
  });
909
994
  return res.json();
910
995
  }
996
+ /**
997
+ * Public entry point for sending signed exchange actions.
998
+ * Used by HyperliquidSpotAdapter to delegate signing.
999
+ */
1000
+ async exchangeAction(action) {
1001
+ return this._signAndSendAction(action);
1002
+ }
911
1003
  /**
912
1004
  * Send a raw exchange action through the SDK.
913
1005
  * Used for methods not directly exposed by the SDK.
914
1006
  */
915
1007
  async _sendExchangeAction(action) {
916
1008
  const exchange = this.sdk.exchange;
917
- // Try using the SDK's internal _postAction if available
1009
+ // Try using the SDK's internal postAction if available
918
1010
  if (typeof exchange.postAction === "function") {
919
1011
  return exchange.postAction(action);
920
1012
  }
921
- // Fallback: direct POST to /exchange
922
- // This requires signing which the SDK normally handles internally
923
- // If we get here, the SDK doesn't expose the method we need
924
- throw new Error(`Hyperliquid SDK does not expose postAction(). ` +
925
- `Action "${action.type}" may need a newer SDK version.`);
1013
+ // Fallback: sign and send directly (bypasses SDK)
1014
+ return this._signAndSendAction(action);
926
1015
  }
927
1016
  }
@@ -70,7 +70,7 @@ export interface ExchangeAdapter {
70
70
  getFundingHistory(symbol: string, limit?: number): Promise<{
71
71
  time: number;
72
72
  rate: string;
73
- price: string;
73
+ price: string | null;
74
74
  }[]>;
75
75
  getKlines(symbol: string, interval: string, startTime: number, endTime: number): Promise<ExchangeKline[]>;
76
76
  getBalance(): Promise<ExchangeBalance>;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Lighter Spot Adapter.
3
+ *
4
+ * Uses the same `/api/v1/sendTx` endpoint as perps — the only difference
5
+ * is the market_id (spot markets have different IDs from perp markets).
6
+ *
7
+ * Composes with LighterAdapter for signing and API access.
8
+ */
9
+ import type { SpotAdapter, SpotMarketInfo, SpotBalance } from "./spot-interface.js";
10
+ import type { LighterAdapter } from "./lighter.js";
11
+ export declare class LighterSpotAdapter implements SpotAdapter {
12
+ readonly name = "lighter";
13
+ private _lt;
14
+ private _spotMarketMap;
15
+ private _spotBaseMap;
16
+ private _spotDecimals;
17
+ private _spotMinSize;
18
+ private _initialized;
19
+ constructor(ltAdapter: LighterAdapter);
20
+ init(): Promise<void>;
21
+ private _loadSpotMarkets;
22
+ /** Resolve a symbol (e.g., "ETH" or "ETH/USDC") to the full spot symbol */
23
+ private resolveSymbol;
24
+ getSpotMarkets(): Promise<SpotMarketInfo[]>;
25
+ getSpotOrderbook(symbol: string): Promise<{
26
+ bids: [string, string][];
27
+ asks: [string, string][];
28
+ }>;
29
+ getSpotBalances(): Promise<SpotBalance[]>;
30
+ spotMarketOrder(symbol: string, side: "buy" | "sell", size: string): Promise<unknown>;
31
+ spotLimitOrder(symbol: string, side: "buy" | "sell", price: string, size: string, opts?: {
32
+ tif?: string;
33
+ }): Promise<unknown>;
34
+ spotCancelOrder(symbol: string, orderId: string): Promise<unknown>;
35
+ /** Transfer USDC from perp account to spot account (required before spot buys) */
36
+ transferUsdcToSpot(amount: number): Promise<unknown>;
37
+ /** Transfer USDC from spot account back to perp account */
38
+ transferUsdcToPerp(amount: number): Promise<unknown>;
39
+ /**
40
+ * Internal self-transfer using raw WASM signTransfer.
41
+ * TS SDK's is_spot_account mapping is buggy (sets both fromRoute and toRoute
42
+ * to the same value), so we call the WASM module directly with explicit routes.
43
+ * Route types: 0 = Perp, 1 = Spot
44
+ */
45
+ private _selfTransfer;
46
+ /** Get the spot market ID for a symbol */
47
+ getSpotMarketId(symbol: string): number | undefined;
48
+ /** Check if a base token has a spot market */
49
+ hasSpotMarket(base: string): boolean;
50
+ private _placeOrder;
51
+ private _getNextNonce;
52
+ private _sendTx;
53
+ private _restGet;
54
+ private _restGetAuth;
55
+ }