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,23 +1,30 @@
|
|
|
1
1
|
import { Hyperliquid } from "hyperliquid";
|
|
2
|
+
import { LocalEvmSigner } from "../signer/evm-local.js";
|
|
2
3
|
export class HyperliquidAdapter {
|
|
3
4
|
name = "hyperliquid";
|
|
4
5
|
sdk;
|
|
5
6
|
_address;
|
|
6
7
|
_privateKey;
|
|
7
8
|
_testnet;
|
|
9
|
+
_evmSigner;
|
|
8
10
|
_assetMap = new Map();
|
|
9
11
|
_assetMapReverse = new Map();
|
|
12
|
+
_szDecimalsMap = new Map(); // symbol → szDecimals
|
|
10
13
|
/** HIP-3 deployed perp dex name. Empty string = native (validator) perps. */
|
|
11
14
|
_dex = "";
|
|
12
15
|
// In-memory cache removed — using file-based cache (src/cache.ts) for cross-process dedup
|
|
13
16
|
constructor(privateKey, testnet = false) {
|
|
17
|
+
// Disable WebSocket in SDK — we use REST for all info calls and raw
|
|
18
|
+
// fetch for /exchange. This avoids the false "Please install ws" warning
|
|
19
|
+
// caused by the SDK's require('ws') failing in ESM context.
|
|
14
20
|
this.sdk = new Hyperliquid({
|
|
15
21
|
privateKey,
|
|
16
22
|
testnet,
|
|
17
23
|
walletAddress: undefined,
|
|
24
|
+
enableWs: false,
|
|
18
25
|
});
|
|
19
26
|
this._address = "";
|
|
20
|
-
this._privateKey = privateKey;
|
|
27
|
+
this._privateKey = privateKey ?? "";
|
|
21
28
|
this._testnet = testnet;
|
|
22
29
|
}
|
|
23
30
|
/** Set the HIP-3 deployed perp dex to query/trade on. */
|
|
@@ -39,6 +46,10 @@ export class HyperliquidAdapter {
|
|
|
39
46
|
get isTestnet() {
|
|
40
47
|
return this._testnet;
|
|
41
48
|
}
|
|
49
|
+
/** Inject an external EVM signer. Call before init() to skip LocalEvmSigner creation. */
|
|
50
|
+
setSigner(signer) {
|
|
51
|
+
this._evmSigner = signer;
|
|
52
|
+
}
|
|
42
53
|
async init() {
|
|
43
54
|
// Suppress "WebSocket connected" noise from hyperliquid SDK
|
|
44
55
|
const origLog = console.log;
|
|
@@ -49,14 +60,10 @@ export class HyperliquidAdapter {
|
|
|
49
60
|
finally {
|
|
50
61
|
console.log = origLog;
|
|
51
62
|
}
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this._address =
|
|
56
|
-
}
|
|
57
|
-
if (!this._address) {
|
|
58
|
-
const { ethers } = await import("ethers");
|
|
59
|
-
this._address = new ethers.Wallet(this._privateKey).address;
|
|
63
|
+
// Initialize EVM signer if not externally injected (skip if no key — read-only mode)
|
|
64
|
+
if (!this._evmSigner && this._privateKey) {
|
|
65
|
+
this._evmSigner = await LocalEvmSigner.create(this._privateKey);
|
|
66
|
+
this._address = this._evmSigner.getAddress();
|
|
60
67
|
}
|
|
61
68
|
// Build asset index map
|
|
62
69
|
await this._loadAssetMap();
|
|
@@ -85,12 +92,15 @@ export class HyperliquidAdapter {
|
|
|
85
92
|
meta.universe.forEach((asset, idx) => {
|
|
86
93
|
this._assetMap.set(asset.name, idx);
|
|
87
94
|
this._assetMapReverse.set(idx, asset.name);
|
|
95
|
+
if (asset.szDecimals !== undefined) {
|
|
96
|
+
this._szDecimalsMap.set(asset.name, asset.szDecimals);
|
|
97
|
+
}
|
|
88
98
|
});
|
|
89
99
|
}
|
|
90
100
|
}
|
|
91
101
|
}
|
|
92
|
-
catch {
|
|
93
|
-
|
|
102
|
+
catch (e) {
|
|
103
|
+
console.error("[hyperliquid] Failed to load asset map:", e instanceof Error ? e.message : e);
|
|
94
104
|
}
|
|
95
105
|
}
|
|
96
106
|
/**
|
|
@@ -115,7 +125,24 @@ export class HyperliquidAdapter {
|
|
|
115
125
|
}
|
|
116
126
|
return sym; // return as-is, let downstream error
|
|
117
127
|
}
|
|
118
|
-
|
|
128
|
+
/** Get szDecimals for a symbol (cached from init/getMeta) */
|
|
129
|
+
getSzDecimals(symbol) {
|
|
130
|
+
const resolved = this.resolveSymbol(symbol.toUpperCase());
|
|
131
|
+
return this._szDecimalsMap.get(resolved) ?? 2; // 2 is safe middle ground
|
|
132
|
+
}
|
|
133
|
+
/** HL official price rounding: 5 significant figures */
|
|
134
|
+
_hlRoundPrice(px, szDecimals, maxDecimals = 6) {
|
|
135
|
+
if (px > 100_000)
|
|
136
|
+
return String(Math.round(px));
|
|
137
|
+
const sig5 = Number(px.toPrecision(5));
|
|
138
|
+
const priceDec = Math.max(0, maxDecimals - szDecimals);
|
|
139
|
+
return Number(sig5.toFixed(priceDec)).toString();
|
|
140
|
+
}
|
|
141
|
+
async getAssetIndex(symbol) {
|
|
142
|
+
// Retry once if asset map is empty (e.g. init failed silently)
|
|
143
|
+
if (this._assetMap.size === 0) {
|
|
144
|
+
await this._loadAssetMap();
|
|
145
|
+
}
|
|
119
146
|
const resolved = this.resolveSymbol(symbol);
|
|
120
147
|
const idx = this._assetMap.get(resolved);
|
|
121
148
|
if (idx !== undefined)
|
|
@@ -177,8 +204,14 @@ export class HyperliquidAdapter {
|
|
|
177
204
|
]),
|
|
178
205
|
};
|
|
179
206
|
}
|
|
207
|
+
ensureAddress() {
|
|
208
|
+
if (!this._address) {
|
|
209
|
+
throw new Error("No private key configured — account data unavailable. Run: perp init");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
180
212
|
/** Always fetches live clearinghouseState, writes result to shared cache for dashboard */
|
|
181
213
|
async _getClearinghouseState() {
|
|
214
|
+
this.ensureAddress();
|
|
182
215
|
const { fetchAndCache, TTL_ACCOUNT } = await import("../cache.js");
|
|
183
216
|
const key = this._dex ? `acct:hl:chs:${this._address}:${this._dex}` : `acct:hl:chs:${this._address}`;
|
|
184
217
|
return fetchAndCache(key, TTL_ACCOUNT, async () => {
|
|
@@ -210,10 +243,10 @@ export class HyperliquidAdapter {
|
|
|
210
243
|
const spotState = await this.sdk.info.spot.getSpotClearinghouseState(this._address);
|
|
211
244
|
const balances = spotState?.balances ?? [];
|
|
212
245
|
const usdc = balances.find((b) => String(b.coin).startsWith("USDC"));
|
|
213
|
-
const spotTotal = Number(usdc
|
|
246
|
+
const spotTotal = usdc?.total !== undefined ? Number(usdc.total) : NaN;
|
|
214
247
|
const spotHold = Number(usdc?.hold ?? 0);
|
|
215
|
-
equity = spotTotal
|
|
216
|
-
available = spotTotal
|
|
248
|
+
equity = !isNaN(spotTotal) ? spotTotal : Number(margin.accountValue ?? cross.accountValue ?? 0);
|
|
249
|
+
available = !isNaN(spotTotal) ? spotTotal - spotHold : Number(s?.withdrawable ?? 0);
|
|
217
250
|
}
|
|
218
251
|
catch {
|
|
219
252
|
// Spot API failed — fall back to perp-only values
|
|
@@ -249,7 +282,7 @@ export class HyperliquidAdapter {
|
|
|
249
282
|
side: szi > 0 ? "long" : "short",
|
|
250
283
|
size: String(Math.abs(szi)),
|
|
251
284
|
entryPrice: String(pos.entryPx ?? "0"),
|
|
252
|
-
markPrice: String(pos.positionValue ?
|
|
285
|
+
markPrice: String(pos.positionValue ? Number(pos.positionValue) / Math.abs(szi) : "0"),
|
|
253
286
|
liquidationPrice: String(pos.liquidationPx ?? "N/A"),
|
|
254
287
|
unrealizedPnl: String(pos.unrealizedPnl ?? "0"),
|
|
255
288
|
leverage: Number(pos.leverage?.value ?? 1),
|
|
@@ -257,11 +290,13 @@ export class HyperliquidAdapter {
|
|
|
257
290
|
});
|
|
258
291
|
}
|
|
259
292
|
async getOpenOrders() {
|
|
293
|
+
this.ensureAddress();
|
|
260
294
|
const orders = await this.sdk.info.getUserOpenOrders(this._address);
|
|
261
295
|
return (orders ?? []).map((o) => ({
|
|
262
296
|
orderId: String(o.oid ?? ""),
|
|
263
297
|
symbol: String(o.coin ?? ""),
|
|
264
|
-
|
|
298
|
+
// SDK convertSymbolsInObject already converts "B"→"buy", "A"→"sell"
|
|
299
|
+
side: String(o.side) === "B" ? "buy" : String(o.side) === "A" ? "sell" : o.side,
|
|
265
300
|
price: String(o.limitPx ?? "0"),
|
|
266
301
|
size: String(o.sz ?? "0"),
|
|
267
302
|
filled: "0",
|
|
@@ -277,6 +312,7 @@ export class HyperliquidAdapter {
|
|
|
277
312
|
catch { /* ignore */ }
|
|
278
313
|
}
|
|
279
314
|
async marketOrder(symbol, side, size) {
|
|
315
|
+
this.ensureSigner();
|
|
280
316
|
if (this._dex) {
|
|
281
317
|
const r = await this._dexMarketOrder(symbol, side, size);
|
|
282
318
|
await this._invalidateAccountCache();
|
|
@@ -300,8 +336,8 @@ export class HyperliquidAdapter {
|
|
|
300
336
|
* and constructs + signs the order action directly.
|
|
301
337
|
*/
|
|
302
338
|
async _dexMarketOrder(symbol, side, size) {
|
|
303
|
-
const assetIndex = this.getAssetIndex(symbol.toUpperCase());
|
|
304
|
-
// Get current mark price from dex meta
|
|
339
|
+
const assetIndex = await this.getAssetIndex(symbol.toUpperCase());
|
|
340
|
+
// Get current mark price + szDecimals from dex meta
|
|
305
341
|
const meta = await this._infoPost({
|
|
306
342
|
type: "metaAndAssetCtxs",
|
|
307
343
|
dex: this._dex,
|
|
@@ -310,57 +346,41 @@ export class HyperliquidAdapter {
|
|
|
310
346
|
const midPrice = Number(ctx?.markPx ?? 0);
|
|
311
347
|
if (midPrice <= 0)
|
|
312
348
|
throw new Error(`Cannot get price for ${symbol} on dex ${this._dex}`);
|
|
313
|
-
|
|
349
|
+
const szDec = meta[0]?.universe?.[assetIndex]?.szDecimals ?? 2;
|
|
350
|
+
// HL official rounding: 5 significant figures, max 6 decimals for perps
|
|
351
|
+
const MAX_DECIMALS_PERP = 6;
|
|
314
352
|
const slippage = 0.05;
|
|
315
353
|
const isBuy = side === "buy";
|
|
316
354
|
const slippagePrice = isBuy ? midPrice * (1 + slippage) : midPrice * (1 - slippage);
|
|
317
|
-
const
|
|
318
|
-
|
|
355
|
+
const limitPrice = slippagePrice > 100_000
|
|
356
|
+
? String(Math.round(slippagePrice))
|
|
357
|
+
: Number(Number(slippagePrice.toPrecision(5)).toFixed(Math.max(0, MAX_DECIMALS_PERP - szDec))).toString();
|
|
319
358
|
return this._rawPlaceOrder({
|
|
320
359
|
assetIndex,
|
|
321
360
|
isBuy,
|
|
322
361
|
price: limitPrice,
|
|
323
|
-
size,
|
|
362
|
+
size: Number(size).toFixed(szDec),
|
|
324
363
|
orderType: { limit: { tif: "Ioc" } },
|
|
325
364
|
reduceOnly: false,
|
|
326
365
|
});
|
|
327
366
|
}
|
|
328
367
|
/**
|
|
329
|
-
*
|
|
330
|
-
*
|
|
368
|
+
* Sign and send any exchange action via raw EIP-712 signing.
|
|
369
|
+
* Bypasses the SDK entirely — works for order, batchModify, twapOrder, etc.
|
|
331
370
|
*/
|
|
332
|
-
|
|
371
|
+
ensureSigner() {
|
|
372
|
+
if (!this._evmSigner) {
|
|
373
|
+
throw new Error("No private key configured. Run: perp init");
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async _signAndSendAction(action) {
|
|
377
|
+
this.ensureSigner();
|
|
333
378
|
const { encode } = await import("@msgpack/msgpack");
|
|
334
379
|
const { ethers, keccak256 } = await import("ethers");
|
|
335
|
-
const wallet = new ethers.Wallet(this._privateKey);
|
|
336
380
|
const isMainnet = !this._testnet;
|
|
337
381
|
const baseUrl = isMainnet
|
|
338
382
|
? "https://api.hyperliquid.xyz"
|
|
339
383
|
: "https://api.hyperliquid-testnet.xyz";
|
|
340
|
-
// Remove trailing zeros (HL API requirement)
|
|
341
|
-
const trimZeros = (s) => {
|
|
342
|
-
if (!s.includes("."))
|
|
343
|
-
return s;
|
|
344
|
-
const n = s.replace(/\.?0+$/, "");
|
|
345
|
-
return n === "-0" ? "0" : n;
|
|
346
|
-
};
|
|
347
|
-
const orderWire = {
|
|
348
|
-
a: opts.assetIndex,
|
|
349
|
-
b: opts.isBuy,
|
|
350
|
-
p: trimZeros(opts.price),
|
|
351
|
-
s: trimZeros(opts.size),
|
|
352
|
-
r: opts.reduceOnly,
|
|
353
|
-
t: opts.orderType,
|
|
354
|
-
};
|
|
355
|
-
const action = {
|
|
356
|
-
type: "order",
|
|
357
|
-
orders: [orderWire],
|
|
358
|
-
grouping: "na",
|
|
359
|
-
};
|
|
360
|
-
// Add dex field for HIP-3 deployed perps
|
|
361
|
-
if (this._dex) {
|
|
362
|
-
action.dex = this._dex;
|
|
363
|
-
}
|
|
364
384
|
// Sign L1 action (replicates SDK's signL1Action)
|
|
365
385
|
const nonce = Date.now();
|
|
366
386
|
const msgPackBytes = encode(action);
|
|
@@ -386,7 +406,7 @@ export class HyperliquidAdapter {
|
|
|
386
406
|
source: isMainnet ? "a" : "b",
|
|
387
407
|
connectionId: hash,
|
|
388
408
|
};
|
|
389
|
-
const sig = await
|
|
409
|
+
const sig = await this._evmSigner.signTypedData(phantomDomain, agentTypes, phantomAgent);
|
|
390
410
|
const parsed = ethers.Signature.from(sig);
|
|
391
411
|
const payload = {
|
|
392
412
|
action,
|
|
@@ -405,18 +425,53 @@ export class HyperliquidAdapter {
|
|
|
405
425
|
}
|
|
406
426
|
return result;
|
|
407
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* Place an order via raw exchange API with EIP-712 signing.
|
|
430
|
+
* This bypasses the SDK's symbolConversion and supports dex-specific orders.
|
|
431
|
+
*/
|
|
432
|
+
async _rawPlaceOrder(opts) {
|
|
433
|
+
// Remove trailing zeros (HL API requirement)
|
|
434
|
+
const trimZeros = (s) => {
|
|
435
|
+
if (!s.includes("."))
|
|
436
|
+
return s;
|
|
437
|
+
const n = s.replace(/\.?0+$/, "");
|
|
438
|
+
return n === "-0" ? "0" : n;
|
|
439
|
+
};
|
|
440
|
+
const orderWire = {
|
|
441
|
+
a: opts.assetIndex,
|
|
442
|
+
b: opts.isBuy,
|
|
443
|
+
p: trimZeros(opts.price),
|
|
444
|
+
s: trimZeros(opts.size),
|
|
445
|
+
r: opts.reduceOnly,
|
|
446
|
+
t: opts.orderType,
|
|
447
|
+
};
|
|
448
|
+
const action = {
|
|
449
|
+
type: "order",
|
|
450
|
+
orders: [orderWire],
|
|
451
|
+
grouping: "na",
|
|
452
|
+
};
|
|
453
|
+
// Add dex field for HIP-3 deployed perps
|
|
454
|
+
if (this._dex) {
|
|
455
|
+
action.dex = this._dex;
|
|
456
|
+
}
|
|
457
|
+
return this._signAndSendAction(action);
|
|
458
|
+
}
|
|
408
459
|
async limitOrder(symbol, side, price, size, opts) {
|
|
409
460
|
// Normalize TIF: accept "IOC"/"GTC"/"ALO" (uppercase) or native "Ioc"/"Gtc"/"Alo"
|
|
410
461
|
const rawTif = opts?.tif ?? "Gtc";
|
|
411
462
|
const tifMap = { IOC: "Ioc", GTC: "Gtc", ALO: "Alo" };
|
|
412
463
|
const tif = (tifMap[rawTif.toUpperCase()] ?? rawTif);
|
|
413
464
|
const reduceOnly = opts?.reduceOnly ?? false;
|
|
465
|
+
// Round price/size to HL-supported decimals
|
|
466
|
+
const szDec = this.getSzDecimals(symbol);
|
|
467
|
+
const roundedPrice = this._hlRoundPrice(Number(price), szDec);
|
|
468
|
+
const roundedSize = Number(size).toFixed(szDec);
|
|
414
469
|
// Use _rawPlaceOrder for both DEX and non-DEX (SDK exchange.placeOrder is unavailable)
|
|
415
470
|
const result = await this._rawPlaceOrder({
|
|
416
|
-
assetIndex: this.getAssetIndex(symbol.toUpperCase()),
|
|
471
|
+
assetIndex: await this.getAssetIndex(symbol.toUpperCase()),
|
|
417
472
|
isBuy: side === "buy",
|
|
418
|
-
price,
|
|
419
|
-
size,
|
|
473
|
+
price: roundedPrice,
|
|
474
|
+
size: roundedSize,
|
|
420
475
|
orderType: { limit: { tif } },
|
|
421
476
|
reduceOnly,
|
|
422
477
|
});
|
|
@@ -424,17 +479,24 @@ export class HyperliquidAdapter {
|
|
|
424
479
|
return result;
|
|
425
480
|
}
|
|
426
481
|
async cancelOrder(symbol, orderId) {
|
|
482
|
+
this.ensureSigner();
|
|
483
|
+
// HL SDK expects coin in "ETH-PERP" format (internal symbol convention)
|
|
484
|
+
const resolved = this.resolveSymbol(symbol);
|
|
427
485
|
const result = await this.sdk.exchange.cancelOrder({
|
|
428
|
-
coin:
|
|
486
|
+
coin: resolved,
|
|
429
487
|
o: parseInt(orderId),
|
|
430
488
|
});
|
|
431
489
|
await this._invalidateAccountCache();
|
|
432
490
|
return result;
|
|
433
491
|
}
|
|
434
492
|
async cancelAllOrders(symbol) {
|
|
493
|
+
this.ensureSigner();
|
|
435
494
|
const orders = await this.getOpenOrders();
|
|
436
495
|
const toCancel = symbol
|
|
437
|
-
? orders.filter((o) =>
|
|
496
|
+
? orders.filter((o) => {
|
|
497
|
+
const norm = (s) => s.toUpperCase().replace(/-PERP$/, "");
|
|
498
|
+
return norm(o.symbol) === norm(symbol);
|
|
499
|
+
})
|
|
438
500
|
: orders;
|
|
439
501
|
const results = [];
|
|
440
502
|
for (const o of toCancel) {
|
|
@@ -447,7 +509,11 @@ export class HyperliquidAdapter {
|
|
|
447
509
|
}
|
|
448
510
|
// ── Interface methods ──
|
|
449
511
|
async editOrder(symbol, orderId, price, size) {
|
|
450
|
-
|
|
512
|
+
// Look up the existing order's side to preserve it (avoid flipping buy↔sell)
|
|
513
|
+
const openOrders = await this.getOpenOrders();
|
|
514
|
+
const existing = openOrders.find(o => o.orderId === orderId);
|
|
515
|
+
const side = existing?.side ?? "buy";
|
|
516
|
+
return this.modifyOrder(symbol, parseInt(orderId), side, price, size);
|
|
451
517
|
}
|
|
452
518
|
async setLeverage(symbol, leverage, marginMode = "cross") {
|
|
453
519
|
return this.updateLeverage(symbol, leverage, marginMode === "cross");
|
|
@@ -484,11 +550,13 @@ export class HyperliquidAdapter {
|
|
|
484
550
|
}));
|
|
485
551
|
}
|
|
486
552
|
async getOrderHistory(limit = 30) {
|
|
553
|
+
this.ensureAddress();
|
|
487
554
|
const fills = await this.client.info.getUserFills(this._address);
|
|
488
555
|
return (fills ?? []).slice(0, limit).map((f) => ({
|
|
489
556
|
orderId: String(f.oid ?? ""),
|
|
490
557
|
symbol: String(f.coin ?? ""),
|
|
491
|
-
|
|
558
|
+
// SDK convertSymbolsInObject already converts "B"→"buy", "A"→"sell"
|
|
559
|
+
side: String(f.side) === "B" ? "buy" : String(f.side) === "A" ? "sell" : f.side,
|
|
492
560
|
price: String(f.px ?? "0"),
|
|
493
561
|
size: String(f.sz ?? ""),
|
|
494
562
|
filled: String(f.sz ?? ""),
|
|
@@ -497,17 +565,20 @@ export class HyperliquidAdapter {
|
|
|
497
565
|
}));
|
|
498
566
|
}
|
|
499
567
|
async getTradeHistory(limit = 30) {
|
|
568
|
+
this.ensureAddress();
|
|
500
569
|
const fills = await this.client.info.getUserFills(this._address);
|
|
501
570
|
return (fills ?? []).slice(0, limit).map((f) => ({
|
|
502
571
|
time: Number(f.time ?? 0),
|
|
503
572
|
symbol: String(f.coin ?? ""),
|
|
504
|
-
|
|
573
|
+
// SDK convertSymbolsInObject already converts "B"→"buy", "A"→"sell"
|
|
574
|
+
side: String(f.side) === "B" ? "buy" : String(f.side) === "A" ? "sell" : f.side,
|
|
505
575
|
price: String(f.px ?? "0"),
|
|
506
576
|
size: String(f.sz ?? ""),
|
|
507
577
|
fee: String(f.fee ?? "0"),
|
|
508
578
|
}));
|
|
509
579
|
}
|
|
510
580
|
async getFundingPayments(limit = 30) {
|
|
581
|
+
this.ensureAddress();
|
|
511
582
|
const now = Date.now();
|
|
512
583
|
const history = await this.client.info.perpetuals.getUserFunding(this._address, now - 7 * 24 * 60 * 60 * 1000);
|
|
513
584
|
return (history ?? []).slice(0, limit).map((h) => {
|
|
@@ -525,7 +596,7 @@ export class HyperliquidAdapter {
|
|
|
525
596
|
return (history ?? []).slice(-limit).map((h) => ({
|
|
526
597
|
time: Number(h.time ?? 0),
|
|
527
598
|
rate: String(h.fundingRate ?? "0"),
|
|
528
|
-
price:
|
|
599
|
+
price: null,
|
|
529
600
|
}));
|
|
530
601
|
}
|
|
531
602
|
// ────────────────────────────────────────────────────────────
|
|
@@ -536,6 +607,7 @@ export class HyperliquidAdapter {
|
|
|
536
607
|
* Python SDK: order() with trigger type {"trigger": {triggerPx, isMarket, tpsl: "tp"|"sl"}}
|
|
537
608
|
*/
|
|
538
609
|
async triggerOrder(symbol, side, size, triggerPrice, tpsl, opts) {
|
|
610
|
+
this.ensureSigner();
|
|
539
611
|
const orderParams = {
|
|
540
612
|
coin: symbol.toUpperCase(),
|
|
541
613
|
is_buy: side === "buy",
|
|
@@ -559,7 +631,7 @@ export class HyperliquidAdapter {
|
|
|
559
631
|
* nktkas TS SDK: twapOrder({twap: {a, b, s, r, m, t}})
|
|
560
632
|
*/
|
|
561
633
|
async twapOrder(symbol, side, size, durationMinutes, opts) {
|
|
562
|
-
const assetIndex = this.getAssetIndex(symbol);
|
|
634
|
+
const assetIndex = await this.getAssetIndex(symbol);
|
|
563
635
|
const action = {
|
|
564
636
|
type: "twapOrder",
|
|
565
637
|
twap: {
|
|
@@ -577,7 +649,7 @@ export class HyperliquidAdapter {
|
|
|
577
649
|
* Cancel a TWAP order.
|
|
578
650
|
*/
|
|
579
651
|
async twapCancel(symbol, twapId) {
|
|
580
|
-
const assetIndex = this.getAssetIndex(symbol);
|
|
652
|
+
const assetIndex = await this.getAssetIndex(symbol);
|
|
581
653
|
const action = {
|
|
582
654
|
type: "twapCancel",
|
|
583
655
|
a: assetIndex,
|
|
@@ -590,6 +662,7 @@ export class HyperliquidAdapter {
|
|
|
590
662
|
* Uses SDK's built-in updateLeverage method.
|
|
591
663
|
*/
|
|
592
664
|
async updateLeverage(symbol, leverage, isCross = true) {
|
|
665
|
+
this.ensureSigner();
|
|
593
666
|
return this.sdk.exchange.updateLeverage(this.resolveSymbol(symbol), isCross ? "cross" : "isolated", leverage);
|
|
594
667
|
}
|
|
595
668
|
/**
|
|
@@ -597,6 +670,7 @@ export class HyperliquidAdapter {
|
|
|
597
670
|
* amount > 0 to add margin, amount < 0 to remove
|
|
598
671
|
*/
|
|
599
672
|
async updateIsolatedMargin(symbol, amount) {
|
|
673
|
+
this.ensureSigner();
|
|
600
674
|
return this.sdk.exchange.updateIsolatedMargin(this.resolveSymbol(symbol), amount > 0, Math.round(Math.abs(amount) * 1e6));
|
|
601
675
|
}
|
|
602
676
|
/**
|
|
@@ -604,6 +678,7 @@ export class HyperliquidAdapter {
|
|
|
604
678
|
* Python SDK: withdraw_from_bridge(amount, destination) → action type "withdraw3"
|
|
605
679
|
*/
|
|
606
680
|
async withdraw(amount, destination) {
|
|
681
|
+
this.ensureSigner();
|
|
607
682
|
try {
|
|
608
683
|
return await this.sdk.exchange.initiateWithdrawal(destination, parseFloat(amount));
|
|
609
684
|
}
|
|
@@ -625,6 +700,7 @@ export class HyperliquidAdapter {
|
|
|
625
700
|
* Python SDK: usd_transfer(amount, destination)
|
|
626
701
|
*/
|
|
627
702
|
async usdTransfer(amount, destination) {
|
|
703
|
+
this.ensureSigner();
|
|
628
704
|
return this.sdk.exchange.usdTransfer(destination, amount);
|
|
629
705
|
}
|
|
630
706
|
/**
|
|
@@ -653,7 +729,15 @@ export class HyperliquidAdapter {
|
|
|
653
729
|
* Modify an existing order.
|
|
654
730
|
*/
|
|
655
731
|
async modifyOrder(symbol, orderId, newSide, newPrice, newSize, opts) {
|
|
656
|
-
const assetIndex = this.getAssetIndex(symbol);
|
|
732
|
+
const assetIndex = await this.getAssetIndex(symbol);
|
|
733
|
+
const szDec = this.getSzDecimals(symbol);
|
|
734
|
+
// Remove trailing zeros (HL API requirement)
|
|
735
|
+
const trimZeros = (s) => {
|
|
736
|
+
if (!s.includes("."))
|
|
737
|
+
return s;
|
|
738
|
+
const n = s.replace(/\.?0+$/, "");
|
|
739
|
+
return n === "-0" ? "0" : n;
|
|
740
|
+
};
|
|
657
741
|
const action = {
|
|
658
742
|
type: "batchModify",
|
|
659
743
|
modifies: [{
|
|
@@ -661,14 +745,14 @@ export class HyperliquidAdapter {
|
|
|
661
745
|
order: {
|
|
662
746
|
a: assetIndex,
|
|
663
747
|
b: newSide === "buy",
|
|
664
|
-
p: newPrice,
|
|
665
|
-
s: newSize,
|
|
748
|
+
p: trimZeros(this._hlRoundPrice(Number(newPrice), szDec)),
|
|
749
|
+
s: trimZeros(Number(newSize).toFixed(szDec)),
|
|
666
750
|
r: opts?.reduceOnly ?? false,
|
|
667
751
|
t: { limit: { tif: "Gtc" } },
|
|
668
752
|
},
|
|
669
753
|
}],
|
|
670
754
|
};
|
|
671
|
-
return this.
|
|
755
|
+
return this._signAndSendAction(action);
|
|
672
756
|
}
|
|
673
757
|
/**
|
|
674
758
|
* Schedule cancel: cancel all orders at a future time.
|
|
@@ -722,6 +806,7 @@ export class HyperliquidAdapter {
|
|
|
722
806
|
* Get user trade fills.
|
|
723
807
|
*/
|
|
724
808
|
async getUserFills(startTime, endTime) {
|
|
809
|
+
this.ensureAddress();
|
|
725
810
|
const baseUrl = this._testnet
|
|
726
811
|
? "https://api.hyperliquid-testnet.xyz"
|
|
727
812
|
: "https://api.hyperliquid.xyz";
|
|
@@ -744,6 +829,7 @@ export class HyperliquidAdapter {
|
|
|
744
829
|
* Get user portfolio analytics.
|
|
745
830
|
*/
|
|
746
831
|
async getPortfolio() {
|
|
832
|
+
this.ensureAddress();
|
|
747
833
|
const baseUrl = this._testnet
|
|
748
834
|
? "https://api.hyperliquid-testnet.xyz"
|
|
749
835
|
: "https://api.hyperliquid.xyz";
|
|
@@ -758,6 +844,7 @@ export class HyperliquidAdapter {
|
|
|
758
844
|
* Query a specific order by OID.
|
|
759
845
|
*/
|
|
760
846
|
async queryOrder(orderId) {
|
|
847
|
+
this.ensureAddress();
|
|
761
848
|
const baseUrl = this._testnet
|
|
762
849
|
? "https://api.hyperliquid-testnet.xyz"
|
|
763
850
|
: "https://api.hyperliquid.xyz";
|
|
@@ -847,42 +934,49 @@ export class HyperliquidAdapter {
|
|
|
847
934
|
* Get referral info.
|
|
848
935
|
*/
|
|
849
936
|
async getReferralInfo() {
|
|
937
|
+
this.ensureAddress();
|
|
850
938
|
return this._infoPost({ type: "referral", user: this._address });
|
|
851
939
|
}
|
|
852
940
|
/**
|
|
853
941
|
* Get fee info.
|
|
854
942
|
*/
|
|
855
943
|
async getUserFees() {
|
|
944
|
+
this.ensureAddress();
|
|
856
945
|
return this._infoPost({ type: "userFees", user: this._address });
|
|
857
946
|
}
|
|
858
947
|
/**
|
|
859
948
|
* Get sub-accounts.
|
|
860
949
|
*/
|
|
861
950
|
async getSubAccounts() {
|
|
951
|
+
this.ensureAddress();
|
|
862
952
|
return this._infoPost({ type: "subAccounts", user: this._address });
|
|
863
953
|
}
|
|
864
954
|
/**
|
|
865
955
|
* Get historical orders (up to 2000).
|
|
866
956
|
*/
|
|
867
957
|
async getHistoricalOrders() {
|
|
958
|
+
this.ensureAddress();
|
|
868
959
|
return this._infoPost({ type: "historicalOrders", user: this._address });
|
|
869
960
|
}
|
|
870
961
|
/**
|
|
871
962
|
* Get approved builders.
|
|
872
963
|
*/
|
|
873
964
|
async getApprovedBuilders() {
|
|
965
|
+
this.ensureAddress();
|
|
874
966
|
return this._infoPost({ type: "approvedBuilders", user: this._address });
|
|
875
967
|
}
|
|
876
968
|
/**
|
|
877
969
|
* Get vault details.
|
|
878
970
|
*/
|
|
879
971
|
async getVaultDetails(vaultAddress) {
|
|
972
|
+
this.ensureAddress();
|
|
880
973
|
return this._infoPost({ type: "vaultDetails", vaultAddress, user: this._address });
|
|
881
974
|
}
|
|
882
975
|
/**
|
|
883
976
|
* Get delegations (staking).
|
|
884
977
|
*/
|
|
885
978
|
async getDelegations() {
|
|
979
|
+
this.ensureAddress();
|
|
886
980
|
return this._infoPost({ type: "delegations", user: this._address });
|
|
887
981
|
}
|
|
888
982
|
/**
|
|
@@ -899,20 +993,24 @@ export class HyperliquidAdapter {
|
|
|
899
993
|
});
|
|
900
994
|
return res.json();
|
|
901
995
|
}
|
|
996
|
+
/**
|
|
997
|
+
* Public entry point for sending signed exchange actions.
|
|
998
|
+
* Used by HyperliquidSpotAdapter to delegate signing.
|
|
999
|
+
*/
|
|
1000
|
+
async exchangeAction(action) {
|
|
1001
|
+
return this._signAndSendAction(action);
|
|
1002
|
+
}
|
|
902
1003
|
/**
|
|
903
1004
|
* Send a raw exchange action through the SDK.
|
|
904
1005
|
* Used for methods not directly exposed by the SDK.
|
|
905
1006
|
*/
|
|
906
1007
|
async _sendExchangeAction(action) {
|
|
907
1008
|
const exchange = this.sdk.exchange;
|
|
908
|
-
// Try using the SDK's internal
|
|
1009
|
+
// Try using the SDK's internal postAction if available
|
|
909
1010
|
if (typeof exchange.postAction === "function") {
|
|
910
1011
|
return exchange.postAction(action);
|
|
911
1012
|
}
|
|
912
|
-
// Fallback:
|
|
913
|
-
|
|
914
|
-
// If we get here, the SDK doesn't expose the method we need
|
|
915
|
-
throw new Error(`Hyperliquid SDK does not expose postAction(). ` +
|
|
916
|
-
`Action "${action.type}" may need a newer SDK version.`);
|
|
1013
|
+
// Fallback: sign and send directly (bypasses SDK)
|
|
1014
|
+
return this._signAndSendAction(action);
|
|
917
1015
|
}
|
|
918
1016
|
}
|
|
@@ -70,7 +70,7 @@ export interface ExchangeAdapter {
|
|
|
70
70
|
getFundingHistory(symbol: string, limit?: number): Promise<{
|
|
71
71
|
time: number;
|
|
72
72
|
rate: string;
|
|
73
|
-
price: string;
|
|
73
|
+
price: string | null;
|
|
74
74
|
}[]>;
|
|
75
75
|
getKlines(symbol: string, interval: string, startTime: number, endTime: number): Promise<ExchangeKline[]>;
|
|
76
76
|
getBalance(): Promise<ExchangeBalance>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lighter Spot Adapter.
|
|
3
|
+
*
|
|
4
|
+
* Uses the same `/api/v1/sendTx` endpoint as perps — the only difference
|
|
5
|
+
* is the market_id (spot markets have different IDs from perp markets).
|
|
6
|
+
*
|
|
7
|
+
* Composes with LighterAdapter for signing and API access.
|
|
8
|
+
*/
|
|
9
|
+
import type { SpotAdapter, SpotMarketInfo, SpotBalance } from "./spot-interface.js";
|
|
10
|
+
import type { LighterAdapter } from "./lighter.js";
|
|
11
|
+
export declare class LighterSpotAdapter implements SpotAdapter {
|
|
12
|
+
readonly name = "lighter";
|
|
13
|
+
private _lt;
|
|
14
|
+
private _spotMarketMap;
|
|
15
|
+
private _spotBaseMap;
|
|
16
|
+
private _spotDecimals;
|
|
17
|
+
private _spotMinSize;
|
|
18
|
+
private _initialized;
|
|
19
|
+
constructor(ltAdapter: LighterAdapter);
|
|
20
|
+
init(): Promise<void>;
|
|
21
|
+
private _loadSpotMarkets;
|
|
22
|
+
/** Resolve a symbol (e.g., "ETH" or "ETH/USDC") to the full spot symbol */
|
|
23
|
+
private resolveSymbol;
|
|
24
|
+
getSpotMarkets(): Promise<SpotMarketInfo[]>;
|
|
25
|
+
getSpotOrderbook(symbol: string): Promise<{
|
|
26
|
+
bids: [string, string][];
|
|
27
|
+
asks: [string, string][];
|
|
28
|
+
}>;
|
|
29
|
+
getSpotBalances(): Promise<SpotBalance[]>;
|
|
30
|
+
spotMarketOrder(symbol: string, side: "buy" | "sell", size: string): Promise<unknown>;
|
|
31
|
+
spotLimitOrder(symbol: string, side: "buy" | "sell", price: string, size: string, opts?: {
|
|
32
|
+
tif?: string;
|
|
33
|
+
}): Promise<unknown>;
|
|
34
|
+
spotCancelOrder(symbol: string, orderId: string): Promise<unknown>;
|
|
35
|
+
/** Transfer USDC from perp account to spot account (required before spot buys) */
|
|
36
|
+
transferUsdcToSpot(amount: number): Promise<unknown>;
|
|
37
|
+
/** Transfer USDC from spot account back to perp account */
|
|
38
|
+
transferUsdcToPerp(amount: number): Promise<unknown>;
|
|
39
|
+
/**
|
|
40
|
+
* Internal self-transfer using raw WASM signTransfer.
|
|
41
|
+
* TS SDK's is_spot_account mapping is buggy (sets both fromRoute and toRoute
|
|
42
|
+
* to the same value), so we call the WASM module directly with explicit routes.
|
|
43
|
+
* Route types: 0 = Perp, 1 = Spot
|
|
44
|
+
*/
|
|
45
|
+
private _selfTransfer;
|
|
46
|
+
/** Get the spot market ID for a symbol */
|
|
47
|
+
getSpotMarketId(symbol: string): number | undefined;
|
|
48
|
+
/** Check if a base token has a spot market */
|
|
49
|
+
hasSpotMarket(base: string): boolean;
|
|
50
|
+
private _placeOrder;
|
|
51
|
+
private _getNextNonce;
|
|
52
|
+
private _sendTx;
|
|
53
|
+
private _restGet;
|
|
54
|
+
private _restGetAuth;
|
|
55
|
+
}
|