openbroker 1.9.3 → 1.9.5
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/CHANGELOG.md +17 -0
- package/README.md +31 -0
- package/SKILL.md +21 -4
- package/bin/cli.ts +4 -2
- package/dist/auto/cli.js +1 -1
- package/dist/auto/examples/price-alert.js +1 -1
- package/dist/auto/realtime.d.ts +45 -0
- package/dist/auto/realtime.d.ts.map +1 -0
- package/dist/auto/realtime.js +177 -0
- package/dist/auto/realtime.test.d.ts +2 -0
- package/dist/auto/realtime.test.d.ts.map +1 -0
- package/dist/auto/realtime.test.js +73 -0
- package/dist/auto/runtime.d.ts +3 -3
- package/dist/auto/runtime.d.ts.map +1 -1
- package/dist/auto/runtime.js +117 -45
- package/dist/auto/types.d.ts +2 -1
- package/dist/auto/types.d.ts.map +1 -1
- package/dist/core/client.d.ts +66 -1
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +141 -4
- package/dist/core/ws.d.ts +14 -2
- package/dist/core/ws.d.ts.map +1 -1
- package/dist/core/ws.js +53 -7
- package/dist/setup/install.js +100 -3
- package/dist/setup/package-catalog.d.ts +12 -0
- package/dist/setup/package-catalog.d.ts.map +1 -0
- package/dist/setup/package-catalog.js +36 -0
- package/dist/setup/package-catalog.test.d.ts +2 -0
- package/dist/setup/package-catalog.test.d.ts.map +1 -0
- package/dist/setup/package-catalog.test.js +31 -0
- package/package.json +5 -3
- package/scripts/auto/cli.ts +1 -1
- package/scripts/auto/examples/price-alert.ts +1 -1
- package/scripts/auto/realtime.test.ts +84 -0
- package/scripts/auto/realtime.ts +194 -0
- package/scripts/auto/runtime.ts +125 -47
- package/scripts/auto/types.ts +2 -1
- package/scripts/core/client.ts +175 -4
- package/scripts/core/ws.ts +57 -8
- package/scripts/setup/install.ts +110 -3
- package/scripts/setup/package-catalog.test.ts +50 -0
- package/scripts/setup/package-catalog.ts +49 -0
package/scripts/auto/types.ts
CHANGED
|
@@ -220,7 +220,7 @@ export interface AutomationAPI {
|
|
|
220
220
|
/** Subscribe to a market/account event */
|
|
221
221
|
on<E extends AutomationEventType>(event: E, handler: AutomationEventHandler<E>): void;
|
|
222
222
|
|
|
223
|
-
/** Run a handler on
|
|
223
|
+
/** Run a handler on its own recurring scheduler, independent of REST reconciliation. */
|
|
224
224
|
every(intervalMs: number, handler: () => void | Promise<void>): void;
|
|
225
225
|
|
|
226
226
|
/** Called after all handlers are registered and polling begins */
|
|
@@ -290,6 +290,7 @@ export interface ScheduledTask {
|
|
|
290
290
|
intervalMs: number;
|
|
291
291
|
handler: () => void | Promise<void>;
|
|
292
292
|
lastRun: number;
|
|
293
|
+
running?: boolean;
|
|
293
294
|
}
|
|
294
295
|
|
|
295
296
|
export interface RunningAutomation {
|
package/scripts/core/client.ts
CHANGED
|
@@ -21,6 +21,28 @@ import type {
|
|
|
21
21
|
import { loadConfig, isMainnet } from './config.js';
|
|
22
22
|
import { roundPrice, roundSize } from './utils.js';
|
|
23
23
|
|
|
24
|
+
export interface RealtimeBookSnapshot {
|
|
25
|
+
coin: string;
|
|
26
|
+
time: number;
|
|
27
|
+
levels: [
|
|
28
|
+
Array<{ px: string; sz: string; n: number }>,
|
|
29
|
+
Array<{ px: string; sz: string; n: number }>,
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RealtimeDataProvider {
|
|
34
|
+
readonly connected: boolean;
|
|
35
|
+
getAllMids(): Record<string, string> | null;
|
|
36
|
+
getMainAssetCtxs(): AssetCtx[] | null;
|
|
37
|
+
getUserState(user: string, dex?: string): ClearinghouseState | null;
|
|
38
|
+
getUserStateAll(user: string): ClearinghouseState | null;
|
|
39
|
+
getSpotBalances(user: string): {
|
|
40
|
+
balances: Array<{ coin: string; token: number; hold: string; total: string; entryNtl: string }>;
|
|
41
|
+
} | null;
|
|
42
|
+
getOpenOrders(user: string): OpenOrder[] | null;
|
|
43
|
+
getL2Book(coin: string): Promise<RealtimeBookSnapshot | null>;
|
|
44
|
+
}
|
|
45
|
+
|
|
24
46
|
export class HyperliquidClient {
|
|
25
47
|
private config: OpenBrokerConfig;
|
|
26
48
|
private account: PrivateKeyAccount;
|
|
@@ -33,6 +55,8 @@ export class HyperliquidClient {
|
|
|
33
55
|
private szDecimalsMap: Map<string, number> = new Map();
|
|
34
56
|
/** Maps coin name → dex info for HIP-3 assets. Main dex assets have dexName=null */
|
|
35
57
|
private coinDexMap: Map<string, { dexName: string | null; dexIdx: number; localName: string }> = new Map();
|
|
58
|
+
/** Static positional universe per dex, used to join allDexsAssetCtxs WS pushes. */
|
|
59
|
+
private dexUniverseMap: Map<string, string[]> = new Map();
|
|
36
60
|
/** Cache of perpDexs list */
|
|
37
61
|
private perpDexsCache: Array<{ name: string; fullName: string; deployer: string } | null> | null = null;
|
|
38
62
|
/** Whether HIP-3 assets have been loaded into maps */
|
|
@@ -63,6 +87,16 @@ export class HyperliquidClient {
|
|
|
63
87
|
private spotMetaLoaded: boolean = false;
|
|
64
88
|
/** HIP-4 outcome metadata cache */
|
|
65
89
|
private outcomeMeta: OutcomeMetaResponse | null = null;
|
|
90
|
+
/** Static main-dex universe retained while live asset contexts arrive over WebSocket. */
|
|
91
|
+
private staticMeta: MetaAndAssetCtxs['meta'] | null = null;
|
|
92
|
+
/** Runtime-owned live data cache. CLI calls leave this detached and remain REST-only. */
|
|
93
|
+
private realtime: RealtimeDataProvider | null = null;
|
|
94
|
+
private predictedFundingsCache: {
|
|
95
|
+
value: Array<[string, Array<[string, { fundingRate: string; nextFundingTime: number }]>]>;
|
|
96
|
+
timestamp: number;
|
|
97
|
+
} | null = null;
|
|
98
|
+
private predictedFundingsInFlight: Promise<Array<[string, Array<[string, { fundingRate: string; nextFundingTime: number }]>]>> | null = null;
|
|
99
|
+
private spotMetaCache: Awaited<ReturnType<HyperliquidClient['getSpotMeta']>> | null = null;
|
|
66
100
|
public verbose: boolean = false;
|
|
67
101
|
|
|
68
102
|
constructor(config?: OpenBrokerConfig) {
|
|
@@ -226,6 +260,11 @@ export class HyperliquidClient {
|
|
|
226
260
|
return !isMainnet();
|
|
227
261
|
}
|
|
228
262
|
|
|
263
|
+
/** Attach/detach the automation runtime's WebSocket-first data cache. */
|
|
264
|
+
setRealtimeDataProvider(provider: RealtimeDataProvider | null): void {
|
|
265
|
+
this.realtime = provider;
|
|
266
|
+
}
|
|
267
|
+
|
|
229
268
|
/**
|
|
230
269
|
* Returns vaultAddress param for SDK exchange calls.
|
|
231
270
|
* Only used for vault trading (HYPERLIQUID_VAULT_ADDRESS set explicitly).
|
|
@@ -255,6 +294,10 @@ export class HyperliquidClient {
|
|
|
255
294
|
// ============ Market Data ============
|
|
256
295
|
|
|
257
296
|
async getMetaAndAssetCtxs(): Promise<MetaAndAssetCtxs> {
|
|
297
|
+
const liveCtxs = this.realtime?.connected ? this.realtime.getMainAssetCtxs() : null;
|
|
298
|
+
if (this.staticMeta && liveCtxs) {
|
|
299
|
+
return { meta: this.staticMeta, assetCtxs: liveCtxs };
|
|
300
|
+
}
|
|
258
301
|
if (this.meta) return this.meta;
|
|
259
302
|
|
|
260
303
|
this.log('Fetching metaAndAssetCtxs...');
|
|
@@ -277,6 +320,8 @@ export class HyperliquidClient {
|
|
|
277
320
|
assetCtxs: response[1] as AssetCtx[],
|
|
278
321
|
};
|
|
279
322
|
this.meta = meta;
|
|
323
|
+
this.staticMeta = meta.meta;
|
|
324
|
+
this.dexUniverseMap.set('', meta.meta.universe.map((asset) => asset.name));
|
|
280
325
|
|
|
281
326
|
// Build lookup maps for main dex
|
|
282
327
|
meta.meta.universe.forEach((asset, index) => {
|
|
@@ -294,6 +339,29 @@ export class HyperliquidClient {
|
|
|
294
339
|
return meta;
|
|
295
340
|
}
|
|
296
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Warm only the native/main static universe for an automation runtime.
|
|
344
|
+
* Dynamic contexts arrive via allDexsAssetCtxs, and HIP-3 metadata is loaded
|
|
345
|
+
* on demand when a strategy references a prefixed market. This avoids the
|
|
346
|
+
* old startup burst that fetched every HIP-3 dex before the socket opened.
|
|
347
|
+
*/
|
|
348
|
+
async initializeRealtimeMetadata(): Promise<void> {
|
|
349
|
+
if (this.staticMeta) return;
|
|
350
|
+
const response = await this.withRetry(() => this.info.metaAndAssetCtxs(), 'metaAndAssetCtxs(realtime-init)');
|
|
351
|
+
const meta: MetaAndAssetCtxs = {
|
|
352
|
+
meta: { universe: response[0].universe as AssetMeta[] },
|
|
353
|
+
assetCtxs: response[1] as AssetCtx[],
|
|
354
|
+
};
|
|
355
|
+
this.meta = meta;
|
|
356
|
+
this.staticMeta = meta.meta;
|
|
357
|
+
this.dexUniverseMap.set('', meta.meta.universe.map((asset) => asset.name));
|
|
358
|
+
meta.meta.universe.forEach((asset, index) => {
|
|
359
|
+
this.assetMap.set(asset.name, index);
|
|
360
|
+
this.szDecimalsMap.set(asset.name, asset.szDecimals);
|
|
361
|
+
this.coinDexMap.set(asset.name, { dexName: null, dexIdx: 0, localName: asset.name });
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
297
365
|
/**
|
|
298
366
|
* Load HIP-3 perp dex assets into the asset/szDecimals maps.
|
|
299
367
|
* Asset index formula: 100000 + dexIdx * 10000 + assetIdx
|
|
@@ -422,11 +490,16 @@ export class HyperliquidClient {
|
|
|
422
490
|
this.coinDexMap.set(coinName, { dexName, dexIdx, localName });
|
|
423
491
|
if (asset.maxLeverage) this.hip3MaxLeverageMap.set(coinName, asset.maxLeverage);
|
|
424
492
|
});
|
|
493
|
+
this.dexUniverseMap.set(dexName, universe.map((asset) => (
|
|
494
|
+
asset.name.startsWith(`${dexName}:`) ? asset.name : `${dexName}:${asset.name}`
|
|
495
|
+
)));
|
|
425
496
|
}
|
|
426
497
|
this.loadedHip3Dexes.add(dexName);
|
|
427
498
|
}
|
|
428
499
|
|
|
429
500
|
async getAllMids(): Promise<Record<string, string>> {
|
|
501
|
+
const live = this.realtime?.connected ? this.realtime.getAllMids() : null;
|
|
502
|
+
if (live) return { ...live };
|
|
430
503
|
this.log('Fetching allMids...');
|
|
431
504
|
let response: Record<string, string>;
|
|
432
505
|
try {
|
|
@@ -584,6 +657,7 @@ export class HyperliquidClient {
|
|
|
584
657
|
isCanonical: boolean;
|
|
585
658
|
}>;
|
|
586
659
|
}> {
|
|
660
|
+
if (this.spotMetaCache) return this.spotMetaCache;
|
|
587
661
|
this.log('Fetching spotMeta...');
|
|
588
662
|
const data = await this.postInfo<{
|
|
589
663
|
tokens: Array<{
|
|
@@ -602,6 +676,7 @@ export class HyperliquidClient {
|
|
|
602
676
|
isCanonical: boolean;
|
|
603
677
|
}>;
|
|
604
678
|
}>({ type: 'spotMeta' }, 'spotMeta');
|
|
679
|
+
this.spotMetaCache = data;
|
|
605
680
|
this.log('spotMeta response:', JSON.stringify(data).slice(0, 500));
|
|
606
681
|
return data;
|
|
607
682
|
}
|
|
@@ -634,6 +709,23 @@ export class HyperliquidClient {
|
|
|
634
709
|
prevDayPx: string;
|
|
635
710
|
}>;
|
|
636
711
|
}> {
|
|
712
|
+
const liveMids = this.realtime?.connected ? this.realtime.getAllMids() : null;
|
|
713
|
+
if (liveMids) {
|
|
714
|
+
const meta = await this.getSpotMeta();
|
|
715
|
+
return {
|
|
716
|
+
meta,
|
|
717
|
+
assetCtxs: meta.universe.map((pair) => {
|
|
718
|
+
const price = liveMids[pair.name] ?? '0';
|
|
719
|
+
return {
|
|
720
|
+
coin: pair.name,
|
|
721
|
+
dayNtlVlm: '0',
|
|
722
|
+
markPx: price,
|
|
723
|
+
midPx: price,
|
|
724
|
+
prevDayPx: price,
|
|
725
|
+
};
|
|
726
|
+
}),
|
|
727
|
+
};
|
|
728
|
+
}
|
|
637
729
|
this.log('Fetching spotMetaAndAssetCtxs...');
|
|
638
730
|
const data = await this.postInfo<unknown>(
|
|
639
731
|
{ type: 'spotMetaAndAssetCtxs' },
|
|
@@ -1003,6 +1095,9 @@ export class HyperliquidClient {
|
|
|
1003
1095
|
entryNtl: string;
|
|
1004
1096
|
}>;
|
|
1005
1097
|
}> {
|
|
1098
|
+
const target = user ?? this.address;
|
|
1099
|
+
const live = this.realtime?.connected ? this.realtime.getSpotBalances(target) : null;
|
|
1100
|
+
if (live) return live;
|
|
1006
1101
|
this.log('Fetching spotClearinghouseState for:', user ?? this.address);
|
|
1007
1102
|
const data = await this.postInfo<{
|
|
1008
1103
|
balances: Array<{
|
|
@@ -1076,6 +1171,32 @@ export class HyperliquidClient {
|
|
|
1076
1171
|
async getPredictedFundings(): Promise<Array<[
|
|
1077
1172
|
string, // coin
|
|
1078
1173
|
Array<[string, { fundingRate: string; nextFundingTime: number }]> // venue funding rates
|
|
1174
|
+
]>> {
|
|
1175
|
+
const ttlMs = 60_000;
|
|
1176
|
+
if (this.predictedFundingsCache && Date.now() - this.predictedFundingsCache.timestamp < ttlMs) {
|
|
1177
|
+
return this.predictedFundingsCache.value;
|
|
1178
|
+
}
|
|
1179
|
+
if (this.predictedFundingsInFlight) return this.predictedFundingsInFlight;
|
|
1180
|
+
|
|
1181
|
+
this.predictedFundingsInFlight = this.fetchPredictedFundings();
|
|
1182
|
+
try {
|
|
1183
|
+
const value = await this.predictedFundingsInFlight;
|
|
1184
|
+
this.predictedFundingsCache = { value, timestamp: Date.now() };
|
|
1185
|
+
return value;
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
if (this.predictedFundingsCache) {
|
|
1188
|
+
this.log('predictedFundings refresh failed; using stale cache:', this.describeError(error));
|
|
1189
|
+
return this.predictedFundingsCache.value;
|
|
1190
|
+
}
|
|
1191
|
+
throw error;
|
|
1192
|
+
} finally {
|
|
1193
|
+
this.predictedFundingsInFlight = null;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
private async fetchPredictedFundings(): Promise<Array<[
|
|
1198
|
+
string,
|
|
1199
|
+
Array<[string, { fundingRate: string; nextFundingTime: number }]>
|
|
1079
1200
|
]>> {
|
|
1080
1201
|
this.log('Fetching predictedFundings...');
|
|
1081
1202
|
const baseUrl = isMainnet()
|
|
@@ -1091,11 +1212,15 @@ export class HyperliquidClient {
|
|
|
1091
1212
|
},
|
|
1092
1213
|
body: JSON.stringify({ type: 'predictedFundings' }),
|
|
1093
1214
|
});
|
|
1215
|
+
if (!response.ok) {
|
|
1216
|
+
throw new Error(`predictedFundings failed: HTTP ${response.status} ${response.statusText}`);
|
|
1217
|
+
}
|
|
1094
1218
|
const data = await response.json() as Array<[
|
|
1095
1219
|
string,
|
|
1096
1220
|
Array<[string, { fundingRate: string; nextFundingTime: number }]>
|
|
1097
1221
|
]>;
|
|
1098
|
-
|
|
1222
|
+
if (!Array.isArray(data)) throw new Error('predictedFundings returned malformed payload');
|
|
1223
|
+
this.log('predictedFundings response length:', data.length);
|
|
1099
1224
|
return data;
|
|
1100
1225
|
}
|
|
1101
1226
|
|
|
@@ -1112,6 +1237,9 @@ export class HyperliquidClient {
|
|
|
1112
1237
|
spread: number;
|
|
1113
1238
|
spreadBps: number;
|
|
1114
1239
|
}> {
|
|
1240
|
+
const live = this.realtime?.connected ? await this.realtime.getL2Book(coin) : null;
|
|
1241
|
+
if (live) return this.normalizeL2Book(live.levels);
|
|
1242
|
+
|
|
1115
1243
|
this.log('Fetching l2Book for:', coin);
|
|
1116
1244
|
// API accepts prefixed names directly (e.g., "xyz:CL")
|
|
1117
1245
|
let response: Awaited<ReturnType<typeof this.info.l2Book>>;
|
|
@@ -1129,8 +1257,20 @@ export class HyperliquidClient {
|
|
|
1129
1257
|
throw new Error(`l2Book(${coin}) returned empty/malformed payload.`);
|
|
1130
1258
|
}
|
|
1131
1259
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1260
|
+
return this.normalizeL2Book(response.levels as RealtimeBookSnapshot['levels']);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
private normalizeL2Book(levels: RealtimeBookSnapshot['levels']): {
|
|
1264
|
+
bids: Array<{ px: string; sz: string; n: number }>;
|
|
1265
|
+
asks: Array<{ px: string; sz: string; n: number }>;
|
|
1266
|
+
bestBid: number;
|
|
1267
|
+
bestAsk: number;
|
|
1268
|
+
midPrice: number;
|
|
1269
|
+
spread: number;
|
|
1270
|
+
spreadBps: number;
|
|
1271
|
+
} {
|
|
1272
|
+
const bids = levels[0] ?? [];
|
|
1273
|
+
const asks = levels[1] ?? [];
|
|
1134
1274
|
|
|
1135
1275
|
const bestBid = bids.length > 0 ? parseFloat(bids[0].px) : 0;
|
|
1136
1276
|
const bestAsk = asks.length > 0 ? parseFloat(asks[0].px) : 0;
|
|
@@ -1266,6 +1406,28 @@ export class HyperliquidClient {
|
|
|
1266
1406
|
// Keep the asset/szDecimals/coinDex maps - they don't change
|
|
1267
1407
|
}
|
|
1268
1408
|
|
|
1409
|
+
/** Join an allDexsAssetCtxs WebSocket payload with the cached static universes. */
|
|
1410
|
+
fundingRatesFromWs(ctxs: ReadonlyArray<[
|
|
1411
|
+
string,
|
|
1412
|
+
ReadonlyArray<{ funding?: string | number | null; premium?: string | number | null }>,
|
|
1413
|
+
]>): Map<string, { rate: number; premium: number }> {
|
|
1414
|
+
const rates = new Map<string, { rate: number; premium: number }>();
|
|
1415
|
+
for (const [dexName, values] of ctxs) {
|
|
1416
|
+
const universe = this.dexUniverseMap.get(dexName || '');
|
|
1417
|
+
if (!universe) continue;
|
|
1418
|
+
for (let index = 0; index < Math.min(universe.length, values.length); index++) {
|
|
1419
|
+
const coin = universe[index];
|
|
1420
|
+
const value = values[index];
|
|
1421
|
+
if (!coin || !value) continue;
|
|
1422
|
+
rates.set(coin, {
|
|
1423
|
+
rate: Number(value.funding ?? 0),
|
|
1424
|
+
premium: Number(value.premium ?? 0),
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
return rates;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1269
1431
|
/**
|
|
1270
1432
|
* Get all loaded asset names (main + HIP-3)
|
|
1271
1433
|
*/
|
|
@@ -1952,6 +2114,9 @@ export class HyperliquidClient {
|
|
|
1952
2114
|
}
|
|
1953
2115
|
|
|
1954
2116
|
async getUserState(user?: string, dex?: string): Promise<ClearinghouseState> {
|
|
2117
|
+
const target = user ?? this.address;
|
|
2118
|
+
const live = this.realtime?.connected ? this.realtime.getUserState(target, dex) : null;
|
|
2119
|
+
if (live) return live;
|
|
1955
2120
|
this.log('Fetching clearinghouseState for:', user ?? this.address, dex ? `dex: ${dex}` : '');
|
|
1956
2121
|
const params: { user: string; dex?: string } = { user: user ?? this.address };
|
|
1957
2122
|
if (dex !== undefined) params.dex = dex;
|
|
@@ -2107,6 +2272,9 @@ export class HyperliquidClient {
|
|
|
2107
2272
|
* For standard accounts: aggregates margin summaries from each dex.
|
|
2108
2273
|
*/
|
|
2109
2274
|
async getUserStateAll(user?: string): Promise<ClearinghouseState> {
|
|
2275
|
+
const target = user ?? this.address;
|
|
2276
|
+
const live = this.realtime?.connected ? this.realtime.getUserStateAll(target) : null;
|
|
2277
|
+
if (live) return live;
|
|
2110
2278
|
await this.getMetaAndAssetCtxs(); // Ensure HIP-3 dex list is loaded
|
|
2111
2279
|
|
|
2112
2280
|
const unified = await this.isUnifiedAccount(user);
|
|
@@ -2149,7 +2317,10 @@ export class HyperliquidClient {
|
|
|
2149
2317
|
}
|
|
2150
2318
|
|
|
2151
2319
|
async getOpenOrders(user?: string): Promise<OpenOrder[]> {
|
|
2152
|
-
|
|
2320
|
+
const target = user ?? this.address;
|
|
2321
|
+
const live = this.realtime?.connected ? this.realtime.getOpenOrders(target) : null;
|
|
2322
|
+
if (live) return live;
|
|
2323
|
+
this.log('Fetching openOrders for:', target);
|
|
2153
2324
|
await this.getMetaAndAssetCtxs(); // Ensure HIP-3 dex list is loaded
|
|
2154
2325
|
|
|
2155
2326
|
// Fetch main dex orders
|
package/scripts/core/ws.ts
CHANGED
|
@@ -12,8 +12,9 @@ import type {
|
|
|
12
12
|
AllDexsAssetCtxsWsEvent,
|
|
13
13
|
AllDexsClearinghouseStateWsEvent,
|
|
14
14
|
SpotStateWsEvent,
|
|
15
|
+
OpenOrdersWsEvent,
|
|
15
16
|
} from '@nktkas/hyperliquid';
|
|
16
|
-
import type { ClearinghouseState } from './types.js';
|
|
17
|
+
import type { ClearinghouseState, OpenOrder } from './types.js';
|
|
17
18
|
import { isMainnet } from './config.js';
|
|
18
19
|
|
|
19
20
|
// ── Event types ────────────────────────────────────────────────────
|
|
@@ -71,6 +72,8 @@ export interface WsEventMap {
|
|
|
71
72
|
spotState: {
|
|
72
73
|
balances: Array<{ coin: string; token: number; total: string; hold: string; entryNtl?: string }>;
|
|
73
74
|
};
|
|
75
|
+
/** Complete open-order snapshot for one user + dex (empty dex = native/main). */
|
|
76
|
+
openOrders: { user: string; dex: string; orders: OpenOrder[] };
|
|
74
77
|
/** Order status changed (filled, canceled, rejected, etc.) */
|
|
75
78
|
orderUpdate: {
|
|
76
79
|
order: {
|
|
@@ -136,6 +139,7 @@ export class WebSocketManager {
|
|
|
136
139
|
private subscriptions: ISubscription[] = [];
|
|
137
140
|
private handlers = new Map<WsEventType, Set<Function>>();
|
|
138
141
|
private _connected = false;
|
|
142
|
+
private closing = false;
|
|
139
143
|
private verbose: boolean;
|
|
140
144
|
|
|
141
145
|
constructor(verbose = false) {
|
|
@@ -158,17 +162,38 @@ export class WebSocketManager {
|
|
|
158
162
|
this.transport = new WebSocketTransport({
|
|
159
163
|
isTestnet: !isMainnet(),
|
|
160
164
|
resubscribe: true, // auto-resubscribe on reconnect
|
|
165
|
+
reconnect: { maxRetries: Infinity },
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const socket = this.transport.socket as unknown as WebSocket;
|
|
169
|
+
socket.addEventListener('open', () => {
|
|
170
|
+
if (this._connected) return;
|
|
171
|
+
this._connected = true;
|
|
172
|
+
this.emit('connected', undefined);
|
|
173
|
+
this.log('Connected to', isMainnet() ? 'mainnet' : 'testnet');
|
|
174
|
+
});
|
|
175
|
+
socket.addEventListener('close', () => {
|
|
176
|
+
if (!this._connected) return;
|
|
177
|
+
this._connected = false;
|
|
178
|
+
this.emit('disconnected', { reason: this.closing ? 'manual close' : 'socket closed' });
|
|
179
|
+
this.log(this.closing ? 'Closed' : 'Disconnected; transport will reconnect');
|
|
180
|
+
});
|
|
181
|
+
socket.addEventListener('error', () => {
|
|
182
|
+
this.emit('error', { error: new Error('Hyperliquid WebSocket transport error') });
|
|
161
183
|
});
|
|
162
184
|
|
|
163
185
|
this.client = new SubscriptionClient({ transport: this.transport });
|
|
164
186
|
|
|
165
187
|
await this.transport.ready();
|
|
166
|
-
this._connected
|
|
167
|
-
|
|
168
|
-
|
|
188
|
+
if (!this._connected) {
|
|
189
|
+
this._connected = true;
|
|
190
|
+
this.emit('connected', undefined);
|
|
191
|
+
this.log('Connected to', isMainnet() ? 'mainnet' : 'testnet');
|
|
192
|
+
}
|
|
169
193
|
}
|
|
170
194
|
|
|
171
195
|
async close(): Promise<void> {
|
|
196
|
+
this.closing = true;
|
|
172
197
|
for (const sub of this.subscriptions) {
|
|
173
198
|
try { await sub.unsubscribe(); } catch { /* ignore */ }
|
|
174
199
|
}
|
|
@@ -180,9 +205,12 @@ export class WebSocketManager {
|
|
|
180
205
|
this.client = null;
|
|
181
206
|
}
|
|
182
207
|
|
|
183
|
-
this._connected
|
|
184
|
-
|
|
185
|
-
|
|
208
|
+
if (this._connected) {
|
|
209
|
+
this._connected = false;
|
|
210
|
+
this.emit('disconnected', { reason: 'manual close' });
|
|
211
|
+
this.log('Closed');
|
|
212
|
+
}
|
|
213
|
+
this.closing = false;
|
|
186
214
|
}
|
|
187
215
|
|
|
188
216
|
// ── Event system ───────────────────────────────────────────────
|
|
@@ -304,6 +332,19 @@ export class WebSocketManager {
|
|
|
304
332
|
return this.trackSub(sub);
|
|
305
333
|
}
|
|
306
334
|
|
|
335
|
+
/** Subscribe to complete open-order snapshots for one dex. */
|
|
336
|
+
async subscribeOpenOrders(user: `0x${string}`, dex = ''): Promise<ISubscription> {
|
|
337
|
+
const client = this.ensureClient();
|
|
338
|
+
const sub = await client.openOrders({ user, dex }, (data: OpenOrdersWsEvent) => {
|
|
339
|
+
this.emit('openOrders', {
|
|
340
|
+
user: data.user,
|
|
341
|
+
dex: data.dex,
|
|
342
|
+
orders: data.orders as unknown as OpenOrder[],
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
return this.trackSub(sub);
|
|
346
|
+
}
|
|
347
|
+
|
|
307
348
|
/**
|
|
308
349
|
* Subscribe to order lifecycle events (fill, cancel, reject, etc.).
|
|
309
350
|
* This is the most important subscription for trading automations.
|
|
@@ -377,16 +418,24 @@ export class WebSocketManager {
|
|
|
377
418
|
/**
|
|
378
419
|
* Start all subscriptions needed for the automation runtime:
|
|
379
420
|
* - allMids (price feed)
|
|
421
|
+
* - allDexsAssetCtxs (funding / mark / oracle contexts)
|
|
422
|
+
* - allDexsClearinghouseState + spotState (positions, margin, balances)
|
|
423
|
+
* - openOrders for main + requested HIP-3 dexes
|
|
380
424
|
* - orderUpdates (order lifecycle)
|
|
381
425
|
* - userFills (trade fills)
|
|
382
426
|
* - userEvents (liquidations, funding payments, system cancels)
|
|
383
427
|
*/
|
|
384
|
-
async subscribeAll(user: `0x${string}
|
|
428
|
+
async subscribeAll(user: `0x${string}`, dexNames: string[] = []): Promise<void> {
|
|
385
429
|
await this.connect();
|
|
386
430
|
this.log('Subscribing to all feeds for', user);
|
|
387
431
|
|
|
388
432
|
await Promise.all([
|
|
389
433
|
this.subscribeAllMids(),
|
|
434
|
+
this.subscribeAllDexsAssetCtxs(),
|
|
435
|
+
this.subscribeAllDexsClearinghouseState(user),
|
|
436
|
+
this.subscribeSpotState(user),
|
|
437
|
+
this.subscribeOpenOrders(user),
|
|
438
|
+
...dexNames.map((dex) => this.subscribeOpenOrders(user, dex)),
|
|
390
439
|
this.subscribeOrderUpdates(user),
|
|
391
440
|
this.subscribeUserFills(user),
|
|
392
441
|
this.subscribeUserEvents(user),
|
package/scripts/setup/install.ts
CHANGED
|
@@ -6,21 +6,59 @@ import * as path from 'path';
|
|
|
6
6
|
import { homedir } from 'os';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import { spawnSync } from 'child_process';
|
|
9
|
+
import {
|
|
10
|
+
INSTALLABLE_PACKAGES,
|
|
11
|
+
packageSpec,
|
|
12
|
+
resolveInstallablePackage,
|
|
13
|
+
} from './package-catalog.js';
|
|
9
14
|
|
|
10
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
16
|
const __dirname = path.dirname(__filename);
|
|
12
17
|
const packageRoot = path.resolve(__dirname, '../..');
|
|
13
|
-
const
|
|
18
|
+
const rawArgs = process.argv.slice(2);
|
|
19
|
+
const args = new Set(rawArgs);
|
|
20
|
+
|
|
21
|
+
function positionalArgs(): string[] {
|
|
22
|
+
const positionals: string[] = [];
|
|
23
|
+
for (let index = 0; index < rawArgs.length; index++) {
|
|
24
|
+
const arg = rawArgs[index];
|
|
25
|
+
if (arg === '--tag') {
|
|
26
|
+
index++;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (!arg.startsWith('-')) positionals.push(arg);
|
|
30
|
+
}
|
|
31
|
+
return positionals;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function optionValue(flag: string): string | null {
|
|
35
|
+
const index = rawArgs.indexOf(flag);
|
|
36
|
+
if (index < 0) return null;
|
|
37
|
+
const value = rawArgs[index + 1];
|
|
38
|
+
if (!value || value.startsWith('-')) fail(`${flag} requires a value`);
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
14
41
|
|
|
15
42
|
function printUsage(): void {
|
|
16
43
|
console.log(`
|
|
17
|
-
OpenBroker
|
|
18
|
-
|
|
44
|
+
OpenBroker Installer
|
|
45
|
+
====================
|
|
19
46
|
|
|
20
47
|
Usage:
|
|
48
|
+
openbroker install <package> [--tag <version>] [--dry]
|
|
49
|
+
openbroker install --list
|
|
21
50
|
openbroker install --codex [options]
|
|
22
51
|
npx openbroker@latest install --codex [options]
|
|
23
52
|
|
|
53
|
+
Companion packages:
|
|
54
|
+
monitoring Install the local automation dashboard
|
|
55
|
+
extended Install the Extended Exchange CLI
|
|
56
|
+
|
|
57
|
+
Package options:
|
|
58
|
+
--tag <tag> Install a release tag or exact version (default: latest)
|
|
59
|
+
--dry Print the npm command without installing
|
|
60
|
+
--list List supported companion packages
|
|
61
|
+
|
|
24
62
|
Harnesses:
|
|
25
63
|
--codex Install the OpenBroker skill for Codex
|
|
26
64
|
|
|
@@ -93,6 +131,62 @@ function installGlobalCli(): void {
|
|
|
93
131
|
}
|
|
94
132
|
}
|
|
95
133
|
|
|
134
|
+
function printInstallablePackages(): void {
|
|
135
|
+
console.log('Installable OpenBroker packages:\n');
|
|
136
|
+
for (const entry of INSTALLABLE_PACKAGES) {
|
|
137
|
+
console.log(` ${entry.key.padEnd(12)} ${entry.packageName.padEnd(26)} ${entry.description}`);
|
|
138
|
+
}
|
|
139
|
+
console.log('\nInstall or upgrade with: openbroker install <package>');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function installCompanionPackage(target: string): void {
|
|
143
|
+
const entry = resolveInstallablePackage(target);
|
|
144
|
+
if (!entry) {
|
|
145
|
+
printInstallablePackages();
|
|
146
|
+
fail(`unknown installable package: ${target}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const allowedFlags = new Set(['--tag', '--dry']);
|
|
150
|
+
const unsupported = rawArgs.filter((arg, index) => (
|
|
151
|
+
arg.startsWith('-')
|
|
152
|
+
&& !allowedFlags.has(arg)
|
|
153
|
+
&& rawArgs[index - 1] !== '--tag'
|
|
154
|
+
));
|
|
155
|
+
if (unsupported.length > 0) fail(`unsupported package option: ${unsupported[0]}`);
|
|
156
|
+
|
|
157
|
+
const tag = optionValue('--tag') ?? 'latest';
|
|
158
|
+
let spec: string;
|
|
159
|
+
try {
|
|
160
|
+
spec = packageSpec(entry, tag);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
166
|
+
const npmArgs = ['install', '--global', spec];
|
|
167
|
+
|
|
168
|
+
console.log(`OpenBroker — Install ${entry.key}`);
|
|
169
|
+
console.log('================================\n');
|
|
170
|
+
console.log(`Package: ${spec}`);
|
|
171
|
+
console.log(`Command: ${npmCommand} ${npmArgs.join(' ')}`);
|
|
172
|
+
|
|
173
|
+
if (args.has('--dry')) {
|
|
174
|
+
console.log('\nDry run only; nothing was installed.');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const result = spawnSync(npmCommand, npmArgs, { stdio: 'inherit' });
|
|
179
|
+
if (result.error) fail(`could not start npm: ${result.error.message}`);
|
|
180
|
+
if (result.status !== 0) {
|
|
181
|
+
fail(`installation failed for ${entry.packageName}; resolve the npm error and retry`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(`\n✅ ${entry.packageName} installed successfully.`);
|
|
185
|
+
console.log(`Available command: ${entry.command}`);
|
|
186
|
+
console.log('\nNext steps:');
|
|
187
|
+
for (const step of entry.nextSteps) console.log(` ${step}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
96
190
|
function runApiWalletSetup(): void {
|
|
97
191
|
const onboardPath = path.join(packageRoot, 'scripts', 'setup', 'onboard.ts');
|
|
98
192
|
|
|
@@ -121,6 +215,19 @@ function main(): void {
|
|
|
121
215
|
return;
|
|
122
216
|
}
|
|
123
217
|
|
|
218
|
+
if (args.has('--list')) {
|
|
219
|
+
printInstallablePackages();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const positionals = positionalArgs();
|
|
224
|
+
if (positionals.length > 0) {
|
|
225
|
+
if (positionals.length > 1) fail(`expected one package name, received: ${positionals.join(' ')}`);
|
|
226
|
+
if (args.has('--codex')) fail('choose either a companion package or the --codex harness installer');
|
|
227
|
+
installCompanionPackage(positionals[0]);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
124
231
|
if (!args.has('--codex')) {
|
|
125
232
|
printUsage();
|
|
126
233
|
fail('choose a supported harness flag such as --codex');
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import {
|
|
6
|
+
INSTALLABLE_PACKAGES,
|
|
7
|
+
packageSpec,
|
|
8
|
+
resolveInstallablePackage,
|
|
9
|
+
} from './package-catalog.js';
|
|
10
|
+
|
|
11
|
+
const installScript = fileURLToPath(new URL('./install.ts', import.meta.url));
|
|
12
|
+
|
|
13
|
+
test('resolves companion packages by short name and npm package name', () => {
|
|
14
|
+
assert.equal(resolveInstallablePackage('monitoring')?.packageName, 'openbroker-monitoring');
|
|
15
|
+
assert.equal(resolveInstallablePackage('openbroker-monitoring')?.key, 'monitoring');
|
|
16
|
+
assert.equal(resolveInstallablePackage('EXTENDED')?.command, 'openbroker-ex');
|
|
17
|
+
assert.equal(resolveInstallablePackage('unknown'), null);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('builds pinned or latest npm package specs without accepting arbitrary specs', () => {
|
|
21
|
+
const monitoring = INSTALLABLE_PACKAGES.find((entry) => entry.key === 'monitoring');
|
|
22
|
+
assert.ok(monitoring);
|
|
23
|
+
assert.equal(packageSpec(monitoring), 'openbroker-monitoring@latest');
|
|
24
|
+
assert.equal(packageSpec(monitoring, '1.4.2'), 'openbroker-monitoring@1.4.2');
|
|
25
|
+
assert.throws(() => packageSpec(monitoring, 'npm:unrelated-package'));
|
|
26
|
+
assert.throws(() => packageSpec(monitoring, '../local-package'));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('installer dry run prints the global npm operation without writing', () => {
|
|
30
|
+
const result = spawnSync(
|
|
31
|
+
process.execPath,
|
|
32
|
+
['--import', 'tsx', installScript, 'monitoring', '--tag', '1.4.2', '--dry'],
|
|
33
|
+
{ encoding: 'utf8' },
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
assert.equal(result.status, 0, result.stderr);
|
|
37
|
+
assert.match(result.stdout, /npm install --global openbroker-monitoring@1\.4\.2/);
|
|
38
|
+
assert.match(result.stdout, /nothing was installed/i);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('installer rejects packages outside the catalog', () => {
|
|
42
|
+
const result = spawnSync(
|
|
43
|
+
process.execPath,
|
|
44
|
+
['--import', 'tsx', installScript, 'unrelated-package', '--dry'],
|
|
45
|
+
{ encoding: 'utf8' },
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
assert.equal(result.status, 1);
|
|
49
|
+
assert.match(result.stderr, /unknown installable package/i);
|
|
50
|
+
});
|