perp-cli 0.4.2 → 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 (68) 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/cli-spec.d.ts +0 -4
  6. package/dist/cli-spec.js +8 -1
  7. package/dist/commands/agent.js +7 -4
  8. package/dist/commands/arb-auto.js +643 -4
  9. package/dist/commands/dex.js +7 -1
  10. package/dist/commands/funding.d.ts +2 -1
  11. package/dist/commands/funding.js +211 -4
  12. package/dist/commands/manage.js +1 -1
  13. package/dist/commands/market.js +1 -1
  14. package/dist/commands/multileg.d.ts +16 -0
  15. package/dist/commands/multileg.js +174 -0
  16. package/dist/commands/pnl.d.ts +12 -0
  17. package/dist/commands/pnl.js +243 -0
  18. package/dist/commands/stream.js +132 -55
  19. package/dist/commands/trade.js +29 -8
  20. package/dist/commands/wallet.js +1 -1
  21. package/dist/config.d.ts +5 -0
  22. package/dist/config.js +12 -0
  23. package/dist/dashboard/ws-feeds.js +1 -1
  24. package/dist/equity-tracker.d.ts +57 -0
  25. package/dist/equity-tracker.js +209 -0
  26. package/dist/exchanges/hyperliquid-spot.d.ts +68 -0
  27. package/dist/exchanges/hyperliquid-spot.js +317 -0
  28. package/dist/exchanges/hyperliquid.d.ts +26 -5
  29. package/dist/exchanges/hyperliquid.js +171 -73
  30. package/dist/exchanges/interface.d.ts +1 -1
  31. package/dist/exchanges/lighter-spot.d.ts +55 -0
  32. package/dist/exchanges/lighter-spot.js +417 -0
  33. package/dist/exchanges/lighter.d.ts +10 -2
  34. package/dist/exchanges/lighter.js +128 -35
  35. package/dist/exchanges/pacifica.d.ts +11 -4
  36. package/dist/exchanges/pacifica.js +52 -37
  37. package/dist/exchanges/spot-interface.d.ts +45 -0
  38. package/dist/exchanges/spot-interface.js +22 -0
  39. package/dist/execution-log.d.ts +2 -2
  40. package/dist/funding/rates.d.ts +36 -0
  41. package/dist/funding/rates.js +146 -0
  42. package/dist/index.js +147 -98
  43. package/dist/liquidity.d.ts +12 -0
  44. package/dist/liquidity.js +43 -0
  45. package/dist/signer/evm-local.d.ts +12 -0
  46. package/dist/signer/evm-local.js +19 -0
  47. package/dist/signer/index.d.ts +3 -0
  48. package/dist/signer/index.js +2 -0
  49. package/dist/signer/interface.d.ts +31 -0
  50. package/dist/signer/interface.js +7 -0
  51. package/dist/signer/solana-local.d.ts +13 -0
  52. package/dist/signer/solana-local.js +19 -0
  53. package/dist/smart-order.js +14 -3
  54. package/dist/utils.js +7 -1
  55. package/dist/ws/market-feed.d.ts +74 -0
  56. package/dist/ws/market-feed.js +227 -0
  57. package/package.json +1 -1
  58. package/skills/perp-cli/SKILL.md +93 -4
  59. package/skills/perp-cli/references/commands.md +16 -1
  60. package/skills/perp-cli/references/strategies.md +1 -1
  61. package/skills/perp-cli/scripts/arb-monitor.sh +74 -0
  62. package/skills/perp-cli/scripts/funding-analysis.sh +85 -0
  63. package/skills/perp-cli/scripts/preflight.sh +98 -0
  64. package/skills/perp-cli/scripts/spot-perp-scan.sh +83 -0
  65. package/skills/perp-cli/scripts/validate-arb.sh +112 -0
  66. package/skills/perp-cli/templates/arb-scan.sh +0 -15
  67. package/skills/perp-cli/templates/open-position.sh +0 -38
  68. 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,18 +425,53 @@ export class HyperliquidAdapter {
405
425
  }
406
426
  return result;
407
427
  }
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
454
+ if (this._dex) {
455
+ action.dex = this._dex;
456
+ }
457
+ return this._signAndSendAction(action);
458
+ }
408
459
  async limitOrder(symbol, side, price, size, opts) {
409
460
  // Normalize TIF: accept "IOC"/"GTC"/"ALO" (uppercase) or native "Ioc"/"Gtc"/"Alo"
410
461
  const rawTif = opts?.tif ?? "Gtc";
411
462
  const tifMap = { IOC: "Ioc", GTC: "Gtc", ALO: "Alo" };
412
463
  const tif = (tifMap[rawTif.toUpperCase()] ?? rawTif);
413
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);
414
469
  // Use _rawPlaceOrder for both DEX and non-DEX (SDK exchange.placeOrder is unavailable)
415
470
  const result = await this._rawPlaceOrder({
416
- assetIndex: this.getAssetIndex(symbol.toUpperCase()),
471
+ assetIndex: await this.getAssetIndex(symbol.toUpperCase()),
417
472
  isBuy: side === "buy",
418
- price,
419
- size,
473
+ price: roundedPrice,
474
+ size: roundedSize,
420
475
  orderType: { limit: { tif } },
421
476
  reduceOnly,
422
477
  });
@@ -424,17 +479,24 @@ export class HyperliquidAdapter {
424
479
  return result;
425
480
  }
426
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);
427
485
  const result = await this.sdk.exchange.cancelOrder({
428
- coin: symbol.toUpperCase(),
486
+ coin: resolved,
429
487
  o: parseInt(orderId),
430
488
  });
431
489
  await this._invalidateAccountCache();
432
490
  return result;
433
491
  }
434
492
  async cancelAllOrders(symbol) {
493
+ this.ensureSigner();
435
494
  const orders = await this.getOpenOrders();
436
495
  const toCancel = symbol
437
- ? 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
+ })
438
500
  : orders;
439
501
  const results = [];
440
502
  for (const o of toCancel) {
@@ -447,7 +509,11 @@ export class HyperliquidAdapter {
447
509
  }
448
510
  // ── Interface methods ──
449
511
  async editOrder(symbol, orderId, price, size) {
450
- 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);
451
517
  }
452
518
  async setLeverage(symbol, leverage, marginMode = "cross") {
453
519
  return this.updateLeverage(symbol, leverage, marginMode === "cross");
@@ -484,11 +550,13 @@ export class HyperliquidAdapter {
484
550
  }));
485
551
  }
486
552
  async getOrderHistory(limit = 30) {
553
+ this.ensureAddress();
487
554
  const fills = await this.client.info.getUserFills(this._address);
488
555
  return (fills ?? []).slice(0, limit).map((f) => ({
489
556
  orderId: String(f.oid ?? ""),
490
557
  symbol: String(f.coin ?? ""),
491
- 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,
492
560
  price: String(f.px ?? "0"),
493
561
  size: String(f.sz ?? ""),
494
562
  filled: String(f.sz ?? ""),
@@ -497,17 +565,20 @@ export class HyperliquidAdapter {
497
565
  }));
498
566
  }
499
567
  async getTradeHistory(limit = 30) {
568
+ this.ensureAddress();
500
569
  const fills = await this.client.info.getUserFills(this._address);
501
570
  return (fills ?? []).slice(0, limit).map((f) => ({
502
571
  time: Number(f.time ?? 0),
503
572
  symbol: String(f.coin ?? ""),
504
- 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,
505
575
  price: String(f.px ?? "0"),
506
576
  size: String(f.sz ?? ""),
507
577
  fee: String(f.fee ?? "0"),
508
578
  }));
509
579
  }
510
580
  async getFundingPayments(limit = 30) {
581
+ this.ensureAddress();
511
582
  const now = Date.now();
512
583
  const history = await this.client.info.perpetuals.getUserFunding(this._address, now - 7 * 24 * 60 * 60 * 1000);
513
584
  return (history ?? []).slice(0, limit).map((h) => {
@@ -525,7 +596,7 @@ export class HyperliquidAdapter {
525
596
  return (history ?? []).slice(-limit).map((h) => ({
526
597
  time: Number(h.time ?? 0),
527
598
  rate: String(h.fundingRate ?? "0"),
528
- price: "-",
599
+ price: null,
529
600
  }));
530
601
  }
531
602
  // ────────────────────────────────────────────────────────────
@@ -536,6 +607,7 @@ export class HyperliquidAdapter {
536
607
  * Python SDK: order() with trigger type {"trigger": {triggerPx, isMarket, tpsl: "tp"|"sl"}}
537
608
  */
538
609
  async triggerOrder(symbol, side, size, triggerPrice, tpsl, opts) {
610
+ this.ensureSigner();
539
611
  const orderParams = {
540
612
  coin: symbol.toUpperCase(),
541
613
  is_buy: side === "buy",
@@ -559,7 +631,7 @@ export class HyperliquidAdapter {
559
631
  * nktkas TS SDK: twapOrder({twap: {a, b, s, r, m, t}})
560
632
  */
561
633
  async twapOrder(symbol, side, size, durationMinutes, opts) {
562
- const assetIndex = this.getAssetIndex(symbol);
634
+ const assetIndex = await this.getAssetIndex(symbol);
563
635
  const action = {
564
636
  type: "twapOrder",
565
637
  twap: {
@@ -577,7 +649,7 @@ export class HyperliquidAdapter {
577
649
  * Cancel a TWAP order.
578
650
  */
579
651
  async twapCancel(symbol, twapId) {
580
- const assetIndex = this.getAssetIndex(symbol);
652
+ const assetIndex = await this.getAssetIndex(symbol);
581
653
  const action = {
582
654
  type: "twapCancel",
583
655
  a: assetIndex,
@@ -590,6 +662,7 @@ export class HyperliquidAdapter {
590
662
  * Uses SDK's built-in updateLeverage method.
591
663
  */
592
664
  async updateLeverage(symbol, leverage, isCross = true) {
665
+ this.ensureSigner();
593
666
  return this.sdk.exchange.updateLeverage(this.resolveSymbol(symbol), isCross ? "cross" : "isolated", leverage);
594
667
  }
595
668
  /**
@@ -597,6 +670,7 @@ export class HyperliquidAdapter {
597
670
  * amount > 0 to add margin, amount < 0 to remove
598
671
  */
599
672
  async updateIsolatedMargin(symbol, amount) {
673
+ this.ensureSigner();
600
674
  return this.sdk.exchange.updateIsolatedMargin(this.resolveSymbol(symbol), amount > 0, Math.round(Math.abs(amount) * 1e6));
601
675
  }
602
676
  /**
@@ -604,6 +678,7 @@ export class HyperliquidAdapter {
604
678
  * Python SDK: withdraw_from_bridge(amount, destination) → action type "withdraw3"
605
679
  */
606
680
  async withdraw(amount, destination) {
681
+ this.ensureSigner();
607
682
  try {
608
683
  return await this.sdk.exchange.initiateWithdrawal(destination, parseFloat(amount));
609
684
  }
@@ -625,6 +700,7 @@ export class HyperliquidAdapter {
625
700
  * Python SDK: usd_transfer(amount, destination)
626
701
  */
627
702
  async usdTransfer(amount, destination) {
703
+ this.ensureSigner();
628
704
  return this.sdk.exchange.usdTransfer(destination, amount);
629
705
  }
630
706
  /**
@@ -653,7 +729,15 @@ export class HyperliquidAdapter {
653
729
  * Modify an existing order.
654
730
  */
655
731
  async modifyOrder(symbol, orderId, newSide, newPrice, newSize, opts) {
656
- 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
+ };
657
741
  const action = {
658
742
  type: "batchModify",
659
743
  modifies: [{
@@ -661,14 +745,14 @@ export class HyperliquidAdapter {
661
745
  order: {
662
746
  a: assetIndex,
663
747
  b: newSide === "buy",
664
- p: newPrice,
665
- s: newSize,
748
+ p: trimZeros(this._hlRoundPrice(Number(newPrice), szDec)),
749
+ s: trimZeros(Number(newSize).toFixed(szDec)),
666
750
  r: opts?.reduceOnly ?? false,
667
751
  t: { limit: { tif: "Gtc" } },
668
752
  },
669
753
  }],
670
754
  };
671
- return this._sendExchangeAction(action);
755
+ return this._signAndSendAction(action);
672
756
  }
673
757
  /**
674
758
  * Schedule cancel: cancel all orders at a future time.
@@ -722,6 +806,7 @@ export class HyperliquidAdapter {
722
806
  * Get user trade fills.
723
807
  */
724
808
  async getUserFills(startTime, endTime) {
809
+ this.ensureAddress();
725
810
  const baseUrl = this._testnet
726
811
  ? "https://api.hyperliquid-testnet.xyz"
727
812
  : "https://api.hyperliquid.xyz";
@@ -744,6 +829,7 @@ export class HyperliquidAdapter {
744
829
  * Get user portfolio analytics.
745
830
  */
746
831
  async getPortfolio() {
832
+ this.ensureAddress();
747
833
  const baseUrl = this._testnet
748
834
  ? "https://api.hyperliquid-testnet.xyz"
749
835
  : "https://api.hyperliquid.xyz";
@@ -758,6 +844,7 @@ export class HyperliquidAdapter {
758
844
  * Query a specific order by OID.
759
845
  */
760
846
  async queryOrder(orderId) {
847
+ this.ensureAddress();
761
848
  const baseUrl = this._testnet
762
849
  ? "https://api.hyperliquid-testnet.xyz"
763
850
  : "https://api.hyperliquid.xyz";
@@ -847,42 +934,49 @@ export class HyperliquidAdapter {
847
934
  * Get referral info.
848
935
  */
849
936
  async getReferralInfo() {
937
+ this.ensureAddress();
850
938
  return this._infoPost({ type: "referral", user: this._address });
851
939
  }
852
940
  /**
853
941
  * Get fee info.
854
942
  */
855
943
  async getUserFees() {
944
+ this.ensureAddress();
856
945
  return this._infoPost({ type: "userFees", user: this._address });
857
946
  }
858
947
  /**
859
948
  * Get sub-accounts.
860
949
  */
861
950
  async getSubAccounts() {
951
+ this.ensureAddress();
862
952
  return this._infoPost({ type: "subAccounts", user: this._address });
863
953
  }
864
954
  /**
865
955
  * Get historical orders (up to 2000).
866
956
  */
867
957
  async getHistoricalOrders() {
958
+ this.ensureAddress();
868
959
  return this._infoPost({ type: "historicalOrders", user: this._address });
869
960
  }
870
961
  /**
871
962
  * Get approved builders.
872
963
  */
873
964
  async getApprovedBuilders() {
965
+ this.ensureAddress();
874
966
  return this._infoPost({ type: "approvedBuilders", user: this._address });
875
967
  }
876
968
  /**
877
969
  * Get vault details.
878
970
  */
879
971
  async getVaultDetails(vaultAddress) {
972
+ this.ensureAddress();
880
973
  return this._infoPost({ type: "vaultDetails", vaultAddress, user: this._address });
881
974
  }
882
975
  /**
883
976
  * Get delegations (staking).
884
977
  */
885
978
  async getDelegations() {
979
+ this.ensureAddress();
886
980
  return this._infoPost({ type: "delegations", user: this._address });
887
981
  }
888
982
  /**
@@ -899,20 +993,24 @@ export class HyperliquidAdapter {
899
993
  });
900
994
  return res.json();
901
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
+ }
902
1003
  /**
903
1004
  * Send a raw exchange action through the SDK.
904
1005
  * Used for methods not directly exposed by the SDK.
905
1006
  */
906
1007
  async _sendExchangeAction(action) {
907
1008
  const exchange = this.sdk.exchange;
908
- // Try using the SDK's internal _postAction if available
1009
+ // Try using the SDK's internal postAction if available
909
1010
  if (typeof exchange.postAction === "function") {
910
1011
  return exchange.postAction(action);
911
1012
  }
912
- // Fallback: direct POST to /exchange
913
- // This requires signing which the SDK normally handles internally
914
- // If we get here, the SDK doesn't expose the method we need
915
- throw new Error(`Hyperliquid SDK does not expose postAction(). ` +
916
- `Action "${action.type}" may need a newer SDK version.`);
1013
+ // Fallback: sign and send directly (bypasses SDK)
1014
+ return this._signAndSendAction(action);
917
1015
  }
918
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
+ }