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,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,12 +58,21 @@ 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
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
+ }
65
76
  const res = await fetch(`${this._baseUrl}/api/v1/account?by=l1_address&value=${this._address}`);
66
77
  const json = await res.json();
67
78
  if (json.accounts && json.accounts.length > 0) {
@@ -73,7 +84,7 @@ export class LighterAdapter {
73
84
  let signerReady = false;
74
85
  if (!this._apiKey && this._accountIndex >= 0) {
75
86
  try {
76
- const autoKeyIndex = 2; // default for auto-setup
87
+ const autoKeyIndex = 4; // default for auto-setup (0-3 reserved by Lighter frontend)
77
88
  const { privateKey: apiKey } = await this.setupApiKey(autoKeyIndex);
78
89
  this._apiKey = apiKey;
79
90
  this._apiKeyIndex = autoKeyIndex;
@@ -108,6 +119,11 @@ export class LighterAdapter {
108
119
  else if (cleanKey.length === 0) {
109
120
  this._apiKey = "";
110
121
  }
122
+ else if (this._accountIndex < 0) {
123
+ // Account lookup failed or returned no accounts — cannot initialize WASM signer
124
+ 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.`);
125
+ this._apiKey = "";
126
+ }
111
127
  else {
112
128
  this._apiKey = cleanKey;
113
129
  const client = await LighterAdapter.getWasmClient();
@@ -121,22 +137,59 @@ export class LighterAdapter {
121
137
  this._signer = client;
122
138
  }
123
139
  }
124
- if (this._apiKey) {
125
- this._readOnly = false;
126
- }
140
+ this._readOnly = !this._apiKey;
127
141
  // 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
- });
142
+ await this._refreshMarketMap();
143
+ }
144
+ /** Populate marketMap + decimals from /orderBooks API (safe to call multiple times) */
145
+ async _refreshMarketMap() {
146
+ // Retry up to 2 times with 1s delay on failure
147
+ for (let attempt = 0; attempt < 2; attempt++) {
148
+ try {
149
+ // Prefer /orderBooks which has supported_size_decimals / supported_price_decimals
150
+ const res = await this.restGet("/orderBooks", {});
151
+ const books = res.order_books ?? [];
152
+ if (books.length === 0 && attempt === 0) {
153
+ // Empty response (possible rate limit) — retry after delay
154
+ await new Promise(r => setTimeout(r, 1000));
155
+ continue;
156
+ }
157
+ for (const d of books) {
158
+ const sym = d.symbol.toUpperCase();
159
+ // Skip spot markets (id ≥ 2048) — those are handled by lighter-spot.ts
160
+ if (d.market_id >= 2048)
161
+ continue;
162
+ this._marketMap.set(sym, d.market_id);
163
+ this._marketDecimals.set(sym, {
164
+ size: d.supported_size_decimals,
165
+ price: d.supported_price_decimals,
166
+ });
167
+ }
168
+ if (this._marketMap.size > 0)
169
+ return; // success
136
170
  }
137
- }
138
- catch {
139
- // Market map will be empty, use fallback
171
+ catch {
172
+ // /orderBooks failed — try fallback below
173
+ }
174
+ // Fallback to /orderBookDetails
175
+ try {
176
+ const res = await this.restGet("/orderBookDetails", {});
177
+ for (const d of res.order_book_details ?? []) {
178
+ this._marketMap.set(d.symbol.toUpperCase(), d.market_id);
179
+ this._marketDecimals.set(d.symbol.toUpperCase(), {
180
+ size: d.size_decimals ?? 0,
181
+ price: d.price_decimals ?? 0,
182
+ });
183
+ }
184
+ if (this._marketMap.size > 0)
185
+ return; // success
186
+ }
187
+ catch {
188
+ // Both APIs failed this attempt
189
+ }
190
+ // Delay before retry
191
+ if (attempt === 0)
192
+ await new Promise(r => setTimeout(r, 1000));
140
193
  }
141
194
  }
142
195
  getMarketIndex(symbol) {
@@ -145,18 +198,32 @@ export class LighterAdapter {
145
198
  throw new Error(`Unknown Lighter market: ${symbol}`);
146
199
  return idx;
147
200
  }
201
+ /** Ensure market map is populated; lazy-refresh if empty */
202
+ async ensureMarketMap() {
203
+ if (this._marketMap.size === 0) {
204
+ await this._refreshMarketMap();
205
+ }
206
+ }
148
207
  async getMarkPrice(symbol) {
149
208
  try {
150
209
  const res = await this.restGet("/orderBookDetails", {});
151
210
  const m = res.order_book_details?.find(d => d.symbol.toUpperCase() === symbol.toUpperCase());
152
- return m?.last_trade_price ?? 0;
211
+ const price = m?.last_trade_price ?? 0;
212
+ if (price <= 0)
213
+ throw new Error(`Cannot determine mark price for ${symbol}`);
214
+ return price;
153
215
  }
154
- catch {
155
- return 0;
216
+ catch (e) {
217
+ if (e instanceof Error && e.message.startsWith("Cannot determine"))
218
+ throw e;
219
+ throw new Error(`Cannot determine mark price for ${symbol}`);
156
220
  }
157
221
  }
158
222
  toTicks(symbol, size, price) {
159
- const dec = this._marketDecimals.get(symbol.toUpperCase()) ?? { size: 0, price: 0 };
223
+ const dec = this._marketDecimals.get(symbol.toUpperCase());
224
+ if (!dec) {
225
+ throw new Error(`No market decimals loaded for ${symbol}. Market data may not be initialized.`);
226
+ }
160
227
  return {
161
228
  baseAmount: Math.round(size * Math.pow(10, dec.size)),
162
229
  priceTicks: Math.round(price * Math.pow(10, dec.price)),
@@ -203,6 +270,7 @@ export class LighterAdapter {
203
270
  }
204
271
  async getOrderbook(symbol) {
205
272
  try {
273
+ await this.ensureMarketMap();
206
274
  const marketId = this.getMarketIndex(symbol);
207
275
  const res = await this.restGet("/orderBookOrders", {
208
276
  market_id: String(marketId),
@@ -246,12 +314,12 @@ export class LighterAdapter {
246
314
  size: String(Math.abs(posSize)),
247
315
  entryPrice: String(p.avg_entry_price || "0"),
248
316
  markPrice: String(posSize !== 0
249
- ? (Number(p.position_value || 0) / Math.abs(posSize)).toFixed(4)
317
+ ? (Number(p.position_value || 0) / Math.abs(posSize)).toFixed(this._marketDecimals.get(String(p.symbol || "").toUpperCase())?.price ?? 4)
250
318
  : "0"),
251
319
  liquidationPrice: String(p.liquidation_price || "N/A"),
252
320
  unrealizedPnl: String(p.unrealized_pnl || "0"),
253
321
  leverage: Number(p.initial_margin_fraction || 0) > 0
254
- ? Math.round(100 / Number(p.initial_margin_fraction))
322
+ ? Math.round(10000 / Number(p.initial_margin_fraction))
255
323
  : 1,
256
324
  };
257
325
  });
@@ -307,6 +375,7 @@ export class LighterAdapter {
307
375
  }
308
376
  async marketOrder(symbol, side, size) {
309
377
  this.ensureSigner();
378
+ await this.ensureMarketMap();
310
379
  const nonce = await this.getNextNonce();
311
380
  const marketIndex = this.getMarketIndex(symbol);
312
381
  const { baseAmount } = this.toTicks(symbol, parseFloat(size), 0);
@@ -331,6 +400,7 @@ export class LighterAdapter {
331
400
  }
332
401
  async limitOrder(symbol, side, price, size, opts) {
333
402
  this.ensureSigner();
403
+ await this.ensureMarketMap();
334
404
  const nonce = await this.getNextNonce();
335
405
  const marketIndex = this.getMarketIndex(symbol);
336
406
  const { baseAmount, priceTicks } = this.toTicks(symbol, parseFloat(size), parseFloat(price));
@@ -353,6 +423,7 @@ export class LighterAdapter {
353
423
  }
354
424
  async cancelOrder(symbol, orderId) {
355
425
  this.ensureSigner();
426
+ await this.ensureMarketMap();
356
427
  const nonce = await this.getNextNonce();
357
428
  const marketIndex = this.getMarketIndex(symbol);
358
429
  const signed = await this._signer.signCancelOrder({
@@ -361,8 +432,18 @@ export class LighterAdapter {
361
432
  });
362
433
  return this.sendTx(signed);
363
434
  }
364
- async cancelAllOrders(_symbol) {
435
+ async cancelAllOrders(symbol) {
365
436
  this.ensureSigner();
437
+ if (symbol) {
438
+ // Cancel only orders for this symbol: fetch open orders, filter, cancel individually
439
+ const orders = await this.getOpenOrders();
440
+ const filtered = orders.filter(o => o.symbol.toUpperCase() === symbol.toUpperCase());
441
+ const results = [];
442
+ for (const o of filtered) {
443
+ results.push(await this.cancelOrder(o.symbol, o.orderId));
444
+ }
445
+ return results;
446
+ }
366
447
  const nonce = await this.getNextNonce();
367
448
  const signed = await this._signer.signCancelAllOrders({
368
449
  timeInForce: 0, time: Math.floor(Date.now() / 1000) + 86400, nonce,
@@ -372,6 +453,7 @@ export class LighterAdapter {
372
453
  }
373
454
  async modifyOrder(symbol, orderId, price, size) {
374
455
  this.ensureSigner();
456
+ await this.ensureMarketMap();
375
457
  const nonce = await this.getNextNonce();
376
458
  const marketIndex = this.getMarketIndex(symbol);
377
459
  const { baseAmount, priceTicks } = this.toTicks(symbol, parseFloat(size), parseFloat(price));
@@ -392,6 +474,7 @@ export class LighterAdapter {
392
474
  }
393
475
  async stopOrder(symbol, side, size, triggerPrice, opts) {
394
476
  this.ensureSigner();
477
+ await this.ensureMarketMap();
395
478
  const nonce = await this.getNextNonce();
396
479
  const marketIndex = this.getMarketIndex(symbol);
397
480
  const { baseAmount } = this.toTicks(symbol, parseFloat(size), 0);
@@ -424,6 +507,7 @@ export class LighterAdapter {
424
507
  }
425
508
  async updateLeverage(symbol, leverage, marginMode = "cross") {
426
509
  this.ensureSigner();
510
+ await this.ensureMarketMap();
427
511
  const nonce = await this.getNextNonce();
428
512
  const marketIndex = this.getMarketIndex(symbol);
429
513
  // fraction = initial margin fraction in basis points: 10000/leverage
@@ -455,13 +539,15 @@ export class LighterAdapter {
455
539
  return this.updateLeverage(symbol, leverage, marginMode);
456
540
  }
457
541
  async getRecentTrades(symbol, limit = 20) {
542
+ await this.ensureMarketMap();
458
543
  const marketId = this.getMarketIndex(symbol);
459
544
  const res = await this.restGet("/recentTrades", { market_id: String(marketId), limit: String(limit) });
460
545
  const trades = (res.trades ?? []);
461
546
  return trades.map((t) => ({
462
547
  time: Number(t.timestamp ?? 0) * 1000,
463
548
  symbol,
464
- side: t.is_ask ? "sell" : "buy",
549
+ // /recentTrades returns is_maker_ask: maker was asking → taker bought
550
+ side: t.is_maker_ask ? "buy" : "sell",
465
551
  price: String(t.price ?? "0"),
466
552
  size: String(t.base_amount ?? t.amount ?? ""),
467
553
  fee: String(t.fee ?? "0"),
@@ -552,6 +638,7 @@ export class LighterAdapter {
552
638
  * Get candle data for a market.
553
639
  */
554
640
  async getCandles(symbol, resolution, startTime, endTime, countBack = 100) {
641
+ await this.ensureMarketMap();
555
642
  const marketId = this.getMarketIndex(symbol);
556
643
  return this.restGet("/candles", {
557
644
  market_id: String(marketId),
@@ -588,6 +675,9 @@ export class LighterAdapter {
588
675
  async _getTradeHistoryRaw(limit = 100) {
589
676
  if (this._accountIndex < 0)
590
677
  return { trades: [] };
678
+ // Ensure market map is populated (init may have been rate-limited)
679
+ if (this._marketMap.size === 0)
680
+ await this._refreshMarketMap();
591
681
  const allTrades = [];
592
682
  const entries = Array.from(this._marketMap.entries());
593
683
  const results = await Promise.allSettled(entries.map(([sym, marketId]) => this.restGetAuth("/trades", {
@@ -612,6 +702,8 @@ export class LighterAdapter {
612
702
  async _getPositionFundingRaw() {
613
703
  if (this._accountIndex < 0)
614
704
  return { funding: [] };
705
+ if (this._marketMap.size === 0)
706
+ await this._refreshMarketMap();
615
707
  const allFunding = [];
616
708
  const entries = Array.from(this._marketMap.entries());
617
709
  const results = await Promise.allSettled(entries.map(([sym, marketId]) => this.restGetAuth("/positionFunding", {
@@ -828,8 +920,10 @@ export class LighterAdapter {
828
920
  * Uses WASM signer to sign + ETH key for L1 signature.
829
921
  * Returns the generated key pair.
830
922
  */
831
- async setupApiKey(apiKeyIndex = 2) {
832
- const { ethers } = await import("ethers");
923
+ async setupApiKey(apiKeyIndex = 4) {
924
+ if (!this._evmSigner) {
925
+ throw new Error("No EVM private key configured. Run: perp init");
926
+ }
833
927
  // 1. Generate key pair
834
928
  const { privateKey, publicKey } = await LighterAdapter.generateApiKey();
835
929
  // 2. Get nonce from API
@@ -859,9 +953,8 @@ export class LighterAdapter {
859
953
  if (!signed.txInfo || !signed.messageToSign) {
860
954
  throw new Error("SignChangePubKey returned incomplete response");
861
955
  }
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);
956
+ // 4. Sign messageToSign with EVM signer (EIP-191 personal_sign)
957
+ const l1Sig = await this._evmSigner.signMessage(signed.messageToSign);
865
958
  // 5. Add L1Sig to txInfo
866
959
  const txInfo = JSON.parse(signed.txInfo);
867
960
  txInfo.L1Sig = l1Sig;
@@ -900,7 +993,7 @@ export class LighterAdapter {
900
993
  continue;
901
994
  }
902
995
  const retryTxInfo = JSON.parse(retrySigned.txInfo);
903
- retryTxInfo.L1Sig = await wallet.signMessage(retrySigned.messageToSign);
996
+ retryTxInfo.L1Sig = await this._evmSigner.signMessage(retrySigned.messageToSign);
904
997
  const retryRes = await fetch(`${this._baseUrl}/api/v1/sendTx`, {
905
998
  method: "POST",
906
999
  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[]>;
@@ -1,22 +1,37 @@
1
- import nacl from "tweetnacl";
2
1
  import { PacificaClient } from "../pacifica/index.js";
3
- function makeSignMessage(keypair) {
4
- return async (message) => {
5
- return nacl.sign.detached(message, keypair.secretKey);
6
- };
7
- }
2
+ import { LocalSolanaSigner } from "../signer/solana-local.js";
8
3
  export class PacificaAdapter {
9
4
  name = "pacifica";
10
5
  client;
11
- keypair;
6
+ _solanaSigner;
7
+ _hasRealKey;
12
8
  account;
13
9
  signMessage;
14
10
  // In-memory caches removed — using file-based cache (src/cache.ts) for cross-process dedup
15
- constructor(keypair, network = "mainnet", builderCode) {
16
- this.keypair = keypair;
17
- this.account = keypair.publicKey.toBase58();
11
+ constructor(keypair, network = "mainnet", builderCode, hasRealKey = true) {
12
+ this._solanaSigner = new LocalSolanaSigner(keypair);
13
+ this._hasRealKey = hasRealKey;
14
+ this.account = this._solanaSigner.getPublicKeyBase58();
18
15
  this.client = new PacificaClient({ network, builderCode });
19
- this.signMessage = makeSignMessage(keypair);
16
+ this.signMessage = (msg) => this._solanaSigner.signMessage(msg);
17
+ }
18
+ ensureSigner() {
19
+ if (!this._hasRealKey) {
20
+ throw new Error("No private key configured. Run: perp init");
21
+ }
22
+ }
23
+ /** Inject an external Solana signer. */
24
+ setSigner(signer) {
25
+ this._solanaSigner = signer;
26
+ this.account = signer.getPublicKeyBase58();
27
+ this.signMessage = (msg) => this._solanaSigner.signMessage(msg);
28
+ }
29
+ /** Access the underlying Keypair (only available with LocalSolanaSigner). */
30
+ get keypair() {
31
+ if (this._solanaSigner instanceof LocalSolanaSigner) {
32
+ return this._solanaSigner._keypair;
33
+ }
34
+ throw new Error("keypair not available with external signer");
20
35
  }
21
36
  async _getPrices() {
22
37
  const { fetchAndCache, TTL_ACCOUNT } = await import("../cache.js");
@@ -62,24 +77,13 @@ export class PacificaAdapter {
62
77
  };
63
78
  }
64
79
  async getBalance() {
65
- const [info, positions, prices] = await Promise.all([
80
+ const [info, positions] = await Promise.all([
66
81
  this.client.getAccount(this.account),
67
82
  this._getPositions(),
68
- this._getPrices(),
69
83
  ]);
70
84
  const raw = info;
71
- const priceMap = new Map(prices.map((p) => [p.symbol, Number(p.mark)]));
72
- // Sum actual unrealized PnL from positions (mark vs entry)
73
- let totalPnl = 0;
74
- for (const p of positions) {
75
- const mark = priceMap.get(p.symbol) ?? 0;
76
- const entry = Number(p.entry_price);
77
- const amount = Number(p.amount);
78
- const side = p.side === "bid" ? "long" : "short";
79
- totalPnl += side === "long"
80
- ? (mark - entry) * amount
81
- : (entry - mark) * amount;
82
- }
85
+ // Use API-provided unrealized PnL from positions
86
+ const totalPnl = positions.reduce((sum, p) => sum + Number(p.unrealized_pnl ?? 0), 0);
83
87
  return {
84
88
  equity: info.account_equity,
85
89
  available: info.available_to_spend,
@@ -104,14 +108,8 @@ export class PacificaAdapter {
104
108
  // Settings API may not be available
105
109
  }
106
110
  return positions.map((p) => {
107
- const mark = priceMap.get(p.symbol)?.mark ?? "0";
108
- const entryPrice = Number(p.entry_price);
109
- const amount = Number(p.amount);
110
- const markNum = Number(mark);
111
+ const mark = priceMap.get(p.symbol)?.mark ?? p.mark_price ?? "0";
111
112
  const side = p.side === "bid" ? "long" : "short";
112
- const pnl = side === "long"
113
- ? (markNum - entryPrice) * amount
114
- : (entryPrice - markNum) * amount;
115
113
  return {
116
114
  symbol: p.symbol,
117
115
  side: side,
@@ -119,8 +117,8 @@ export class PacificaAdapter {
119
117
  entryPrice: String(p.entry_price),
120
118
  markPrice: mark,
121
119
  liquidationPrice: String(p.liquidation_price ?? "N/A"),
122
- unrealizedPnl: pnl.toFixed(4),
123
- leverage: levMap.get(String(p.symbol)) ?? 1,
120
+ unrealizedPnl: String(p.unrealized_pnl ?? "0"),
121
+ leverage: p.leverage ?? levMap.get(String(p.symbol)) ?? 1,
124
122
  };
125
123
  });
126
124
  }
@@ -141,21 +139,37 @@ export class PacificaAdapter {
141
139
  });
142
140
  }
143
141
  async marketOrder(symbol, side, size) {
142
+ this.ensureSigner();
144
143
  return this.client.createMarketOrder({ symbol, amount: size, side: side === "buy" ? "bid" : "ask", reduce_only: false, slippage_percent: "1" }, this.account, this.signMessage);
145
144
  }
146
145
  async limitOrder(symbol, side, price, size, opts) {
146
+ this.ensureSigner();
147
147
  return this.client.createLimitOrder({ symbol, price, amount: size, side: side === "buy" ? "bid" : "ask", reduce_only: opts?.reduceOnly ?? false, tif: (opts?.tif ?? "GTC") }, this.account, this.signMessage);
148
148
  }
149
149
  async cancelOrder(symbol, orderId) {
150
+ this.ensureSigner();
150
151
  return this.client.cancelOrder({ symbol, order_id: Number(orderId) }, this.account, this.signMessage);
151
152
  }
152
- async cancelAllOrders(_symbol) {
153
+ async cancelAllOrders(symbol) {
154
+ this.ensureSigner();
155
+ if (symbol) {
156
+ // Cancel only orders for this symbol: fetch open orders, filter, cancel individually
157
+ const orders = await this.getOpenOrders();
158
+ const filtered = orders.filter(o => o.symbol.toUpperCase() === symbol.toUpperCase());
159
+ const results = [];
160
+ for (const o of filtered) {
161
+ results.push(await this.cancelOrder(o.symbol, o.orderId));
162
+ }
163
+ return results;
164
+ }
153
165
  return this.client.cancelAllOrders({ all_symbols: true, exclude_reduce_only: false }, this.account, this.signMessage);
154
166
  }
155
167
  async editOrder(symbol, orderId, price, size) {
168
+ this.ensureSigner();
156
169
  return this.client.editOrder({ symbol, order_id: Number(orderId), price, amount: size }, this.account, this.signMessage);
157
170
  }
158
171
  async setLeverage(symbol, leverage, marginMode = "cross") {
172
+ this.ensureSigner();
159
173
  await this.client.updateLeverage({ symbol, leverage }, this.account, this.signMessage);
160
174
  if (marginMode === "isolated") {
161
175
  await this.client.updateMarginMode({ symbol, is_isolated: true }, this.account, this.signMessage);
@@ -163,6 +177,7 @@ export class PacificaAdapter {
163
177
  return { symbol, leverage, marginMode };
164
178
  }
165
179
  async stopOrder(symbol, side, size, triggerPrice, opts) {
180
+ this.ensureSigner();
166
181
  return this.client.createStopOrder({
167
182
  symbol,
168
183
  side: side === "buy" ? "bid" : "ask",
@@ -173,12 +188,12 @@ export class PacificaAdapter {
173
188
  async getRecentTrades(symbol, _limit = 20) {
174
189
  const trades = await this.client.getTrades(symbol);
175
190
  return trades.slice(0, _limit).map((t) => ({
176
- time: new Date(t.created_at).getTime(),
191
+ time: Number(t.created_at ?? 0),
177
192
  symbol,
178
193
  side: t.side === "bid" ? "buy" : "sell",
179
194
  price: t.price,
180
195
  size: t.amount,
181
- fee: "0",
196
+ fee: String(t.fee ?? "0"),
182
197
  }));
183
198
  }
184
199
  async getFundingHistory(symbol, limit = 10) {
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Spot trading interface for exchanges that support both spot and perp.
3
+ * Separate from ExchangeAdapter because spot uses balance-based tracking
4
+ * (not position-based) and has no leverage, liquidation, or funding concepts.
5
+ *
6
+ * Supported: Hyperliquid, Lighter (Pacifica is perp-only)
7
+ */
8
+ export interface SpotMarketInfo {
9
+ symbol: string;
10
+ baseToken: string;
11
+ quoteToken: string;
12
+ markPrice: string;
13
+ volume24h: string;
14
+ sizeDecimals: number;
15
+ priceDecimals: number;
16
+ }
17
+ export interface SpotBalance {
18
+ token: string;
19
+ total: string;
20
+ available: string;
21
+ held: string;
22
+ }
23
+ export interface SpotAdapter {
24
+ readonly name: string;
25
+ getSpotMarkets(): Promise<SpotMarketInfo[]>;
26
+ getSpotOrderbook(symbol: string): Promise<{
27
+ bids: [string, string][];
28
+ asks: [string, string][];
29
+ }>;
30
+ getSpotBalances(): Promise<SpotBalance[]>;
31
+ spotMarketOrder(symbol: string, side: "buy" | "sell", size: string): Promise<unknown>;
32
+ spotLimitOrder(symbol: string, side: "buy" | "sell", price: string, size: string, opts?: {
33
+ tif?: string;
34
+ }): Promise<unknown>;
35
+ spotCancelOrder(symbol: string, orderId: string): Promise<unknown>;
36
+ }
37
+ /**
38
+ * U-token (Unit protocol) → underlying perp symbol mapping.
39
+ * These are bridged tokens on HyperEVM with the same price as the underlying (~0.1% deviation).
40
+ */
41
+ export declare const SPOT_PERP_TOKEN_MAP: Record<string, string>;
42
+ /** Reverse: perp symbol → U-token spot name on HL */
43
+ export declare const PERP_TO_SPOT_MAP: Record<string, string>;
44
+ /** Maximum price deviation (%) between spot and perp to be considered same underlying */
45
+ export declare const MAX_PRICE_DEVIATION_PCT = 5;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Spot trading interface for exchanges that support both spot and perp.
3
+ * Separate from ExchangeAdapter because spot uses balance-based tracking
4
+ * (not position-based) and has no leverage, liquidation, or funding concepts.
5
+ *
6
+ * Supported: Hyperliquid, Lighter (Pacifica is perp-only)
7
+ */
8
+ // ── Token mapping & price verification ──
9
+ /**
10
+ * U-token (Unit protocol) → underlying perp symbol mapping.
11
+ * These are bridged tokens on HyperEVM with the same price as the underlying (~0.1% deviation).
12
+ */
13
+ export const SPOT_PERP_TOKEN_MAP = {
14
+ UBTC: "BTC",
15
+ UETH: "ETH",
16
+ USOL: "SOL",
17
+ UFART: "FARTCOIN",
18
+ };
19
+ /** Reverse: perp symbol → U-token spot name on HL */
20
+ export const PERP_TO_SPOT_MAP = Object.fromEntries(Object.entries(SPOT_PERP_TOKEN_MAP).map(([k, v]) => [v, k]));
21
+ /** Maximum price deviation (%) between spot and perp to be considered same underlying */
22
+ export const MAX_PRICE_DEVIATION_PCT = 5;
@@ -1,14 +1,14 @@
1
1
  export interface ExecutionRecord {
2
2
  id: string;
3
3
  timestamp: string;
4
- type: "market_order" | "limit_order" | "cancel" | "stop_order" | "edit_order" | "rebalance" | "arb_entry" | "arb_close" | "bridge";
4
+ type: "market_order" | "limit_order" | "cancel" | "stop_order" | "edit_order" | "rebalance" | "arb_entry" | "arb_close" | "bridge" | "multi_leg" | "multi_leg_rollback";
5
5
  exchange: string;
6
6
  symbol: string;
7
7
  side: string;
8
8
  size: string;
9
9
  price?: string;
10
10
  notional?: number;
11
- status: "success" | "failed" | "simulated";
11
+ status: "success" | "failed" | "simulated" | "unverified";
12
12
  error?: string;
13
13
  meta?: Record<string, unknown>;
14
14
  dryRun: boolean;