perp-cli 0.4.2 → 0.4.9

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 (89) hide show
  1. package/README.md +46 -5
  2. package/dist/arb/reconcile.d.ts +28 -0
  3. package/dist/arb/reconcile.js +162 -0
  4. package/dist/arb/sizing.d.ts +12 -1
  5. package/dist/arb/sizing.js +38 -5
  6. package/dist/arb/state.d.ts +6 -0
  7. package/dist/cli-spec.d.ts +0 -4
  8. package/dist/cli-spec.js +8 -1
  9. package/dist/commands/account.d.ts +1 -1
  10. package/dist/commands/account.js +300 -63
  11. package/dist/commands/agent.js +16 -9
  12. package/dist/commands/arb/index.js +243 -49
  13. package/dist/commands/arb-auto.js +696 -9
  14. package/dist/commands/arb.d.ts +2 -1
  15. package/dist/commands/arb.js +535 -131
  16. package/dist/commands/bot.js +207 -0
  17. package/dist/commands/dex.js +7 -1
  18. package/dist/commands/funding.d.ts +2 -1
  19. package/dist/commands/funding.js +211 -4
  20. package/dist/commands/funds.d.ts +4 -0
  21. package/dist/commands/funds.js +741 -0
  22. package/dist/commands/history.d.ts +2 -1
  23. package/dist/commands/history.js +508 -5
  24. package/dist/commands/manage.js +6 -4
  25. package/dist/commands/market.js +35 -2
  26. package/dist/commands/multileg.d.ts +16 -0
  27. package/dist/commands/multileg.js +174 -0
  28. package/dist/commands/pnl.d.ts +12 -0
  29. package/dist/commands/pnl.js +243 -0
  30. package/dist/commands/risk.d.ts +2 -0
  31. package/dist/commands/risk.js +56 -0
  32. package/dist/commands/settings.js +147 -0
  33. package/dist/commands/stream.js +132 -55
  34. package/dist/commands/trade.d.ts +1 -1
  35. package/dist/commands/trade.js +270 -267
  36. package/dist/commands/wallet.d.ts +4 -0
  37. package/dist/commands/wallet.js +11 -1
  38. package/dist/config.d.ts +6 -1
  39. package/dist/config.js +21 -1
  40. package/dist/dashboard/ws-feeds.js +1 -1
  41. package/dist/equity-tracker.d.ts +57 -0
  42. package/dist/equity-tracker.js +209 -0
  43. package/dist/exchanges/hyperliquid-spot.d.ts +69 -0
  44. package/dist/exchanges/hyperliquid-spot.js +309 -0
  45. package/dist/exchanges/hyperliquid.d.ts +26 -5
  46. package/dist/exchanges/hyperliquid.js +171 -73
  47. package/dist/exchanges/interface.d.ts +1 -1
  48. package/dist/exchanges/lighter-spot.d.ts +55 -0
  49. package/dist/exchanges/lighter-spot.js +415 -0
  50. package/dist/exchanges/lighter.d.ts +10 -2
  51. package/dist/exchanges/lighter.js +210 -56
  52. package/dist/exchanges/pacifica.d.ts +11 -4
  53. package/dist/exchanges/pacifica.js +64 -37
  54. package/dist/exchanges/spot-interface.d.ts +45 -0
  55. package/dist/exchanges/spot-interface.js +22 -0
  56. package/dist/execution-log.d.ts +2 -2
  57. package/dist/funding/rates.d.ts +36 -0
  58. package/dist/funding/rates.js +146 -0
  59. package/dist/index.js +126 -161
  60. package/dist/liquidity.d.ts +12 -0
  61. package/dist/liquidity.js +43 -0
  62. package/dist/mcp-server.js +43 -96
  63. package/dist/signer/evm-local.d.ts +12 -0
  64. package/dist/signer/evm-local.js +19 -0
  65. package/dist/signer/index.d.ts +3 -0
  66. package/dist/signer/index.js +2 -0
  67. package/dist/signer/interface.d.ts +31 -0
  68. package/dist/signer/interface.js +7 -0
  69. package/dist/signer/solana-local.d.ts +13 -0
  70. package/dist/signer/solana-local.js +19 -0
  71. package/dist/smart-order.js +14 -3
  72. package/dist/strategies/split-order.d.ts +49 -0
  73. package/dist/strategies/split-order.js +138 -0
  74. package/dist/utils.js +7 -1
  75. package/dist/ws/market-feed.d.ts +74 -0
  76. package/dist/ws/market-feed.js +227 -0
  77. package/package.json +1 -1
  78. package/skills/perp-cli/SKILL.md +80 -4
  79. package/skills/perp-cli/references/agent-operations.md +19 -9
  80. package/skills/perp-cli/references/commands.md +54 -32
  81. package/skills/perp-cli/references/strategies.md +27 -17
  82. package/skills/perp-cli/scripts/arb-monitor.sh +74 -0
  83. package/skills/perp-cli/scripts/funding-analysis.sh +85 -0
  84. package/skills/perp-cli/scripts/preflight.sh +98 -0
  85. package/skills/perp-cli/scripts/spot-perp-scan.sh +83 -0
  86. package/skills/perp-cli/scripts/validate-arb.sh +112 -0
  87. package/skills/perp-cli/templates/arb-scan.sh +0 -15
  88. package/skills/perp-cli/templates/open-position.sh +0 -38
  89. package/skills/perp-cli/templates/wallet-setup.sh +0 -17
@@ -1,3 +1,4 @@
1
+ import { LocalEvmSigner } from "../signer/evm-local.js";
1
2
  export class LighterAdapter {
2
3
  name = "lighter";
3
4
  _signer;
@@ -13,6 +14,7 @@ export class LighterAdapter {
13
14
  _chainId;
14
15
  _testnet;
15
16
  _readOnly;
17
+ _evmSigner;
16
18
  // In-memory cache removed — using file-based cache (src/cache.ts) for cross-process dedup
17
19
  /**
18
20
  * @param evmKey EVM private key (0x-prefixed, 32 bytes) — for deposits & key registration
@@ -23,7 +25,7 @@ export class LighterAdapter {
23
25
  this._evmKey = evmKey;
24
26
  this._apiKey = opts?.apiKey || process.env.LIGHTER_API_KEY || "";
25
27
  this._accountIndexInit = opts?.accountIndex ?? parseInt(process.env.LIGHTER_ACCOUNT_INDEX || "-1");
26
- this._apiKeyIndex = parseInt(process.env.LIGHTER_API_KEY_INDEX || "2");
28
+ this._apiKeyIndex = parseInt(process.env.LIGHTER_API_KEY_INDEX || "4");
27
29
  this._address = "";
28
30
  this._testnet = testnet;
29
31
  this._readOnly = !this._apiKey;
@@ -56,24 +58,49 @@ export class LighterAdapter {
56
58
  get isReadOnly() {
57
59
  return this._readOnly;
58
60
  }
61
+ /** Inject an external EVM signer. Call before init() to skip LocalEvmSigner creation. */
62
+ setSigner(signer) {
63
+ this._evmSigner = signer;
64
+ }
59
65
  async init() {
60
- // Resolve address and account index from EVM key via REST
61
- const { ethers } = await import("ethers");
62
- const wallet = new ethers.Wallet(this._evmKey);
63
- this._address = wallet.address;
64
- // Fetch account index from REST API
65
- const res = await fetch(`${this._baseUrl}/api/v1/account?by=l1_address&value=${this._address}`);
66
- const json = await res.json();
67
- if (json.accounts && json.accounts.length > 0) {
68
- this._accountIndex = this._accountIndexInit >= 0
69
- ? this._accountIndexInit
70
- : json.accounts[0].account_index;
66
+ // Initialize EVM signer if not externally injected (skip if no key — read-only mode)
67
+ if (!this._evmSigner && this._evmKey) {
68
+ this._evmSigner = await LocalEvmSigner.create(this._evmKey);
69
+ this._address = this._evmSigner.getAddress();
70
+ }
71
+ // Fetch account index from REST API (skip if no address — read-only mode)
72
+ if (!this._address) {
73
+ await this._refreshMarketMap();
74
+ return;
75
+ }
76
+ // Retry account lookup on 429 (rate limit) — critical for _accountIndex
77
+ for (let attempt = 0; attempt < 3; attempt++) {
78
+ try {
79
+ const res = await fetch(`${this._baseUrl}/api/v1/account?by=l1_address&value=${this._address}`);
80
+ if (res.status === 429 && attempt < 2) {
81
+ await new Promise(r => setTimeout(r, 1000 * (attempt + 1)));
82
+ continue;
83
+ }
84
+ const json = await res.json();
85
+ if (json.accounts && json.accounts.length > 0) {
86
+ this._accountIndex = this._accountIndexInit >= 0
87
+ ? this._accountIndexInit
88
+ : json.accounts[0].account_index;
89
+ }
90
+ break;
91
+ }
92
+ catch {
93
+ if (attempt < 2) {
94
+ await new Promise(r => setTimeout(r, 1000));
95
+ continue;
96
+ }
97
+ }
71
98
  }
72
99
  // Auto-generate API key if we have PK but no API key and account exists
73
100
  let signerReady = false;
74
101
  if (!this._apiKey && this._accountIndex >= 0) {
75
102
  try {
76
- const autoKeyIndex = 2; // default for auto-setup
103
+ const autoKeyIndex = 4; // default for auto-setup (0-3 reserved by Lighter frontend)
77
104
  const { privateKey: apiKey } = await this.setupApiKey(autoKeyIndex);
78
105
  this._apiKey = apiKey;
79
106
  this._apiKeyIndex = autoKeyIndex;
@@ -108,6 +135,11 @@ export class LighterAdapter {
108
135
  else if (cleanKey.length === 0) {
109
136
  this._apiKey = "";
110
137
  }
138
+ else if (this._accountIndex < 0) {
139
+ // Account lookup failed or returned no accounts — cannot initialize WASM signer
140
+ console.error(`[lighter] Account index not available (got ${this._accountIndex}). Trading will be read-only. Ensure the wallet has a Lighter account or set LIGHTER_ACCOUNT_INDEX.`);
141
+ this._apiKey = "";
142
+ }
111
143
  else {
112
144
  this._apiKey = cleanKey;
113
145
  const client = await LighterAdapter.getWasmClient();
@@ -121,22 +153,59 @@ export class LighterAdapter {
121
153
  this._signer = client;
122
154
  }
123
155
  }
124
- if (this._apiKey) {
125
- this._readOnly = false;
126
- }
156
+ this._readOnly = !this._apiKey;
127
157
  // Build symbol → marketIndex map + decimals from orderBookDetails
128
- try {
129
- const res = await this.restGet("/orderBookDetails", {});
130
- for (const d of res.order_book_details ?? []) {
131
- this._marketMap.set(d.symbol.toUpperCase(), d.market_id);
132
- this._marketDecimals.set(d.symbol.toUpperCase(), {
133
- size: d.size_decimals ?? 0,
134
- price: d.price_decimals ?? 0,
135
- });
158
+ await this._refreshMarketMap();
159
+ }
160
+ /** Populate marketMap + decimals from /orderBooks API (safe to call multiple times) */
161
+ async _refreshMarketMap() {
162
+ // Retry up to 2 times with 1s delay on failure
163
+ for (let attempt = 0; attempt < 2; attempt++) {
164
+ try {
165
+ // Prefer /orderBooks which has supported_size_decimals / supported_price_decimals
166
+ const res = await this.restGet("/orderBooks", {});
167
+ const books = res.order_books ?? [];
168
+ if (books.length === 0 && attempt === 0) {
169
+ // Empty response (possible rate limit) — retry after delay
170
+ await new Promise(r => setTimeout(r, 1000));
171
+ continue;
172
+ }
173
+ for (const d of books) {
174
+ const sym = d.symbol.toUpperCase();
175
+ // Skip spot markets (id ≥ 2048) — those are handled by lighter-spot.ts
176
+ if (d.market_id >= 2048)
177
+ continue;
178
+ this._marketMap.set(sym, d.market_id);
179
+ this._marketDecimals.set(sym, {
180
+ size: d.supported_size_decimals,
181
+ price: d.supported_price_decimals,
182
+ });
183
+ }
184
+ if (this._marketMap.size > 0)
185
+ return; // success
136
186
  }
137
- }
138
- catch {
139
- // Market map will be empty, use fallback
187
+ catch {
188
+ // /orderBooks failed — try fallback below
189
+ }
190
+ // Fallback to /orderBookDetails
191
+ try {
192
+ const res = await this.restGet("/orderBookDetails", {});
193
+ for (const d of res.order_book_details ?? []) {
194
+ this._marketMap.set(d.symbol.toUpperCase(), d.market_id);
195
+ this._marketDecimals.set(d.symbol.toUpperCase(), {
196
+ size: d.size_decimals ?? 0,
197
+ price: d.price_decimals ?? 0,
198
+ });
199
+ }
200
+ if (this._marketMap.size > 0)
201
+ return; // success
202
+ }
203
+ catch {
204
+ // Both APIs failed this attempt
205
+ }
206
+ // Delay before retry
207
+ if (attempt === 0)
208
+ await new Promise(r => setTimeout(r, 1000));
140
209
  }
141
210
  }
142
211
  getMarketIndex(symbol) {
@@ -145,18 +214,32 @@ export class LighterAdapter {
145
214
  throw new Error(`Unknown Lighter market: ${symbol}`);
146
215
  return idx;
147
216
  }
217
+ /** Ensure market map is populated; lazy-refresh if empty */
218
+ async ensureMarketMap() {
219
+ if (this._marketMap.size === 0) {
220
+ await this._refreshMarketMap();
221
+ }
222
+ }
148
223
  async getMarkPrice(symbol) {
149
224
  try {
150
225
  const res = await this.restGet("/orderBookDetails", {});
151
226
  const m = res.order_book_details?.find(d => d.symbol.toUpperCase() === symbol.toUpperCase());
152
- return m?.last_trade_price ?? 0;
227
+ const price = m?.last_trade_price ?? 0;
228
+ if (price <= 0)
229
+ throw new Error(`Cannot determine mark price for ${symbol}`);
230
+ return price;
153
231
  }
154
- catch {
155
- return 0;
232
+ catch (e) {
233
+ if (e instanceof Error && e.message.startsWith("Cannot determine"))
234
+ throw e;
235
+ throw new Error(`Cannot determine mark price for ${symbol}`);
156
236
  }
157
237
  }
158
238
  toTicks(symbol, size, price) {
159
- const dec = this._marketDecimals.get(symbol.toUpperCase()) ?? { size: 0, price: 0 };
239
+ const dec = this._marketDecimals.get(symbol.toUpperCase());
240
+ if (!dec) {
241
+ throw new Error(`No market decimals loaded for ${symbol}. Market data may not be initialized.`);
242
+ }
160
243
  return {
161
244
  baseAmount: Math.round(size * Math.pow(10, dec.size)),
162
245
  priceTicks: Math.round(price * Math.pow(10, dec.price)),
@@ -203,6 +286,7 @@ export class LighterAdapter {
203
286
  }
204
287
  async getOrderbook(symbol) {
205
288
  try {
289
+ await this.ensureMarketMap();
206
290
  const marketId = this.getMarketIndex(symbol);
207
291
  const res = await this.restGet("/orderBookOrders", {
208
292
  market_id: String(marketId),
@@ -218,6 +302,8 @@ export class LighterAdapter {
218
302
  }
219
303
  }
220
304
  async getBalance() {
305
+ if (!this._address)
306
+ throw new Error("No private key configured — account data unavailable. Run: perp init");
221
307
  const acct = await this.fetchAccount();
222
308
  if (!acct)
223
309
  return { equity: "0", available: "0", marginUsed: "0", unrealizedPnl: "0" };
@@ -233,6 +319,8 @@ export class LighterAdapter {
233
319
  };
234
320
  }
235
321
  async getPositions() {
322
+ if (!this._address)
323
+ throw new Error("No private key configured — account data unavailable. Run: perp init");
236
324
  const acct = await this.fetchAccount();
237
325
  if (!acct)
238
326
  return [];
@@ -246,12 +334,12 @@ export class LighterAdapter {
246
334
  size: String(Math.abs(posSize)),
247
335
  entryPrice: String(p.avg_entry_price || "0"),
248
336
  markPrice: String(posSize !== 0
249
- ? (Number(p.position_value || 0) / Math.abs(posSize)).toFixed(4)
337
+ ? (Number(p.position_value || 0) / Math.abs(posSize)).toFixed(this._marketDecimals.get(String(p.symbol || "").toUpperCase())?.price ?? 4)
250
338
  : "0"),
251
339
  liquidationPrice: String(p.liquidation_price || "N/A"),
252
340
  unrealizedPnl: String(p.unrealized_pnl || "0"),
253
341
  leverage: Number(p.initial_margin_fraction || 0) > 0
254
- ? Math.round(100 / Number(p.initial_margin_fraction))
342
+ ? Math.round(10000 / Number(p.initial_margin_fraction))
255
343
  : 1,
256
344
  };
257
345
  });
@@ -268,6 +356,8 @@ export class LighterAdapter {
268
356
  return this.restGet(path, params);
269
357
  }
270
358
  async getOpenOrders() {
359
+ if (!this._address)
360
+ throw new Error("No private key configured — account data unavailable. Run: perp init");
271
361
  if (this._accountIndex < 0 || this._readOnly)
272
362
  return [];
273
363
  try {
@@ -276,13 +366,26 @@ export class LighterAdapter {
276
366
  const totalOrders = Number(acct?.total_order_count ?? 0);
277
367
  if (totalOrders === 0)
278
368
  return [];
279
- // Query each market in parallel (accountActiveOrders requires auth + market_id)
369
+ // Only query markets that have open orders (avoids 156-market fan-out)
370
+ const activeMarketIds = new Set();
371
+ for (const p of acct?.positions ?? []) {
372
+ if (Number(p.open_order_count ?? 0) > 0 || Number(p.pending_order_count ?? 0) > 0) {
373
+ activeMarketIds.add(Number(p.market_id));
374
+ }
375
+ }
376
+ if (activeMarketIds.size === 0)
377
+ return [];
378
+ const reverseMap = new Map();
379
+ for (const [sym, id] of this._marketMap)
380
+ reverseMap.set(id, sym);
280
381
  const allOrders = [];
281
- const entries = Array.from(this._marketMap.entries());
282
- const results = await Promise.allSettled(entries.map(([sym, marketId]) => this.restGetAuth("/accountActiveOrders", {
283
- account_index: String(this._accountIndex),
284
- market_id: String(marketId),
285
- }).then(res => ({ sym, orders: (res.orders ?? []) }))));
382
+ const results = await Promise.allSettled(Array.from(activeMarketIds).map((marketId) => {
383
+ const sym = reverseMap.get(marketId) ?? `Market-${marketId}`;
384
+ return this.restGetAuth("/accountActiveOrders", {
385
+ account_index: String(this._accountIndex),
386
+ market_id: String(marketId),
387
+ }).then(res => ({ sym, orders: (res.orders ?? []) }));
388
+ }));
286
389
  for (const r of results) {
287
390
  if (r.status !== "fulfilled" || !r.value.orders.length)
288
391
  continue;
@@ -307,6 +410,7 @@ export class LighterAdapter {
307
410
  }
308
411
  async marketOrder(symbol, side, size) {
309
412
  this.ensureSigner();
413
+ await this.ensureMarketMap();
310
414
  const nonce = await this.getNextNonce();
311
415
  const marketIndex = this.getMarketIndex(symbol);
312
416
  const { baseAmount } = this.toTicks(symbol, parseFloat(size), 0);
@@ -331,6 +435,7 @@ export class LighterAdapter {
331
435
  }
332
436
  async limitOrder(symbol, side, price, size, opts) {
333
437
  this.ensureSigner();
438
+ await this.ensureMarketMap();
334
439
  const nonce = await this.getNextNonce();
335
440
  const marketIndex = this.getMarketIndex(symbol);
336
441
  const { baseAmount, priceTicks } = this.toTicks(symbol, parseFloat(size), parseFloat(price));
@@ -353,6 +458,7 @@ export class LighterAdapter {
353
458
  }
354
459
  async cancelOrder(symbol, orderId) {
355
460
  this.ensureSigner();
461
+ await this.ensureMarketMap();
356
462
  const nonce = await this.getNextNonce();
357
463
  const marketIndex = this.getMarketIndex(symbol);
358
464
  const signed = await this._signer.signCancelOrder({
@@ -361,8 +467,18 @@ export class LighterAdapter {
361
467
  });
362
468
  return this.sendTx(signed);
363
469
  }
364
- async cancelAllOrders(_symbol) {
470
+ async cancelAllOrders(symbol) {
365
471
  this.ensureSigner();
472
+ if (symbol) {
473
+ // Cancel only orders for this symbol: fetch open orders, filter, cancel individually
474
+ const orders = await this.getOpenOrders();
475
+ const filtered = orders.filter(o => o.symbol.toUpperCase() === symbol.toUpperCase());
476
+ const results = [];
477
+ for (const o of filtered) {
478
+ results.push(await this.cancelOrder(o.symbol, o.orderId));
479
+ }
480
+ return results;
481
+ }
366
482
  const nonce = await this.getNextNonce();
367
483
  const signed = await this._signer.signCancelAllOrders({
368
484
  timeInForce: 0, time: Math.floor(Date.now() / 1000) + 86400, nonce,
@@ -372,6 +488,7 @@ export class LighterAdapter {
372
488
  }
373
489
  async modifyOrder(symbol, orderId, price, size) {
374
490
  this.ensureSigner();
491
+ await this.ensureMarketMap();
375
492
  const nonce = await this.getNextNonce();
376
493
  const marketIndex = this.getMarketIndex(symbol);
377
494
  const { baseAmount, priceTicks } = this.toTicks(symbol, parseFloat(size), parseFloat(price));
@@ -392,6 +509,7 @@ export class LighterAdapter {
392
509
  }
393
510
  async stopOrder(symbol, side, size, triggerPrice, opts) {
394
511
  this.ensureSigner();
512
+ await this.ensureMarketMap();
395
513
  const nonce = await this.getNextNonce();
396
514
  const marketIndex = this.getMarketIndex(symbol);
397
515
  const { baseAmount } = this.toTicks(symbol, parseFloat(size), 0);
@@ -424,6 +542,7 @@ export class LighterAdapter {
424
542
  }
425
543
  async updateLeverage(symbol, leverage, marginMode = "cross") {
426
544
  this.ensureSigner();
545
+ await this.ensureMarketMap();
427
546
  const nonce = await this.getNextNonce();
428
547
  const marketIndex = this.getMarketIndex(symbol);
429
548
  // fraction = initial margin fraction in basis points: 10000/leverage
@@ -455,13 +574,15 @@ export class LighterAdapter {
455
574
  return this.updateLeverage(symbol, leverage, marginMode);
456
575
  }
457
576
  async getRecentTrades(symbol, limit = 20) {
577
+ await this.ensureMarketMap();
458
578
  const marketId = this.getMarketIndex(symbol);
459
579
  const res = await this.restGet("/recentTrades", { market_id: String(marketId), limit: String(limit) });
460
580
  const trades = (res.trades ?? []);
461
581
  return trades.map((t) => ({
462
582
  time: Number(t.timestamp ?? 0) * 1000,
463
583
  symbol,
464
- side: t.is_ask ? "sell" : "buy",
584
+ // /recentTrades returns is_maker_ask: maker was asking → taker bought
585
+ side: t.is_maker_ask ? "buy" : "sell",
465
586
  price: String(t.price ?? "0"),
466
587
  size: String(t.base_amount ?? t.amount ?? ""),
467
588
  fee: String(t.fee ?? "0"),
@@ -489,6 +610,8 @@ export class LighterAdapter {
489
610
  }));
490
611
  }
491
612
  async getOrderHistory(limit = 30) {
613
+ if (!this._address)
614
+ throw new Error("No private key configured — account data unavailable. Run: perp init");
492
615
  const raw = await this._getOrderHistoryRaw();
493
616
  const orders = (raw.orders ?? []);
494
617
  return orders.slice(0, limit).map((o) => ({
@@ -503,6 +626,8 @@ export class LighterAdapter {
503
626
  }));
504
627
  }
505
628
  async getTradeHistory(limit = 30) {
629
+ if (!this._address)
630
+ throw new Error("No private key configured — account data unavailable. Run: perp init");
506
631
  const raw = await this._getTradeHistoryRaw(limit);
507
632
  const trades = (raw.trades ?? []);
508
633
  return trades.slice(0, limit).map((t) => ({
@@ -515,6 +640,8 @@ export class LighterAdapter {
515
640
  }));
516
641
  }
517
642
  async getFundingPayments(limit = 30) {
643
+ if (!this._address)
644
+ throw new Error("No private key configured — account data unavailable. Run: perp init");
518
645
  const raw = await this._getPositionFundingRaw();
519
646
  const items = (raw.funding ?? []);
520
647
  return items.slice(0, limit).map((f) => ({
@@ -552,6 +679,7 @@ export class LighterAdapter {
552
679
  * Get candle data for a market.
553
680
  */
554
681
  async getCandles(symbol, resolution, startTime, endTime, countBack = 100) {
682
+ await this.ensureMarketMap();
555
683
  const marketId = this.getMarketIndex(symbol);
556
684
  return this.restGet("/candles", {
557
685
  market_id: String(marketId),
@@ -588,6 +716,9 @@ export class LighterAdapter {
588
716
  async _getTradeHistoryRaw(limit = 100) {
589
717
  if (this._accountIndex < 0)
590
718
  return { trades: [] };
719
+ // Ensure market map is populated (init may have been rate-limited)
720
+ if (this._marketMap.size === 0)
721
+ await this._refreshMarketMap();
591
722
  const allTrades = [];
592
723
  const entries = Array.from(this._marketMap.entries());
593
724
  const results = await Promise.allSettled(entries.map(([sym, marketId]) => this.restGetAuth("/trades", {
@@ -612,15 +743,37 @@ export class LighterAdapter {
612
743
  async _getPositionFundingRaw() {
613
744
  if (this._accountIndex < 0)
614
745
  return { funding: [] };
746
+ if (this._marketMap.size === 0)
747
+ await this._refreshMarketMap();
748
+ // Only query markets where the account has/had positions (avoids 156-market fan-out)
749
+ const acct = await this.fetchAccount();
750
+ const activeMarketIds = new Set();
751
+ if (acct) {
752
+ for (const p of acct.positions ?? []) {
753
+ const pos = Number(p.position || 0);
754
+ const rpnl = Number(p.realized_pnl || 0);
755
+ if (pos !== 0 || rpnl !== 0) {
756
+ activeMarketIds.add(Number(p.market_id));
757
+ }
758
+ }
759
+ }
760
+ if (activeMarketIds.size === 0)
761
+ return { funding: [] };
762
+ // Resolve market_id → symbol from marketMap
763
+ const reverseMap = new Map();
764
+ for (const [sym, id] of this._marketMap)
765
+ reverseMap.set(id, sym);
615
766
  const allFunding = [];
616
- const entries = Array.from(this._marketMap.entries());
617
- const results = await Promise.allSettled(entries.map(([sym, marketId]) => this.restGetAuth("/positionFunding", {
618
- account_index: String(this._accountIndex),
619
- market_id: String(marketId),
620
- }).then((res) => {
621
- const items = (res.funding ?? res.data ?? []);
622
- return items.map((f) => ({ ...f, symbol: f.symbol ?? sym }));
623
- })));
767
+ const results = await Promise.allSettled(Array.from(activeMarketIds).map((marketId) => {
768
+ const sym = reverseMap.get(marketId) ?? `Market-${marketId}`;
769
+ return this.restGetAuth("/positionFunding", {
770
+ account_index: String(this._accountIndex),
771
+ market_id: String(marketId),
772
+ }).then((res) => {
773
+ const items = (res.funding ?? res.data ?? []);
774
+ return items.map((f) => ({ ...f, symbol: f.symbol ?? sym }));
775
+ });
776
+ }));
624
777
  for (const r of results) {
625
778
  if (r.status === "fulfilled")
626
779
  allFunding.push(...r.value);
@@ -780,7 +933,7 @@ export class LighterAdapter {
780
933
  return res.json();
781
934
  }
782
935
  ensureSigner() {
783
- if (this._readOnly) {
936
+ if (this._readOnly || this._accountIndex < 0 || !this._signer) {
784
937
  throw new Error("This command requires a Lighter API key. Run `perp -e lighter manage setup-api-key` first, " +
785
938
  "then set LIGHTER_API_KEY in your .env");
786
939
  }
@@ -828,8 +981,10 @@ export class LighterAdapter {
828
981
  * Uses WASM signer to sign + ETH key for L1 signature.
829
982
  * Returns the generated key pair.
830
983
  */
831
- async setupApiKey(apiKeyIndex = 2) {
832
- const { ethers } = await import("ethers");
984
+ async setupApiKey(apiKeyIndex = 4) {
985
+ if (!this._evmSigner) {
986
+ throw new Error("No EVM private key configured. Run: perp init");
987
+ }
833
988
  // 1. Generate key pair
834
989
  const { privateKey, publicKey } = await LighterAdapter.generateApiKey();
835
990
  // 2. Get nonce from API
@@ -859,9 +1014,8 @@ export class LighterAdapter {
859
1014
  if (!signed.txInfo || !signed.messageToSign) {
860
1015
  throw new Error("SignChangePubKey returned incomplete response");
861
1016
  }
862
- // 4. Sign messageToSign with ETH key (EIP-191 personal_sign)
863
- const wallet = new ethers.Wallet(this._evmKey);
864
- const l1Sig = await wallet.signMessage(signed.messageToSign);
1017
+ // 4. Sign messageToSign with EVM signer (EIP-191 personal_sign)
1018
+ const l1Sig = await this._evmSigner.signMessage(signed.messageToSign);
865
1019
  // 5. Add L1Sig to txInfo
866
1020
  const txInfo = JSON.parse(signed.txInfo);
867
1021
  txInfo.L1Sig = l1Sig;
@@ -900,7 +1054,7 @@ export class LighterAdapter {
900
1054
  continue;
901
1055
  }
902
1056
  const retryTxInfo = JSON.parse(retrySigned.txInfo);
903
- retryTxInfo.L1Sig = await wallet.signMessage(retrySigned.messageToSign);
1057
+ retryTxInfo.L1Sig = await this._evmSigner.signMessage(retrySigned.messageToSign);
904
1058
  const retryRes = await fetch(`${this._baseUrl}/api/v1/sendTx`, {
905
1059
  method: "POST",
906
1060
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
@@ -1,13 +1,20 @@
1
1
  import { Keypair } from "@solana/web3.js";
2
2
  import { PacificaClient, type Network } from "../pacifica/index.js";
3
3
  import type { ExchangeAdapter, ExchangeMarketInfo, ExchangePosition, ExchangeOrder, ExchangeBalance, ExchangeTrade, ExchangeFundingPayment, ExchangeKline } from "./interface.js";
4
+ import type { SolanaSigner } from "../signer/interface.js";
4
5
  export declare class PacificaAdapter implements ExchangeAdapter {
5
6
  readonly name = "pacifica";
6
7
  private client;
7
- readonly keypair: Keypair;
8
+ private _solanaSigner;
9
+ private _hasRealKey;
8
10
  private account;
9
11
  private signMessage;
10
- constructor(keypair: Keypair, network?: Network, builderCode?: string);
12
+ constructor(keypair: Keypair, network?: Network, builderCode?: string, hasRealKey?: boolean);
13
+ private ensureSigner;
14
+ /** Inject an external Solana signer. */
15
+ setSigner(signer: SolanaSigner): void;
16
+ /** Access the underlying Keypair (only available with LocalSolanaSigner). */
17
+ get keypair(): Keypair;
11
18
  private _getPrices;
12
19
  private _getPositions;
13
20
  get publicKey(): string;
@@ -27,7 +34,7 @@ export declare class PacificaAdapter implements ExchangeAdapter {
27
34
  tif?: string;
28
35
  }): Promise<unknown>;
29
36
  cancelOrder(symbol: string, orderId: string): Promise<unknown>;
30
- cancelAllOrders(_symbol?: string): Promise<unknown>;
37
+ cancelAllOrders(symbol?: string): Promise<unknown>;
31
38
  editOrder(symbol: string, orderId: string, price: string, size: string): Promise<unknown>;
32
39
  setLeverage(symbol: string, leverage: number, marginMode?: "cross" | "isolated"): Promise<{
33
40
  symbol: string;
@@ -42,7 +49,7 @@ export declare class PacificaAdapter implements ExchangeAdapter {
42
49
  getFundingHistory(symbol: string, limit?: number): Promise<{
43
50
  time: number;
44
51
  rate: string;
45
- price: string;
52
+ price: string | null;
46
53
  }[]>;
47
54
  getKlines(symbol: string, interval: string, startTime: number, endTime: number): Promise<ExchangeKline[]>;
48
55
  getOrderHistory(limit?: number): Promise<ExchangeOrder[]>;