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.
- 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/cli-spec.d.ts +0 -4
- package/dist/cli-spec.js +8 -1
- package/dist/commands/agent.js +7 -4
- package/dist/commands/arb-auto.js +643 -4
- 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/stream.js +132 -55
- package/dist/commands/trade.js +29 -8
- package/dist/commands/wallet.js +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.js +12 -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 +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 +417 -0
- package/dist/exchanges/lighter.d.ts +10 -2
- package/dist/exchanges/lighter.js +128 -35
- 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 +147 -98
- package/dist/liquidity.d.ts +12 -0
- package/dist/liquidity.js +43 -0
- 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/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 +93 -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,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(
|
|
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
|
-
|
|
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 =
|
|
832
|
-
|
|
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
|
|
863
|
-
const
|
|
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
|
|
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
|
-
|
|
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;
|