perp-cli 0.4.0 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -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/arb/utils.d.ts +2 -0
- package/dist/arb/utils.js +1 -1
- package/dist/cli-spec.d.ts +0 -4
- package/dist/cli-spec.js +8 -1
- package/dist/commands/agent.js +12 -9
- package/dist/commands/arb/index.js +18 -3
- package/dist/commands/arb-auto.js +885 -67
- 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/manage.js +1 -1
- package/dist/commands/market.js +1 -1
- 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/settings.d.ts +2 -1
- package/dist/commands/settings.js +133 -2
- package/dist/commands/stream.js +132 -55
- package/dist/commands/trade.js +118 -43
- package/dist/commands/wallet.js +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.js +12 -0
- package/dist/constants.d.ts +14 -0
- package/dist/constants.js +40 -0
- 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 +68 -0
- package/dist/exchanges/hyperliquid-spot.js +317 -0
- package/dist/exchanges/hyperliquid.d.ts +26 -5
- package/dist/exchanges/hyperliquid.js +181 -92
- package/dist/exchanges/interface.d.ts +1 -1
- package/dist/exchanges/lighter-spot.d.ts +55 -0
- package/dist/exchanges/lighter-spot.js +417 -0
- package/dist/exchanges/lighter.d.ts +10 -2
- package/dist/exchanges/lighter.js +132 -37
- package/dist/exchanges/pacifica.d.ts +11 -4
- package/dist/exchanges/pacifica.js +52 -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 +148 -99
- package/dist/liquidity.d.ts +12 -0
- package/dist/liquidity.js +43 -0
- package/dist/settings.d.ts +6 -0
- package/dist/settings.js +18 -2
- 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.d.ts +44 -0
- package/dist/smart-order.js +145 -0
- package/dist/trade-validator.js +2 -1
- 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 +102 -4
- package/skills/perp-cli/references/commands.md +16 -1
- package/skills/perp-cli/references/strategies.md +1 -1
- 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,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
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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 =
|
|
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
|
-
|
|
125
|
-
this._readOnly = false;
|
|
126
|
-
}
|
|
140
|
+
this._readOnly = !this._apiKey;
|
|
127
141
|
// Build symbol → marketIndex map + decimals from orderBookDetails
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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())
|
|
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(
|
|
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,9 +400,12 @@ 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));
|
|
407
|
+
// Map TIF string → Lighter numeric: IOC=0, GTT/GTC=1
|
|
408
|
+
const isIoc = opts?.tif?.toUpperCase() === "IOC";
|
|
337
409
|
const signed = await this.signOrder({
|
|
338
410
|
marketIndex,
|
|
339
411
|
clientOrderIndex: 0,
|
|
@@ -341,16 +413,17 @@ export class LighterAdapter {
|
|
|
341
413
|
price: priceTicks,
|
|
342
414
|
isAsk: side === "sell" ? 1 : 0,
|
|
343
415
|
orderType: 0, // ORDER_TYPE_LIMIT
|
|
344
|
-
timeInForce: 1, //
|
|
416
|
+
timeInForce: isIoc ? 0 : 1, // 0=IOC (immediate fill or cancel), 1=GTT (rests on book)
|
|
345
417
|
reduceOnly: opts?.reduceOnly ? 1 : 0,
|
|
346
418
|
triggerPrice: 0,
|
|
347
|
-
orderExpiry: -1, //
|
|
419
|
+
orderExpiry: isIoc ? 0 : -1, // IOC: no expiry; GTT: 28-day default
|
|
348
420
|
nonce,
|
|
349
421
|
});
|
|
350
422
|
return this.sendTx(signed);
|
|
351
423
|
}
|
|
352
424
|
async cancelOrder(symbol, orderId) {
|
|
353
425
|
this.ensureSigner();
|
|
426
|
+
await this.ensureMarketMap();
|
|
354
427
|
const nonce = await this.getNextNonce();
|
|
355
428
|
const marketIndex = this.getMarketIndex(symbol);
|
|
356
429
|
const signed = await this._signer.signCancelOrder({
|
|
@@ -359,8 +432,18 @@ export class LighterAdapter {
|
|
|
359
432
|
});
|
|
360
433
|
return this.sendTx(signed);
|
|
361
434
|
}
|
|
362
|
-
async cancelAllOrders(
|
|
435
|
+
async cancelAllOrders(symbol) {
|
|
363
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
|
+
}
|
|
364
447
|
const nonce = await this.getNextNonce();
|
|
365
448
|
const signed = await this._signer.signCancelAllOrders({
|
|
366
449
|
timeInForce: 0, time: Math.floor(Date.now() / 1000) + 86400, nonce,
|
|
@@ -370,6 +453,7 @@ export class LighterAdapter {
|
|
|
370
453
|
}
|
|
371
454
|
async modifyOrder(symbol, orderId, price, size) {
|
|
372
455
|
this.ensureSigner();
|
|
456
|
+
await this.ensureMarketMap();
|
|
373
457
|
const nonce = await this.getNextNonce();
|
|
374
458
|
const marketIndex = this.getMarketIndex(symbol);
|
|
375
459
|
const { baseAmount, priceTicks } = this.toTicks(symbol, parseFloat(size), parseFloat(price));
|
|
@@ -390,6 +474,7 @@ export class LighterAdapter {
|
|
|
390
474
|
}
|
|
391
475
|
async stopOrder(symbol, side, size, triggerPrice, opts) {
|
|
392
476
|
this.ensureSigner();
|
|
477
|
+
await this.ensureMarketMap();
|
|
393
478
|
const nonce = await this.getNextNonce();
|
|
394
479
|
const marketIndex = this.getMarketIndex(symbol);
|
|
395
480
|
const { baseAmount } = this.toTicks(symbol, parseFloat(size), 0);
|
|
@@ -422,6 +507,7 @@ export class LighterAdapter {
|
|
|
422
507
|
}
|
|
423
508
|
async updateLeverage(symbol, leverage, marginMode = "cross") {
|
|
424
509
|
this.ensureSigner();
|
|
510
|
+
await this.ensureMarketMap();
|
|
425
511
|
const nonce = await this.getNextNonce();
|
|
426
512
|
const marketIndex = this.getMarketIndex(symbol);
|
|
427
513
|
// fraction = initial margin fraction in basis points: 10000/leverage
|
|
@@ -453,13 +539,15 @@ export class LighterAdapter {
|
|
|
453
539
|
return this.updateLeverage(symbol, leverage, marginMode);
|
|
454
540
|
}
|
|
455
541
|
async getRecentTrades(symbol, limit = 20) {
|
|
542
|
+
await this.ensureMarketMap();
|
|
456
543
|
const marketId = this.getMarketIndex(symbol);
|
|
457
544
|
const res = await this.restGet("/recentTrades", { market_id: String(marketId), limit: String(limit) });
|
|
458
545
|
const trades = (res.trades ?? []);
|
|
459
546
|
return trades.map((t) => ({
|
|
460
547
|
time: Number(t.timestamp ?? 0) * 1000,
|
|
461
548
|
symbol,
|
|
462
|
-
|
|
549
|
+
// /recentTrades returns is_maker_ask: maker was asking → taker bought
|
|
550
|
+
side: t.is_maker_ask ? "buy" : "sell",
|
|
463
551
|
price: String(t.price ?? "0"),
|
|
464
552
|
size: String(t.base_amount ?? t.amount ?? ""),
|
|
465
553
|
fee: String(t.fee ?? "0"),
|
|
@@ -550,6 +638,7 @@ export class LighterAdapter {
|
|
|
550
638
|
* Get candle data for a market.
|
|
551
639
|
*/
|
|
552
640
|
async getCandles(symbol, resolution, startTime, endTime, countBack = 100) {
|
|
641
|
+
await this.ensureMarketMap();
|
|
553
642
|
const marketId = this.getMarketIndex(symbol);
|
|
554
643
|
return this.restGet("/candles", {
|
|
555
644
|
market_id: String(marketId),
|
|
@@ -586,6 +675,9 @@ export class LighterAdapter {
|
|
|
586
675
|
async _getTradeHistoryRaw(limit = 100) {
|
|
587
676
|
if (this._accountIndex < 0)
|
|
588
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();
|
|
589
681
|
const allTrades = [];
|
|
590
682
|
const entries = Array.from(this._marketMap.entries());
|
|
591
683
|
const results = await Promise.allSettled(entries.map(([sym, marketId]) => this.restGetAuth("/trades", {
|
|
@@ -610,6 +702,8 @@ export class LighterAdapter {
|
|
|
610
702
|
async _getPositionFundingRaw() {
|
|
611
703
|
if (this._accountIndex < 0)
|
|
612
704
|
return { funding: [] };
|
|
705
|
+
if (this._marketMap.size === 0)
|
|
706
|
+
await this._refreshMarketMap();
|
|
613
707
|
const allFunding = [];
|
|
614
708
|
const entries = Array.from(this._marketMap.entries());
|
|
615
709
|
const results = await Promise.allSettled(entries.map(([sym, marketId]) => this.restGetAuth("/positionFunding", {
|
|
@@ -826,8 +920,10 @@ export class LighterAdapter {
|
|
|
826
920
|
* Uses WASM signer to sign + ETH key for L1 signature.
|
|
827
921
|
* Returns the generated key pair.
|
|
828
922
|
*/
|
|
829
|
-
async setupApiKey(apiKeyIndex =
|
|
830
|
-
|
|
923
|
+
async setupApiKey(apiKeyIndex = 4) {
|
|
924
|
+
if (!this._evmSigner) {
|
|
925
|
+
throw new Error("No EVM private key configured. Run: perp init");
|
|
926
|
+
}
|
|
831
927
|
// 1. Generate key pair
|
|
832
928
|
const { privateKey, publicKey } = await LighterAdapter.generateApiKey();
|
|
833
929
|
// 2. Get nonce from API
|
|
@@ -857,9 +953,8 @@ export class LighterAdapter {
|
|
|
857
953
|
if (!signed.txInfo || !signed.messageToSign) {
|
|
858
954
|
throw new Error("SignChangePubKey returned incomplete response");
|
|
859
955
|
}
|
|
860
|
-
// 4. Sign messageToSign with
|
|
861
|
-
const
|
|
862
|
-
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);
|
|
863
958
|
// 5. Add L1Sig to txInfo
|
|
864
959
|
const txInfo = JSON.parse(signed.txInfo);
|
|
865
960
|
txInfo.L1Sig = l1Sig;
|
|
@@ -898,7 +993,7 @@ export class LighterAdapter {
|
|
|
898
993
|
continue;
|
|
899
994
|
}
|
|
900
995
|
const retryTxInfo = JSON.parse(retrySigned.txInfo);
|
|
901
|
-
retryTxInfo.L1Sig = await
|
|
996
|
+
retryTxInfo.L1Sig = await this._evmSigner.signMessage(retrySigned.messageToSign);
|
|
902
997
|
const retryRes = await fetch(`${this._baseUrl}/api/v1/sendTx`, {
|
|
903
998
|
method: "POST",
|
|
904
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
|
-
|
|
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[]>;
|
|
@@ -1,22 +1,37 @@
|
|
|
1
|
-
import nacl from "tweetnacl";
|
|
2
1
|
import { PacificaClient } from "../pacifica/index.js";
|
|
3
|
-
|
|
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
|
-
|
|
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.
|
|
17
|
-
this.
|
|
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 =
|
|
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
|
|
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
|
-
|
|
72
|
-
|
|
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:
|
|
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(
|
|
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:
|
|
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;
|
package/dist/execution-log.d.ts
CHANGED
|
@@ -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;
|