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.
- package/README.md +46 -5
- package/dist/arb/reconcile.d.ts +28 -0
- package/dist/arb/reconcile.js +162 -0
- package/dist/arb/sizing.d.ts +12 -1
- package/dist/arb/sizing.js +38 -5
- package/dist/arb/state.d.ts +6 -0
- package/dist/cli-spec.d.ts +0 -4
- package/dist/cli-spec.js +8 -1
- package/dist/commands/account.d.ts +1 -1
- package/dist/commands/account.js +300 -63
- package/dist/commands/agent.js +16 -9
- package/dist/commands/arb/index.js +243 -49
- package/dist/commands/arb-auto.js +696 -9
- package/dist/commands/arb.d.ts +2 -1
- package/dist/commands/arb.js +535 -131
- package/dist/commands/bot.js +207 -0
- package/dist/commands/dex.js +7 -1
- package/dist/commands/funding.d.ts +2 -1
- package/dist/commands/funding.js +211 -4
- package/dist/commands/funds.d.ts +4 -0
- package/dist/commands/funds.js +741 -0
- package/dist/commands/history.d.ts +2 -1
- package/dist/commands/history.js +508 -5
- package/dist/commands/manage.js +6 -4
- package/dist/commands/market.js +35 -2
- package/dist/commands/multileg.d.ts +16 -0
- package/dist/commands/multileg.js +174 -0
- package/dist/commands/pnl.d.ts +12 -0
- package/dist/commands/pnl.js +243 -0
- package/dist/commands/risk.d.ts +2 -0
- package/dist/commands/risk.js +56 -0
- package/dist/commands/settings.js +147 -0
- package/dist/commands/stream.js +132 -55
- package/dist/commands/trade.d.ts +1 -1
- package/dist/commands/trade.js +270 -267
- package/dist/commands/wallet.d.ts +4 -0
- package/dist/commands/wallet.js +11 -1
- package/dist/config.d.ts +6 -1
- package/dist/config.js +21 -1
- package/dist/dashboard/ws-feeds.js +1 -1
- package/dist/equity-tracker.d.ts +57 -0
- package/dist/equity-tracker.js +209 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +69 -0
- package/dist/exchanges/hyperliquid-spot.js +309 -0
- package/dist/exchanges/hyperliquid.d.ts +26 -5
- package/dist/exchanges/hyperliquid.js +171 -73
- package/dist/exchanges/interface.d.ts +1 -1
- package/dist/exchanges/lighter-spot.d.ts +55 -0
- package/dist/exchanges/lighter-spot.js +415 -0
- package/dist/exchanges/lighter.d.ts +10 -2
- package/dist/exchanges/lighter.js +210 -56
- package/dist/exchanges/pacifica.d.ts +11 -4
- package/dist/exchanges/pacifica.js +64 -37
- package/dist/exchanges/spot-interface.d.ts +45 -0
- package/dist/exchanges/spot-interface.js +22 -0
- package/dist/execution-log.d.ts +2 -2
- package/dist/funding/rates.d.ts +36 -0
- package/dist/funding/rates.js +146 -0
- package/dist/index.js +126 -161
- package/dist/liquidity.d.ts +12 -0
- package/dist/liquidity.js +43 -0
- package/dist/mcp-server.js +43 -96
- package/dist/signer/evm-local.d.ts +12 -0
- package/dist/signer/evm-local.js +19 -0
- package/dist/signer/index.d.ts +3 -0
- package/dist/signer/index.js +2 -0
- package/dist/signer/interface.d.ts +31 -0
- package/dist/signer/interface.js +7 -0
- package/dist/signer/solana-local.d.ts +13 -0
- package/dist/signer/solana-local.js +19 -0
- package/dist/smart-order.js +14 -3
- package/dist/strategies/split-order.d.ts +49 -0
- package/dist/strategies/split-order.js +138 -0
- package/dist/utils.js +7 -1
- package/dist/ws/market-feed.d.ts +74 -0
- package/dist/ws/market-feed.js +227 -0
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +80 -4
- package/skills/perp-cli/references/agent-operations.md +19 -9
- package/skills/perp-cli/references/commands.md +54 -32
- package/skills/perp-cli/references/strategies.md +27 -17
- package/skills/perp-cli/scripts/arb-monitor.sh +74 -0
- package/skills/perp-cli/scripts/funding-analysis.sh +85 -0
- package/skills/perp-cli/scripts/preflight.sh +98 -0
- package/skills/perp-cli/scripts/spot-perp-scan.sh +83 -0
- package/skills/perp-cli/scripts/validate-arb.sh +112 -0
- package/skills/perp-cli/templates/arb-scan.sh +0 -15
- package/skills/perp-cli/templates/open-position.sh +0 -38
- 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 || "
|
|
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
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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 =
|
|
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
|
-
|
|
125
|
-
this._readOnly = false;
|
|
126
|
-
}
|
|
156
|
+
this._readOnly = !this._apiKey;
|
|
127
157
|
// Build symbol → marketIndex map + decimals from orderBookDetails
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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())
|
|
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(
|
|
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
|
-
//
|
|
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
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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 =
|
|
832
|
-
|
|
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
|
|
863
|
-
const
|
|
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
|
|
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
|
-
|
|
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(
|
|
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[]>;
|