openbroker 1.0.80 → 1.0.82
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/SKILL.md +1 -1
- package/bin/cli.ts +22 -1
- package/bin/openbroker.js +4 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/auto/cli.ts +9 -1
- package/scripts/auto/loader.ts +37 -3
- package/scripts/auto/runtime.ts +5 -4
- package/scripts/core/client.ts +603 -214
- package/scripts/core/config.ts +19 -0
- package/scripts/core/types.ts +1 -0
- package/scripts/core/utils.ts +4 -1
- package/scripts/info/account.ts +12 -8
- package/scripts/info/all-markets.ts +3 -0
- package/scripts/info/candles.ts +3 -0
- package/scripts/info/funding-history.ts +3 -0
- package/scripts/info/funding-scan.ts +4 -0
- package/scripts/info/funding.ts +7 -0
- package/scripts/info/markets.ts +7 -0
- package/scripts/info/orders.ts +5 -0
- package/scripts/info/search-markets.ts +8 -0
- package/scripts/info/trades.ts +3 -0
- package/scripts/setup/onboard.ts +95 -44
package/scripts/core/client.ts
CHANGED
|
@@ -31,6 +31,10 @@ export class HyperliquidClient {
|
|
|
31
31
|
private perpDexsCache: Array<{ name: string; fullName: string; deployer: string } | null> | null = null;
|
|
32
32
|
/** Whether HIP-3 assets have been loaded into maps */
|
|
33
33
|
private hip3Loaded: boolean = false;
|
|
34
|
+
/** Whether API wallet setup has been validated */
|
|
35
|
+
private apiWalletValidated: boolean = false;
|
|
36
|
+
/** Set of HIP-3 dex names that have been loaded (for testnet on-demand loading) */
|
|
37
|
+
private loadedHip3Dexes: Set<string> = new Set();
|
|
34
38
|
/** HIP-3 assets that have had isolated margin set this session */
|
|
35
39
|
private hip3IsolatedSet: Set<string> = new Set();
|
|
36
40
|
/** Cached maxLeverage for HIP-3 assets */
|
|
@@ -39,6 +43,8 @@ export class HyperliquidClient {
|
|
|
39
43
|
private accountMode: string | null = null;
|
|
40
44
|
/** Spot asset index map: coin name → 10000 + spotMeta.universe[i].index */
|
|
41
45
|
private spotAssetMap: Map<string, number> = new Map();
|
|
46
|
+
/** Spot market key map: coin name → pair.name (e.g. "@230", "PURR/USDC") */
|
|
47
|
+
private spotPairNameMap: Map<string, string> = new Map();
|
|
42
48
|
/** Spot szDecimals map: coin name → base token szDecimals */
|
|
43
49
|
private spotSzDecimalsMap: Map<string, number> = new Map();
|
|
44
50
|
/** Whether spot metadata has been loaded */
|
|
@@ -51,13 +57,24 @@ export class HyperliquidClient {
|
|
|
51
57
|
this.verbose = process.env.VERBOSE === '1' || process.env.VERBOSE === 'true';
|
|
52
58
|
|
|
53
59
|
// Initialize SDK clients
|
|
54
|
-
this.transport = new HttpTransport({
|
|
60
|
+
this.transport = new HttpTransport({ isTestnet: !isMainnet() });
|
|
55
61
|
this.info = new InfoClient({ transport: this.transport });
|
|
56
62
|
this.exchange = new ExchangeClient({
|
|
57
63
|
transport: this.transport,
|
|
58
64
|
wallet: this.account,
|
|
59
|
-
isMainnet: isMainnet(),
|
|
60
65
|
});
|
|
66
|
+
|
|
67
|
+
this.log(
|
|
68
|
+
'Client init:',
|
|
69
|
+
JSON.stringify({
|
|
70
|
+
network: isMainnet() ? 'mainnet' : 'testnet',
|
|
71
|
+
apiUrl: this.config.baseUrl,
|
|
72
|
+
accountAddress: this.config.accountAddress,
|
|
73
|
+
walletAddress: this.config.walletAddress,
|
|
74
|
+
isApiWallet: this.config.isApiWallet,
|
|
75
|
+
isReadOnly: this.config.isReadOnly,
|
|
76
|
+
})
|
|
77
|
+
);
|
|
61
78
|
}
|
|
62
79
|
|
|
63
80
|
private log(...args: unknown[]) {
|
|
@@ -66,6 +83,95 @@ export class HyperliquidClient {
|
|
|
66
83
|
}
|
|
67
84
|
}
|
|
68
85
|
|
|
86
|
+
private describeError(error: unknown): string {
|
|
87
|
+
if (!(error instanceof Error)) return String(error);
|
|
88
|
+
|
|
89
|
+
const response = (error as Error & { response?: Response }).response;
|
|
90
|
+
const body = (error as Error & { body?: string }).body;
|
|
91
|
+
const cause = (error as Error & { cause?: unknown }).cause;
|
|
92
|
+
const parts = [error.message];
|
|
93
|
+
|
|
94
|
+
if (response) {
|
|
95
|
+
parts.push(`status=${response.status} ${response.statusText}`.trim());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (body) {
|
|
99
|
+
parts.push(`body=${body.length > 300 ? `${body.slice(0, 300)}...` : body}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (cause instanceof Error && cause.message && cause.message !== error.message) {
|
|
103
|
+
parts.push(`cause=${cause.message}`);
|
|
104
|
+
} else if (cause && !(cause instanceof Error)) {
|
|
105
|
+
parts.push(`cause=${String(cause)}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return parts.join(' | ');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Retry an async operation on transient failures (fetch failed, ECONNRESET, etc.) */
|
|
112
|
+
private async withRetry<T>(fn: () => Promise<T>, label: string, maxRetries = 3): Promise<T> {
|
|
113
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
114
|
+
try {
|
|
115
|
+
return await fn();
|
|
116
|
+
} catch (error) {
|
|
117
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
118
|
+
const isTransient = /fetch failed|ECONNRESET|ETIMEDOUT|ENOTFOUND|socket hang up/i.test(message);
|
|
119
|
+
if (!isTransient || attempt === maxRetries) throw error;
|
|
120
|
+
const delay = attempt * 1000; // 1s, 2s, 3s
|
|
121
|
+
this.log(`${label} attempt ${attempt}/${maxRetries} failed (${message}), retrying in ${delay}ms...`);
|
|
122
|
+
await new Promise(r => setTimeout(r, delay));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
throw new Error('unreachable');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private getTransportContext(label: string): string {
|
|
129
|
+
return JSON.stringify({
|
|
130
|
+
label,
|
|
131
|
+
network: isMainnet() ? 'mainnet' : 'testnet',
|
|
132
|
+
apiUrl: this.config.baseUrl,
|
|
133
|
+
accountAddress: this.config.accountAddress,
|
|
134
|
+
walletAddress: this.config.walletAddress,
|
|
135
|
+
isApiWallet: this.config.isApiWallet,
|
|
136
|
+
isReadOnly: this.config.isReadOnly,
|
|
137
|
+
verbose: this.verbose,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private async postInfo<T>(payload: Record<string, unknown>, label: string): Promise<T> {
|
|
142
|
+
const baseUrl = isMainnet()
|
|
143
|
+
? 'https://api.hyperliquid.xyz'
|
|
144
|
+
: 'https://api.hyperliquid-testnet.xyz';
|
|
145
|
+
|
|
146
|
+
const response = await this.withRetry(async () => {
|
|
147
|
+
try {
|
|
148
|
+
return await fetch(baseUrl + '/info', {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: { 'Content-Type': 'application/json' },
|
|
151
|
+
body: JSON.stringify(payload),
|
|
152
|
+
});
|
|
153
|
+
} catch (error) {
|
|
154
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
155
|
+
throw new Error(`${label} request failed before response: ${message}`);
|
|
156
|
+
}
|
|
157
|
+
}, label);
|
|
158
|
+
|
|
159
|
+
const text = await response.text();
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
const snippet = text.length > 300 ? `${text.slice(0, 300)}...` : text;
|
|
162
|
+
throw new Error(
|
|
163
|
+
`${label} failed: HTTP ${response.status} ${response.statusText}${snippet ? ` | body=${snippet}` : ''}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
return JSON.parse(text) as T;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
171
|
+
throw new Error(`${label} returned invalid JSON: ${message}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
69
175
|
/** The address we're trading on behalf of (may be different from wallet if using API wallet) */
|
|
70
176
|
get address(): string {
|
|
71
177
|
return this.config.accountAddress;
|
|
@@ -101,13 +207,35 @@ export class HyperliquidClient {
|
|
|
101
207
|
return this.config.isReadOnly;
|
|
102
208
|
}
|
|
103
209
|
|
|
104
|
-
/**
|
|
105
|
-
|
|
210
|
+
/** Whether connected to testnet (HIP-3 dexes not auto-loaded) */
|
|
211
|
+
get isTestnet(): boolean {
|
|
212
|
+
return !isMainnet();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Returns vaultAddress param for SDK exchange calls.
|
|
217
|
+
* Only used for vault trading (HYPERLIQUID_VAULT_ADDRESS set explicitly).
|
|
218
|
+
* Standard API wallets (agents) do NOT need this — the API maps agent → master automatically.
|
|
219
|
+
*/
|
|
220
|
+
private get vaultParam(): { vaultAddress: `0x${string}` } | Record<string, never> {
|
|
221
|
+
if (this.config.vaultAddress) {
|
|
222
|
+
return { vaultAddress: this.config.vaultAddress as `0x${string}` };
|
|
223
|
+
}
|
|
224
|
+
return {};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Throw error if trying to trade in read-only mode. Validates API wallet on first call. */
|
|
228
|
+
private async requireTrading(): Promise<void> {
|
|
106
229
|
if (this.config.isReadOnly) {
|
|
107
230
|
throw new Error(
|
|
108
231
|
'Trading not available. Run "openbroker setup" to configure your wallet.'
|
|
109
232
|
);
|
|
110
233
|
}
|
|
234
|
+
// One-time API wallet validation on first trade attempt
|
|
235
|
+
if (this.config.isApiWallet && !this.apiWalletValidated) {
|
|
236
|
+
this.apiWalletValidated = true;
|
|
237
|
+
await this.validateApiWalletSetup();
|
|
238
|
+
}
|
|
111
239
|
}
|
|
112
240
|
|
|
113
241
|
// ============ Market Data ============
|
|
@@ -116,7 +244,16 @@ export class HyperliquidClient {
|
|
|
116
244
|
if (this.meta) return this.meta;
|
|
117
245
|
|
|
118
246
|
this.log('Fetching metaAndAssetCtxs...');
|
|
119
|
-
|
|
247
|
+
let response;
|
|
248
|
+
try {
|
|
249
|
+
response = await this.withRetry(() => this.info.metaAndAssetCtxs(), 'metaAndAssetCtxs');
|
|
250
|
+
} catch (error) {
|
|
251
|
+
this.log('metaAndAssetCtxs failure context:', this.getTransportContext('metaAndAssetCtxs'));
|
|
252
|
+
if (error instanceof Error && error.stack) {
|
|
253
|
+
this.log('metaAndAssetCtxs stack:', error.stack);
|
|
254
|
+
}
|
|
255
|
+
throw new Error(`metaAndAssetCtxs failed: ${this.describeError(error)}`);
|
|
256
|
+
}
|
|
120
257
|
this.log('metaAndAssetCtxs response:', JSON.stringify(response, null, 2).slice(0, 500) + '...');
|
|
121
258
|
|
|
122
259
|
this.meta = {
|
|
@@ -145,80 +282,166 @@ export class HyperliquidClient {
|
|
|
145
282
|
* Asset index formula: 100000 + dexIdx * 10000 + assetIdx
|
|
146
283
|
* Coins are keyed as "dexName:COIN" (e.g., "xyz:CL")
|
|
147
284
|
*/
|
|
285
|
+
/** Max concurrent HIP-3 API requests to avoid rate limiting */
|
|
286
|
+
private static readonly HIP3_CONCURRENCY = 5;
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Like Promise.allSettled but with a concurrency limit.
|
|
290
|
+
* Processes tasks in batches to avoid hitting API rate limits.
|
|
291
|
+
*/
|
|
292
|
+
private async batchSettled<T>(tasks: Array<() => Promise<T>>): Promise<PromiseSettledResult<T>[]> {
|
|
293
|
+
const results: PromiseSettledResult<T>[] = [];
|
|
294
|
+
const concurrency = HyperliquidClient.HIP3_CONCURRENCY;
|
|
295
|
+
for (let i = 0; i < tasks.length; i += concurrency) {
|
|
296
|
+
const batch = tasks.slice(i, i + concurrency);
|
|
297
|
+
const batchResults = await Promise.allSettled(batch.map(fn => fn()));
|
|
298
|
+
results.push(...batchResults);
|
|
299
|
+
}
|
|
300
|
+
return results;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Load HIP-3 perp dex assets into the asset/szDecimals maps.
|
|
305
|
+
* On testnet: skips auto-loading (too many junk dexes). Use loadSingleHip3Dex() on demand.
|
|
306
|
+
* On mainnet: loads all dexes with concurrency limit.
|
|
307
|
+
*/
|
|
148
308
|
private async loadHip3Assets(): Promise<void> {
|
|
149
309
|
try {
|
|
150
310
|
const dexs = await this.getPerpDexs();
|
|
151
|
-
const baseUrl = isMainnet()
|
|
152
|
-
? 'https://api.hyperliquid.xyz'
|
|
153
|
-
: 'https://api.hyperliquid-testnet.xyz';
|
|
154
311
|
|
|
312
|
+
// On testnet, skip auto-loading — too many junk dexes cause rate limiting.
|
|
313
|
+
// Users can reference specific dexes (e.g., "felix:BTC") which triggers on-demand loading.
|
|
314
|
+
if (!isMainnet()) {
|
|
315
|
+
const dexCount = dexs.filter(d => d != null).length - 1; // exclude null at index 0
|
|
316
|
+
if (dexCount > 0) {
|
|
317
|
+
this.log(`Testnet: skipping auto-load of ${dexCount} HIP-3 dexes. Use "dexName:COIN" to load a specific dex on demand.`);
|
|
318
|
+
}
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Mainnet: load all dexes
|
|
323
|
+
const dexEntries: Array<{ dex: { name: string }; dexIdx: number }> = [];
|
|
155
324
|
for (let dexIdx = 1; dexIdx < dexs.length; dexIdx++) {
|
|
156
325
|
const dex = dexs[dexIdx];
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
// Extract local name by stripping dex prefix if present
|
|
175
|
-
const localName = coinName.startsWith(dex.name + ':') ? coinName.slice(dex.name.length + 1) : coinName;
|
|
176
|
-
const globalIndex = 100000 + dexIdx * 10000 + assetIdx;
|
|
177
|
-
|
|
178
|
-
this.assetMap.set(coinName, globalIndex);
|
|
179
|
-
this.szDecimalsMap.set(coinName, asset.szDecimals);
|
|
180
|
-
this.coinDexMap.set(coinName, { dexName: dex.name, dexIdx, localName });
|
|
181
|
-
if (asset.maxLeverage) this.hip3MaxLeverageMap.set(coinName, asset.maxLeverage);
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
} catch (e) {
|
|
185
|
-
this.log(`Failed to load HIP-3 dex ${dex.name}:`, e);
|
|
326
|
+
if (dex) dexEntries.push({ dex, dexIdx });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const results = await this.batchSettled(
|
|
330
|
+
dexEntries.map(({ dex, dexIdx }) => async () => {
|
|
331
|
+
const data = await this.postInfo<unknown>(
|
|
332
|
+
{ type: 'metaAndAssetCtxs', dex: dex.name },
|
|
333
|
+
`metaAndAssetCtxs(${dex.name})`
|
|
334
|
+
);
|
|
335
|
+
return { dex, dexIdx, data };
|
|
336
|
+
})
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
for (const result of results) {
|
|
340
|
+
if (result.status === 'rejected') {
|
|
341
|
+
this.log(`Failed to load HIP-3 dex:`, result.reason);
|
|
342
|
+
continue;
|
|
186
343
|
}
|
|
344
|
+
const { dex, dexIdx, data: dexData } = result.value;
|
|
345
|
+
this.registerHip3Dex(dex.name, dexIdx, dexData);
|
|
187
346
|
}
|
|
188
347
|
} catch (e) {
|
|
189
348
|
this.log('Failed to load HIP-3 assets:', e);
|
|
190
349
|
}
|
|
191
350
|
}
|
|
192
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Load a single HIP-3 dex by name (on-demand, e.g. when user references "felix:BTC").
|
|
354
|
+
* No-op if already loaded.
|
|
355
|
+
*/
|
|
356
|
+
async loadSingleHip3Dex(dexName: string): Promise<boolean> {
|
|
357
|
+
if (this.loadedHip3Dexes.has(dexName)) return true;
|
|
358
|
+
|
|
359
|
+
const dexs = await this.getPerpDexs();
|
|
360
|
+
const dexIdx = dexs.findIndex(d => d?.name === dexName);
|
|
361
|
+
if (dexIdx < 1) {
|
|
362
|
+
this.log(`HIP-3 dex "${dexName}" not found in perpDexs list`);
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
this.log(`On-demand loading HIP-3 dex: ${dexName}`);
|
|
368
|
+
const data = await this.postInfo<unknown>(
|
|
369
|
+
{ type: 'metaAndAssetCtxs', dex: dexName },
|
|
370
|
+
`metaAndAssetCtxs(${dexName})`
|
|
371
|
+
);
|
|
372
|
+
this.registerHip3Dex(dexName, dexIdx, data);
|
|
373
|
+
return true;
|
|
374
|
+
} catch (e) {
|
|
375
|
+
this.log(`Failed to load HIP-3 dex ${dexName}:`, e);
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Get HIP-3 dexes to iterate over for bulk queries.
|
|
382
|
+
* Mainnet: all dexes. Testnet: only explicitly loaded dexes.
|
|
383
|
+
*/
|
|
384
|
+
private async getIterableHip3Dexs(): Promise<Array<{ name: string; fullName: string; deployer: string }>> {
|
|
385
|
+
const dexs = await this.getPerpDexs();
|
|
386
|
+
const all = dexs.slice(1).filter((d): d is NonNullable<typeof d> => d != null);
|
|
387
|
+
if (isMainnet()) return all;
|
|
388
|
+
// Testnet: only return dexes that have been explicitly loaded
|
|
389
|
+
return all.filter(d => this.loadedHip3Dexes.has(d.name));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Register a fetched HIP-3 dex's assets into lookup maps */
|
|
393
|
+
private registerHip3Dex(dexName: string, dexIdx: number, dexData: any): void {
|
|
394
|
+
if (dexData && dexData[0]?.universe) {
|
|
395
|
+
const universe = dexData[0].universe as Array<{ name: string; szDecimals: number; maxLeverage: number; onlyIsolated?: boolean }>;
|
|
396
|
+
this.log(`Loading HIP-3 dex: ${dexName} with ${universe.length} markets`);
|
|
397
|
+
|
|
398
|
+
universe.forEach((asset, assetIdx) => {
|
|
399
|
+
const coinName = asset.name;
|
|
400
|
+
const localName = coinName.startsWith(dexName + ':') ? coinName.slice(dexName.length + 1) : coinName;
|
|
401
|
+
const globalIndex = 100000 + dexIdx * 10000 + assetIdx;
|
|
402
|
+
|
|
403
|
+
this.assetMap.set(coinName, globalIndex);
|
|
404
|
+
this.szDecimalsMap.set(coinName, asset.szDecimals);
|
|
405
|
+
this.coinDexMap.set(coinName, { dexName, dexIdx, localName });
|
|
406
|
+
if (asset.maxLeverage) this.hip3MaxLeverageMap.set(coinName, asset.maxLeverage);
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
this.loadedHip3Dexes.add(dexName);
|
|
410
|
+
}
|
|
411
|
+
|
|
193
412
|
async getAllMids(): Promise<Record<string, string>> {
|
|
194
413
|
this.log('Fetching allMids...');
|
|
195
|
-
|
|
414
|
+
let response: Record<string, string>;
|
|
415
|
+
try {
|
|
416
|
+
response = await this.withRetry(() => this.info.allMids(), 'allMids') as Record<string, string>;
|
|
417
|
+
} catch (error) {
|
|
418
|
+
this.log('allMids failure context:', this.getTransportContext('allMids'));
|
|
419
|
+
if (error instanceof Error && error.stack) {
|
|
420
|
+
this.log('allMids stack:', error.stack);
|
|
421
|
+
}
|
|
422
|
+
throw new Error(`allMids failed: ${this.describeError(error)}`);
|
|
423
|
+
}
|
|
196
424
|
|
|
197
|
-
// Also fetch HIP-3 dex mids
|
|
425
|
+
// Also fetch HIP-3 dex mids (in parallel; testnet: only loaded dexes)
|
|
198
426
|
try {
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
427
|
+
const validDexs = await this.getIterableHip3Dexs();
|
|
428
|
+
const results = await this.batchSettled(
|
|
429
|
+
validDexs.map((dex) => async () => {
|
|
430
|
+
const mids = await this.postInfo<Record<string, string>>(
|
|
431
|
+
{ type: 'allMids', dex: dex.name },
|
|
432
|
+
`allMids(${dex.name})`
|
|
433
|
+
);
|
|
434
|
+
return { dex, mids };
|
|
435
|
+
})
|
|
436
|
+
);
|
|
203
437
|
|
|
204
|
-
for (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
headers: { 'Content-Type': 'application/json' },
|
|
212
|
-
body: JSON.stringify({ type: 'allMids', dex: dex.name }),
|
|
213
|
-
});
|
|
214
|
-
const dexMids = await dexResponse.json() as Record<string, string>;
|
|
215
|
-
|
|
216
|
-
// Merge directly — API already returns prefixed keys (e.g., "xyz:CL")
|
|
217
|
-
for (const [coin, mid] of Object.entries(dexMids)) {
|
|
218
|
-
response[coin] = mid;
|
|
219
|
-
}
|
|
220
|
-
} catch (e) {
|
|
221
|
-
this.log(`Failed to fetch mids for HIP-3 dex ${dex.name}:`, e);
|
|
438
|
+
for (const result of results) {
|
|
439
|
+
if (result.status === 'rejected') {
|
|
440
|
+
this.log('Failed to fetch HIP-3 dex mids:', result.reason);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
for (const [coin, mid] of Object.entries(result.value.mids)) {
|
|
444
|
+
response[coin] = mid;
|
|
222
445
|
}
|
|
223
446
|
}
|
|
224
447
|
} catch (e) {
|
|
@@ -240,16 +463,11 @@ export class HyperliquidClient {
|
|
|
240
463
|
if (this.perpDexsCache) return this.perpDexsCache;
|
|
241
464
|
|
|
242
465
|
this.log('Fetching perpDexs...');
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
method: 'POST',
|
|
249
|
-
headers: { 'Content-Type': 'application/json' },
|
|
250
|
-
body: JSON.stringify({ type: 'perpDexs' }),
|
|
251
|
-
});
|
|
252
|
-
const data = await response.json();
|
|
466
|
+
const data = await this.postInfo<Array<{
|
|
467
|
+
name: string;
|
|
468
|
+
fullName: string;
|
|
469
|
+
deployer: string;
|
|
470
|
+
} | null>>({ type: 'perpDexs' }, 'perpDexs');
|
|
253
471
|
this.log('perpDexs response:', JSON.stringify(data).slice(0, 500));
|
|
254
472
|
this.perpDexsCache = data;
|
|
255
473
|
return data;
|
|
@@ -273,10 +491,6 @@ export class HyperliquidClient {
|
|
|
273
491
|
}>;
|
|
274
492
|
}>> {
|
|
275
493
|
this.log('Fetching all perp markets...');
|
|
276
|
-
const baseUrl = isMainnet()
|
|
277
|
-
? 'https://api.hyperliquid.xyz'
|
|
278
|
-
: 'https://api.hyperliquid-testnet.xyz';
|
|
279
|
-
|
|
280
494
|
const results: Array<{
|
|
281
495
|
dexName: string | null;
|
|
282
496
|
meta: { universe: Array<{ name: string; szDecimals: number; maxLeverage: number; onlyIsolated?: boolean }> };
|
|
@@ -292,12 +506,7 @@ export class HyperliquidClient {
|
|
|
292
506
|
}> = [];
|
|
293
507
|
|
|
294
508
|
// Get main dex data (no dex parameter)
|
|
295
|
-
const
|
|
296
|
-
method: 'POST',
|
|
297
|
-
headers: { 'Content-Type': 'application/json' },
|
|
298
|
-
body: JSON.stringify({ type: 'metaAndAssetCtxs' }),
|
|
299
|
-
});
|
|
300
|
-
const mainData = await mainResponse.json();
|
|
509
|
+
const mainData = await this.postInfo<any>({ type: 'metaAndAssetCtxs' }, 'metaAndAssetCtxs(main)');
|
|
301
510
|
this.log('Main dex data fetched');
|
|
302
511
|
|
|
303
512
|
results.push({
|
|
@@ -306,32 +515,32 @@ export class HyperliquidClient {
|
|
|
306
515
|
assetCtxs: mainData[1],
|
|
307
516
|
});
|
|
308
517
|
|
|
309
|
-
// Get HIP-3 dex names
|
|
310
|
-
const
|
|
518
|
+
// Get HIP-3 dex names and fetch all in parallel (testnet: only loaded dexes)
|
|
519
|
+
const validDexs = await this.getIterableHip3Dexs();
|
|
311
520
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
521
|
+
const hip3Results = await this.batchSettled(
|
|
522
|
+
validDexs.map((dex) => async () => {
|
|
523
|
+
const data = await this.postInfo<any>(
|
|
524
|
+
{ type: 'metaAndAssetCtxs', dex: dex.name },
|
|
525
|
+
`metaAndAssetCtxs(${dex.name})`
|
|
526
|
+
);
|
|
527
|
+
return { dex, data };
|
|
528
|
+
})
|
|
529
|
+
);
|
|
316
530
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
531
|
+
for (const result of hip3Results) {
|
|
532
|
+
if (result.status === 'rejected') {
|
|
533
|
+
this.log('Failed to fetch HIP-3 dex:', result.reason);
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
const { dex, data: dexData } = result.value;
|
|
537
|
+
if (dexData && dexData[0]?.universe) {
|
|
538
|
+
this.log(`Fetched HIP-3 dex: ${dex.name} with ${dexData[0].universe.length} markets`);
|
|
539
|
+
results.push({
|
|
540
|
+
dexName: dex.name,
|
|
541
|
+
meta: { universe: dexData[0].universe },
|
|
542
|
+
assetCtxs: dexData[1] || [],
|
|
322
543
|
});
|
|
323
|
-
const dexData = await dexResponse.json();
|
|
324
|
-
|
|
325
|
-
if (dexData && dexData[0]?.universe) {
|
|
326
|
-
this.log(`Fetched HIP-3 dex: ${dex.name} with ${dexData[0].universe.length} markets`);
|
|
327
|
-
results.push({
|
|
328
|
-
dexName: dex.name,
|
|
329
|
-
meta: { universe: dexData[0].universe },
|
|
330
|
-
assetCtxs: dexData[1] || [],
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
} catch (e) {
|
|
334
|
-
this.log(`Failed to fetch HIP-3 dex ${dex.name}:`, e);
|
|
335
544
|
}
|
|
336
545
|
}
|
|
337
546
|
|
|
@@ -359,16 +568,23 @@ export class HyperliquidClient {
|
|
|
359
568
|
}>;
|
|
360
569
|
}> {
|
|
361
570
|
this.log('Fetching spotMeta...');
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
571
|
+
const data = await this.postInfo<{
|
|
572
|
+
tokens: Array<{
|
|
573
|
+
name: string;
|
|
574
|
+
szDecimals: number;
|
|
575
|
+
weiDecimals: number;
|
|
576
|
+
index: number;
|
|
577
|
+
tokenId: string;
|
|
578
|
+
isCanonical: boolean;
|
|
579
|
+
fullName: string | null;
|
|
580
|
+
}>;
|
|
581
|
+
universe: Array<{
|
|
582
|
+
name: string;
|
|
583
|
+
tokens: [number, number];
|
|
584
|
+
index: number;
|
|
585
|
+
isCanonical: boolean;
|
|
586
|
+
}>;
|
|
587
|
+
}>({ type: 'spotMeta' }, 'spotMeta');
|
|
372
588
|
this.log('spotMeta response:', JSON.stringify(data).slice(0, 500));
|
|
373
589
|
return data;
|
|
374
590
|
}
|
|
@@ -394,6 +610,7 @@ export class HyperliquidClient {
|
|
|
394
610
|
}>;
|
|
395
611
|
};
|
|
396
612
|
assetCtxs: Array<{
|
|
613
|
+
coin?: string;
|
|
397
614
|
dayNtlVlm: string;
|
|
398
615
|
markPx: string;
|
|
399
616
|
midPx: string;
|
|
@@ -401,17 +618,35 @@ export class HyperliquidClient {
|
|
|
401
618
|
}>;
|
|
402
619
|
}> {
|
|
403
620
|
this.log('Fetching spotMetaAndAssetCtxs...');
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const response = await fetch(baseUrl + '/info', {
|
|
409
|
-
method: 'POST',
|
|
410
|
-
headers: { 'Content-Type': 'application/json' },
|
|
411
|
-
body: JSON.stringify({ type: 'spotMetaAndAssetCtxs' }),
|
|
412
|
-
});
|
|
413
|
-
const data = await response.json();
|
|
621
|
+
const data = await this.postInfo<unknown>(
|
|
622
|
+
{ type: 'spotMetaAndAssetCtxs' },
|
|
623
|
+
'spotMetaAndAssetCtxs'
|
|
624
|
+
);
|
|
414
625
|
this.log('spotMetaAndAssetCtxs response:', JSON.stringify(data).slice(0, 500));
|
|
626
|
+
|
|
627
|
+
if (!Array.isArray(data) || !data[0] || !data[1]) {
|
|
628
|
+
this.log('spotMetaAndAssetCtxs returned null/malformed data, falling back to spotMeta + allMids');
|
|
629
|
+
|
|
630
|
+
const [meta, mids] = await Promise.all([
|
|
631
|
+
this.getSpotMeta(),
|
|
632
|
+
this.getAllMids(),
|
|
633
|
+
]);
|
|
634
|
+
|
|
635
|
+
return {
|
|
636
|
+
meta,
|
|
637
|
+
assetCtxs: meta.universe.map((pair) => {
|
|
638
|
+
const price = mids[pair.name] ?? '0';
|
|
639
|
+
return {
|
|
640
|
+
coin: pair.name,
|
|
641
|
+
dayNtlVlm: '0',
|
|
642
|
+
markPx: price,
|
|
643
|
+
midPx: price,
|
|
644
|
+
prevDayPx: price,
|
|
645
|
+
};
|
|
646
|
+
}),
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
415
650
|
return {
|
|
416
651
|
meta: data[0],
|
|
417
652
|
assetCtxs: data[1],
|
|
@@ -453,9 +688,12 @@ export class HyperliquidClient {
|
|
|
453
688
|
}
|
|
454
689
|
|
|
455
690
|
this.spotAssetMap.set(baseToken.name, spotAssetIndex);
|
|
691
|
+
this.spotPairNameMap.set(baseToken.name, pair.name);
|
|
456
692
|
this.spotSzDecimalsMap.set(baseToken.name, baseToken.szDecimals);
|
|
457
693
|
|
|
458
|
-
this.log(
|
|
694
|
+
this.log(
|
|
695
|
+
`Spot: ${baseToken.name} → asset ${spotAssetIndex}, market ${pair.name} (szDecimals: ${baseToken.szDecimals})`
|
|
696
|
+
);
|
|
459
697
|
}
|
|
460
698
|
|
|
461
699
|
this.spotMetaLoaded = true;
|
|
@@ -470,6 +708,11 @@ export class HyperliquidClient {
|
|
|
470
708
|
return this.spotAssetMap.get(coin);
|
|
471
709
|
}
|
|
472
710
|
|
|
711
|
+
/** Get the preferred spot market key for a coin (e.g. "@230", "PURR/USDC") */
|
|
712
|
+
getSpotMarketKey(coin: string): string | undefined {
|
|
713
|
+
return this.spotPairNameMap.get(coin);
|
|
714
|
+
}
|
|
715
|
+
|
|
473
716
|
/** Get spot szDecimals for a coin */
|
|
474
717
|
getSpotSzDecimals(coin: string): number | undefined {
|
|
475
718
|
return this.spotSzDecimalsMap.get(coin);
|
|
@@ -493,19 +736,18 @@ export class HyperliquidClient {
|
|
|
493
736
|
}>;
|
|
494
737
|
}> {
|
|
495
738
|
this.log('Fetching spotClearinghouseState for:', user ?? this.address);
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
});
|
|
508
|
-
const data = await response.json();
|
|
739
|
+
const data = await this.postInfo<{
|
|
740
|
+
balances: Array<{
|
|
741
|
+
coin: string;
|
|
742
|
+
token: number;
|
|
743
|
+
hold: string;
|
|
744
|
+
total: string;
|
|
745
|
+
entryNtl: string;
|
|
746
|
+
}>;
|
|
747
|
+
}>({
|
|
748
|
+
type: 'spotClearinghouseState',
|
|
749
|
+
user: user ?? this.address,
|
|
750
|
+
}, 'spotClearinghouseState');
|
|
509
751
|
this.log('spotClearinghouseState response:', JSON.stringify(data).slice(0, 500));
|
|
510
752
|
return data;
|
|
511
753
|
}
|
|
@@ -585,7 +827,16 @@ export class HyperliquidClient {
|
|
|
585
827
|
}> {
|
|
586
828
|
this.log('Fetching l2Book for:', coin);
|
|
587
829
|
// API accepts prefixed names directly (e.g., "xyz:CL")
|
|
588
|
-
|
|
830
|
+
let response;
|
|
831
|
+
try {
|
|
832
|
+
response = await this.info.l2Book({ coin });
|
|
833
|
+
} catch (error) {
|
|
834
|
+
this.log('l2Book failure context:', this.getTransportContext(`l2Book(${coin})`));
|
|
835
|
+
if (error instanceof Error && error.stack) {
|
|
836
|
+
this.log('l2Book stack:', error.stack);
|
|
837
|
+
}
|
|
838
|
+
throw new Error(`l2Book(${coin}) failed: ${this.describeError(error)}`);
|
|
839
|
+
}
|
|
589
840
|
|
|
590
841
|
const bids = response.levels[0] as Array<{ px: string; sz: string; n: number }>;
|
|
591
842
|
const asks = response.levels[1] as Array<{ px: string; sz: string; n: number }>;
|
|
@@ -607,6 +858,28 @@ export class HyperliquidClient {
|
|
|
607
858
|
};
|
|
608
859
|
}
|
|
609
860
|
|
|
861
|
+
async getAssetIndexAsync(coin: string): Promise<number> {
|
|
862
|
+
let index = this.assetMap.get(coin);
|
|
863
|
+
if (index === undefined && coin.includes(':')) {
|
|
864
|
+
// Try on-demand loading the dex (e.g., "felix:BTC" → load "felix")
|
|
865
|
+
const dexName = coin.split(':')[0];
|
|
866
|
+
await this.loadSingleHip3Dex(dexName);
|
|
867
|
+
index = this.assetMap.get(coin);
|
|
868
|
+
}
|
|
869
|
+
if (index === undefined) {
|
|
870
|
+
const hip3Matches = this.findHip3Matches(coin);
|
|
871
|
+
if (hip3Matches.length > 0) {
|
|
872
|
+
const suggestions = hip3Matches.map(m => `${m}`).join(', ');
|
|
873
|
+
throw new Error(
|
|
874
|
+
`Unknown asset: ${coin}. Did you mean one of these HIP-3 assets? ${suggestions}\n` +
|
|
875
|
+
`Use "openbroker search --query ${coin}" to find the full ticker.`
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
throw new Error(`Unknown asset: ${coin}. Available: ${Array.from(this.assetMap.keys()).slice(0, 10).join(', ')}...`);
|
|
879
|
+
}
|
|
880
|
+
return index;
|
|
881
|
+
}
|
|
882
|
+
|
|
610
883
|
getAssetIndex(coin: string): number {
|
|
611
884
|
const index = this.assetMap.get(coin);
|
|
612
885
|
if (index === undefined) {
|
|
@@ -624,6 +897,25 @@ export class HyperliquidClient {
|
|
|
624
897
|
return index;
|
|
625
898
|
}
|
|
626
899
|
|
|
900
|
+
async getSzDecimalsAsync(coin: string): Promise<number> {
|
|
901
|
+
let decimals = this.szDecimalsMap.get(coin);
|
|
902
|
+
if (decimals === undefined && coin.includes(':')) {
|
|
903
|
+
const dexName = coin.split(':')[0];
|
|
904
|
+
await this.loadSingleHip3Dex(dexName);
|
|
905
|
+
decimals = this.szDecimalsMap.get(coin);
|
|
906
|
+
}
|
|
907
|
+
if (decimals === undefined) {
|
|
908
|
+
const hip3Matches = this.findHip3Matches(coin);
|
|
909
|
+
if (hip3Matches.length > 0) {
|
|
910
|
+
throw new Error(
|
|
911
|
+
`Unknown asset: ${coin}. Did you mean: ${hip3Matches.join(', ')}?`
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
throw new Error(`Unknown asset: ${coin}`);
|
|
915
|
+
}
|
|
916
|
+
return decimals;
|
|
917
|
+
}
|
|
918
|
+
|
|
627
919
|
getSzDecimals(coin: string): number {
|
|
628
920
|
const decimals = this.szDecimalsMap.get(coin);
|
|
629
921
|
if (decimals === undefined) {
|
|
@@ -755,6 +1047,67 @@ export class HyperliquidClient {
|
|
|
755
1047
|
return mode === 'unified' || mode === 'portfolio';
|
|
756
1048
|
}
|
|
757
1049
|
|
|
1050
|
+
/**
|
|
1051
|
+
* Query the role of an address on HyperCore L1.
|
|
1052
|
+
* Returns: "user" | "agent" | "vault" | "subAccount" | "missing"
|
|
1053
|
+
* Useful for verifying API wallet (agent) registration.
|
|
1054
|
+
*/
|
|
1055
|
+
async getUserRole(address?: string): Promise<{ role: string; data?: Record<string, string> }> {
|
|
1056
|
+
const target = address ?? this.address;
|
|
1057
|
+
this.log('Fetching userRole for:', target);
|
|
1058
|
+
try {
|
|
1059
|
+
const response = await this.postInfo<{ role: string; data?: Record<string, string> }>(
|
|
1060
|
+
{ type: 'userRole', user: target },
|
|
1061
|
+
'userRole'
|
|
1062
|
+
);
|
|
1063
|
+
this.log('userRole response:', JSON.stringify(response));
|
|
1064
|
+
return response ?? { role: 'missing' };
|
|
1065
|
+
} catch (e) {
|
|
1066
|
+
this.log('userRole query failed:', e);
|
|
1067
|
+
return { role: 'unknown' };
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* Validate API wallet setup: check that the signing wallet is recognized
|
|
1073
|
+
* as an "agent" on HyperCore and the account address exists.
|
|
1074
|
+
* Logs warnings if misconfigured.
|
|
1075
|
+
*/
|
|
1076
|
+
async validateApiWalletSetup(): Promise<{ valid: boolean; walletRole: string; accountRole: string }> {
|
|
1077
|
+
const walletResult = await this.getUserRole(this.walletAddress);
|
|
1078
|
+
const accountResult = await this.getUserRole(this.address);
|
|
1079
|
+
const walletRole = walletResult.role;
|
|
1080
|
+
const accountRole = accountResult.role;
|
|
1081
|
+
|
|
1082
|
+
this.log(`API wallet validation: wallet ${this.walletAddress} role=${walletRole}, account ${this.address} role=${accountRole}`);
|
|
1083
|
+
|
|
1084
|
+
if (walletRole === 'agent') {
|
|
1085
|
+
const masterAddress = walletResult.data?.user;
|
|
1086
|
+
if (masterAddress && masterAddress.toLowerCase() !== this.address.toLowerCase()) {
|
|
1087
|
+
console.warn(
|
|
1088
|
+
`\x1b[33m⚠️ API wallet ${this.walletAddress} is an agent for ${masterAddress}, but HYPERLIQUID_ACCOUNT_ADDRESS is ${this.address}.\n` +
|
|
1089
|
+
` These should match.\x1b[0m`
|
|
1090
|
+
);
|
|
1091
|
+
} else {
|
|
1092
|
+
this.log(`API wallet confirmed as agent for ${masterAddress ?? this.address}`);
|
|
1093
|
+
}
|
|
1094
|
+
} else {
|
|
1095
|
+
console.warn(
|
|
1096
|
+
`\x1b[33m⚠️ API wallet ${this.walletAddress} has role "${walletRole}" on HyperCore (expected "agent").\n` +
|
|
1097
|
+
` Make sure the agent is registered via CoreWriter.registerAgent() on the correct network (${isMainnet() ? 'mainnet' : 'testnet'}).\x1b[0m`
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (accountRole === 'missing') {
|
|
1102
|
+
console.warn(
|
|
1103
|
+
`\x1b[33m⚠️ Account ${this.address} has role "missing" on HyperCore.\n` +
|
|
1104
|
+
` The account may not exist on ${isMainnet() ? 'mainnet' : 'testnet'} yet. Ensure the contract is deployed and has interacted with HyperCore.\x1b[0m`
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
return { valid: walletRole === 'agent', walletRole, accountRole };
|
|
1109
|
+
}
|
|
1110
|
+
|
|
758
1111
|
/**
|
|
759
1112
|
* Check if an address has sub-accounts (is a master account)
|
|
760
1113
|
* Sub-accounts cannot approve builder fees - only master accounts can
|
|
@@ -844,7 +1197,7 @@ export class HyperliquidClient {
|
|
|
844
1197
|
const response = await this.exchange.approveBuilderFee({
|
|
845
1198
|
builder: targetBuilder as `0x${string}`,
|
|
846
1199
|
maxFeeRate,
|
|
847
|
-
});
|
|
1200
|
+
}, this.vaultParam);
|
|
848
1201
|
this.log('approveBuilderFee response:', response);
|
|
849
1202
|
return { status: 'ok', response };
|
|
850
1203
|
} catch (error) {
|
|
@@ -1193,7 +1546,17 @@ export class HyperliquidClient {
|
|
|
1193
1546
|
this.log('Fetching clearinghouseState for:', user ?? this.address, dex ? `dex: ${dex}` : '');
|
|
1194
1547
|
const params: { user: string; dex?: string } = { user: user ?? this.address };
|
|
1195
1548
|
if (dex !== undefined) params.dex = dex;
|
|
1196
|
-
const
|
|
1549
|
+
const label = dex ? `clearinghouseState(${dex})` : 'clearinghouseState(main)';
|
|
1550
|
+
let response;
|
|
1551
|
+
try {
|
|
1552
|
+
response = await this.withRetry(() => this.info.clearinghouseState(params as any), label);
|
|
1553
|
+
} catch (error) {
|
|
1554
|
+
this.log(`${label} failure context:`, this.getTransportContext(label));
|
|
1555
|
+
if (error instanceof Error && error.stack) {
|
|
1556
|
+
this.log(`${label} stack:`, error.stack);
|
|
1557
|
+
}
|
|
1558
|
+
throw new Error(`${label} failed: ${this.describeError(error)}`);
|
|
1559
|
+
}
|
|
1197
1560
|
|
|
1198
1561
|
// The SDK response has `withdrawable` as a top-level field, not inside
|
|
1199
1562
|
// marginSummary/crossMarginSummary. Copy it into our MarginSummary shape.
|
|
@@ -1218,43 +1581,48 @@ export class HyperliquidClient {
|
|
|
1218
1581
|
|
|
1219
1582
|
const unified = await this.isUnifiedAccount(user);
|
|
1220
1583
|
const mainState = await this.getUserState(user);
|
|
1221
|
-
const dexs = await this.getPerpDexs();
|
|
1222
1584
|
|
|
1223
|
-
// Collect positions from all HIP-3 dexes
|
|
1585
|
+
// Collect positions from all HIP-3 dexes (in parallel; testnet: only loaded dexes)
|
|
1586
|
+
const validDexs = await this.getIterableHip3Dexs();
|
|
1587
|
+
const dexResults = await this.batchSettled(
|
|
1588
|
+
validDexs.map((dex) => async () => {
|
|
1589
|
+
const dexState = await this.getUserState(user, dex.name);
|
|
1590
|
+
return { dex, dexState };
|
|
1591
|
+
})
|
|
1592
|
+
);
|
|
1593
|
+
|
|
1224
1594
|
let hip3Errors = 0;
|
|
1225
|
-
|
|
1226
|
-
const
|
|
1227
|
-
|
|
1595
|
+
const safeAdd = (a: string | undefined, b: string | undefined): string => {
|
|
1596
|
+
const va = parseFloat(a ?? '0') || 0;
|
|
1597
|
+
const vb = parseFloat(b ?? '0') || 0;
|
|
1598
|
+
return String(va + vb);
|
|
1599
|
+
};
|
|
1228
1600
|
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1601
|
+
for (const result of dexResults) {
|
|
1602
|
+
if (result.status === 'rejected') {
|
|
1603
|
+
hip3Errors++;
|
|
1604
|
+
this.log(`Failed to fetch state for HIP-3 dex:`, result.reason instanceof Error ? result.reason.message : String(result.reason));
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
const { dexState } = result.value;
|
|
1608
|
+
if (dexState.assetPositions?.length > 0) {
|
|
1609
|
+
mainState.assetPositions.push(...dexState.assetPositions);
|
|
1610
|
+
}
|
|
1234
1611
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
summary.totalMarginUsed = safeAdd(summary.totalMarginUsed, dexMargin.totalMarginUsed);
|
|
1249
|
-
summary.withdrawable = safeAdd(summary.withdrawable, dexMargin.withdrawable);
|
|
1250
|
-
};
|
|
1251
|
-
addToSummary(mainState.marginSummary);
|
|
1252
|
-
addToSummary(mainState.crossMarginSummary);
|
|
1253
|
-
}
|
|
1612
|
+
// For standard accounts, aggregate margin from each dex
|
|
1613
|
+
if (!unified) {
|
|
1614
|
+
const dexMargin = dexState.marginSummary;
|
|
1615
|
+
if (dexMargin) {
|
|
1616
|
+
const addToSummary = (summary: { accountValue: string; totalNtlPos: string; totalRawUsd: string; totalMarginUsed: string; withdrawable: string }) => {
|
|
1617
|
+
summary.accountValue = safeAdd(summary.accountValue, dexMargin.accountValue);
|
|
1618
|
+
summary.totalNtlPos = safeAdd(summary.totalNtlPos, dexMargin.totalNtlPos);
|
|
1619
|
+
summary.totalRawUsd = safeAdd(summary.totalRawUsd, dexMargin.totalRawUsd);
|
|
1620
|
+
summary.totalMarginUsed = safeAdd(summary.totalMarginUsed, dexMargin.totalMarginUsed);
|
|
1621
|
+
summary.withdrawable = safeAdd(summary.withdrawable, dexMargin.withdrawable);
|
|
1622
|
+
};
|
|
1623
|
+
addToSummary(mainState.marginSummary);
|
|
1624
|
+
addToSummary(mainState.crossMarginSummary);
|
|
1254
1625
|
}
|
|
1255
|
-
} catch (err) {
|
|
1256
|
-
hip3Errors++;
|
|
1257
|
-
this.log(`Failed to fetch state for dex ${dex.name}:`, err instanceof Error ? err.message : String(err));
|
|
1258
1626
|
}
|
|
1259
1627
|
}
|
|
1260
1628
|
|
|
@@ -1314,21 +1682,26 @@ export class HyperliquidClient {
|
|
|
1314
1682
|
await this.getMetaAndAssetCtxs(); // Ensure HIP-3 dex list is loaded
|
|
1315
1683
|
|
|
1316
1684
|
// Fetch main dex orders
|
|
1317
|
-
const orders = await this.info.openOrders({ user: user ?? this.address }) as OpenOrder[];
|
|
1685
|
+
const orders = await this.withRetry(() => this.info.openOrders({ user: user ?? this.address }), 'openOrders') as OpenOrder[];
|
|
1318
1686
|
|
|
1319
|
-
// Fetch HIP-3 dex orders
|
|
1320
|
-
const
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
if (!dex) continue;
|
|
1324
|
-
try {
|
|
1687
|
+
// Fetch HIP-3 dex orders (in parallel; testnet: only loaded dexes)
|
|
1688
|
+
const validDexs = await this.getIterableHip3Dexs();
|
|
1689
|
+
const dexResults = await this.batchSettled(
|
|
1690
|
+
validDexs.map((dex) => async () => {
|
|
1325
1691
|
const dexOrders = await this.info.openOrders({ user: user ?? this.address, dex: dex.name }) as OpenOrder[];
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1692
|
+
return { dex, dexOrders };
|
|
1693
|
+
})
|
|
1694
|
+
);
|
|
1695
|
+
|
|
1696
|
+
for (const result of dexResults) {
|
|
1697
|
+
if (result.status === 'rejected') {
|
|
1698
|
+
this.log('Failed to fetch open orders for HIP-3 dex:', result.reason instanceof Error ? result.reason.message : String(result.reason));
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1701
|
+
const { dex, dexOrders } = result.value;
|
|
1702
|
+
if (dexOrders.length > 0) {
|
|
1703
|
+
this.log(`Found ${dexOrders.length} open orders on HIP-3 dex ${dex.name}`);
|
|
1704
|
+
orders.push(...dexOrders);
|
|
1332
1705
|
}
|
|
1333
1706
|
}
|
|
1334
1707
|
|
|
@@ -1384,7 +1757,7 @@ export class HyperliquidClient {
|
|
|
1384
1757
|
destinationDex: dexInfo.dexName,
|
|
1385
1758
|
token: 'USDC:0x6d1e7cde53ba9467b783cb7c530ce054',
|
|
1386
1759
|
amount: String(transferAmount),
|
|
1387
|
-
});
|
|
1760
|
+
}, this.vaultParam as any);
|
|
1388
1761
|
this.log(`Transferred ${transferAmount} USDC to ${dexInfo.dexName} dex`);
|
|
1389
1762
|
} catch (err) {
|
|
1390
1763
|
// Log but don't block — dex may already have sufficient balance
|
|
@@ -1402,7 +1775,7 @@ export class HyperliquidClient {
|
|
|
1402
1775
|
includeBuilder: boolean = true,
|
|
1403
1776
|
leverage?: number
|
|
1404
1777
|
): Promise<OrderResponse> {
|
|
1405
|
-
this.requireTrading();
|
|
1778
|
+
await this.requireTrading();
|
|
1406
1779
|
await this.getMetaAndAssetCtxs();
|
|
1407
1780
|
|
|
1408
1781
|
// Set leverage if specified (for main perps, cross margin; for HIP-3, handled in ensureHip3Ready)
|
|
@@ -1437,14 +1810,14 @@ export class HyperliquidClient {
|
|
|
1437
1810
|
grouping: 'na',
|
|
1438
1811
|
};
|
|
1439
1812
|
|
|
1440
|
-
// Add builder fee if configured
|
|
1441
|
-
if (includeBuilder && this.config.builderAddress !== '0x0000000000000000000000000000000000000000') {
|
|
1813
|
+
// Add builder fee if configured (skip on testnet — builder may not be approved)
|
|
1814
|
+
if (includeBuilder && !this.isTestnet && this.config.builderAddress !== '0x0000000000000000000000000000000000000000') {
|
|
1442
1815
|
orderRequest.builder = this.builderInfo;
|
|
1443
1816
|
this.log('Including builder fee:', this.builderInfo);
|
|
1444
1817
|
}
|
|
1445
1818
|
|
|
1446
1819
|
try {
|
|
1447
|
-
const response = await this.exchange.order(orderRequest);
|
|
1820
|
+
const response = await this.exchange.order(orderRequest, this.vaultParam);
|
|
1448
1821
|
this.log('Order response:', JSON.stringify(response, null, 2));
|
|
1449
1822
|
return response as unknown as OrderResponse;
|
|
1450
1823
|
} catch (error) {
|
|
@@ -1534,7 +1907,7 @@ export class HyperliquidClient {
|
|
|
1534
1907
|
reduceOnly: boolean = true,
|
|
1535
1908
|
leverage?: number
|
|
1536
1909
|
): Promise<OrderResponse> {
|
|
1537
|
-
this.requireTrading();
|
|
1910
|
+
await this.requireTrading();
|
|
1538
1911
|
await this.getMetaAndAssetCtxs();
|
|
1539
1912
|
|
|
1540
1913
|
// Set leverage if specified (for main perps)
|
|
@@ -1578,14 +1951,14 @@ export class HyperliquidClient {
|
|
|
1578
1951
|
grouping: 'na',
|
|
1579
1952
|
};
|
|
1580
1953
|
|
|
1581
|
-
// Add builder fee if configured
|
|
1582
|
-
if (this.config.builderAddress !== '0x0000000000000000000000000000000000000000') {
|
|
1954
|
+
// Add builder fee if configured (skip on testnet — builder may not be approved)
|
|
1955
|
+
if (!this.isTestnet && this.config.builderAddress !== '0x0000000000000000000000000000000000000000') {
|
|
1583
1956
|
orderRequest.builder = this.builderInfo;
|
|
1584
1957
|
this.log('Including builder fee:', this.builderInfo);
|
|
1585
1958
|
}
|
|
1586
1959
|
|
|
1587
1960
|
try {
|
|
1588
|
-
const response = await this.exchange.order(orderRequest);
|
|
1961
|
+
const response = await this.exchange.order(orderRequest, this.vaultParam);
|
|
1589
1962
|
this.log('Trigger order response:', JSON.stringify(response, null, 2));
|
|
1590
1963
|
return response as unknown as OrderResponse;
|
|
1591
1964
|
} catch (error) {
|
|
@@ -1631,7 +2004,7 @@ export class HyperliquidClient {
|
|
|
1631
2004
|
}
|
|
1632
2005
|
|
|
1633
2006
|
async cancel(coin: string, oid: number): Promise<CancelResponse> {
|
|
1634
|
-
this.requireTrading();
|
|
2007
|
+
await this.requireTrading();
|
|
1635
2008
|
await this.getMetaAndAssetCtxs();
|
|
1636
2009
|
|
|
1637
2010
|
const assetIndex = this.getAssetIndex(coin);
|
|
@@ -1641,7 +2014,7 @@ export class HyperliquidClient {
|
|
|
1641
2014
|
try {
|
|
1642
2015
|
const response = await this.exchange.cancel({
|
|
1643
2016
|
cancels: [{ a: assetIndex, o: oid }],
|
|
1644
|
-
});
|
|
2017
|
+
}, this.vaultParam);
|
|
1645
2018
|
this.log('Cancel response:', JSON.stringify(response, null, 2));
|
|
1646
2019
|
return response as unknown as CancelResponse;
|
|
1647
2020
|
} catch (error) {
|
|
@@ -1688,7 +2061,7 @@ export class HyperliquidClient {
|
|
|
1688
2061
|
orderType: { limit: { tif: 'Gtc' | 'Ioc' | 'Alo' } },
|
|
1689
2062
|
includeBuilder: boolean = true,
|
|
1690
2063
|
): Promise<OrderResponse> {
|
|
1691
|
-
this.requireTrading();
|
|
2064
|
+
await this.requireTrading();
|
|
1692
2065
|
await this.loadSpotMeta();
|
|
1693
2066
|
|
|
1694
2067
|
const assetIndex = this.spotAssetMap.get(coin);
|
|
@@ -1721,14 +2094,14 @@ export class HyperliquidClient {
|
|
|
1721
2094
|
grouping: 'na',
|
|
1722
2095
|
};
|
|
1723
2096
|
|
|
1724
|
-
// Add builder fee if configured (
|
|
1725
|
-
if (includeBuilder && this.config.builderAddress !== '0x0000000000000000000000000000000000000000') {
|
|
2097
|
+
// Add builder fee if configured (skip on testnet — builder may not be approved)
|
|
2098
|
+
if (includeBuilder && !this.isTestnet && this.config.builderAddress !== '0x0000000000000000000000000000000000000000') {
|
|
1726
2099
|
orderRequest.builder = this.builderInfo;
|
|
1727
2100
|
this.log('Including builder fee:', this.builderInfo);
|
|
1728
2101
|
}
|
|
1729
2102
|
|
|
1730
2103
|
try {
|
|
1731
|
-
const response = await this.exchange.order(orderRequest);
|
|
2104
|
+
const response = await this.exchange.order(orderRequest, this.vaultParam);
|
|
1732
2105
|
this.log('Spot order response:', JSON.stringify(response, null, 2));
|
|
1733
2106
|
return response as unknown as OrderResponse;
|
|
1734
2107
|
} catch (error) {
|
|
@@ -1755,18 +2128,33 @@ export class HyperliquidClient {
|
|
|
1755
2128
|
): Promise<OrderResponse> {
|
|
1756
2129
|
await this.loadSpotMeta();
|
|
1757
2130
|
|
|
1758
|
-
// Get the spot pair name (@index or PURR/USDC) for allMids lookup
|
|
1759
2131
|
const assetIndex = this.spotAssetMap.get(coin);
|
|
1760
|
-
|
|
2132
|
+
const spotCoinKey = this.spotPairNameMap.get(coin);
|
|
2133
|
+
if (assetIndex === undefined || !spotCoinKey) {
|
|
1761
2134
|
throw new Error(`Unknown spot asset: ${coin}. Use "openbroker spot" to see available markets.`);
|
|
1762
2135
|
}
|
|
1763
|
-
const spotPairIndex = assetIndex - 10000;
|
|
1764
|
-
// Canonical PURR/USDC is index 0, everything else uses @index
|
|
1765
|
-
const spotCoinKey = spotPairIndex === 0 ? 'PURR/USDC' : `@${spotPairIndex}`;
|
|
1766
2136
|
|
|
1767
|
-
// Use
|
|
2137
|
+
// Use the exact spot market key from spotMeta (e.g. "@230", "PURR/USDC").
|
|
2138
|
+
// On testnet the tradable asset id and displayed market key can diverge.
|
|
1768
2139
|
const mids = await this.getAllMids();
|
|
1769
|
-
|
|
2140
|
+
let midStr = mids[spotCoinKey];
|
|
2141
|
+
|
|
2142
|
+
// Fallback: allMids may omit spot pairs (especially on testnet).
|
|
2143
|
+
// Try spotMetaAndAssetCtxs which returns markPx directly.
|
|
2144
|
+
if (!midStr) {
|
|
2145
|
+
this.log(`allMids missing spot key "${spotCoinKey}", falling back to spotMetaAndAssetCtxs`);
|
|
2146
|
+
try {
|
|
2147
|
+
const spotData = await this.getSpotMetaAndAssetCtxs();
|
|
2148
|
+
const ctxMap = new Map<string, string>();
|
|
2149
|
+
for (const ctx of spotData.assetCtxs as Array<{ coin?: string; midPx?: string; markPx: string }>) {
|
|
2150
|
+
if (ctx.coin) ctxMap.set(ctx.coin, ctx.midPx || ctx.markPx);
|
|
2151
|
+
}
|
|
2152
|
+
midStr = ctxMap.get(spotCoinKey);
|
|
2153
|
+
} catch (e) {
|
|
2154
|
+
this.log(`spotMetaAndAssetCtxs fallback failed:`, e);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
|
|
1770
2158
|
const midPrice = midStr ? parseFloat(midStr) : 0;
|
|
1771
2159
|
|
|
1772
2160
|
if (!midPrice || midPrice === 0) {
|
|
@@ -1818,7 +2206,7 @@ export class HyperliquidClient {
|
|
|
1818
2206
|
* Cancel a spot order by coin and order ID.
|
|
1819
2207
|
*/
|
|
1820
2208
|
async spotCancel(coin: string, oid: number): Promise<CancelResponse> {
|
|
1821
|
-
this.requireTrading();
|
|
2209
|
+
await this.requireTrading();
|
|
1822
2210
|
await this.loadSpotMeta();
|
|
1823
2211
|
|
|
1824
2212
|
const assetIndex = this.spotAssetMap.get(coin);
|
|
@@ -1831,7 +2219,7 @@ export class HyperliquidClient {
|
|
|
1831
2219
|
try {
|
|
1832
2220
|
const response = await this.exchange.cancel({
|
|
1833
2221
|
cancels: [{ a: assetIndex, o: oid }],
|
|
1834
|
-
});
|
|
2222
|
+
}, this.vaultParam);
|
|
1835
2223
|
this.log('Spot cancel response:', JSON.stringify(response, null, 2));
|
|
1836
2224
|
return response as unknown as CancelResponse;
|
|
1837
2225
|
} catch (error) {
|
|
@@ -1850,7 +2238,7 @@ export class HyperliquidClient {
|
|
|
1850
2238
|
leverage: number,
|
|
1851
2239
|
isCross: boolean = true
|
|
1852
2240
|
): Promise<unknown> {
|
|
1853
|
-
this.requireTrading();
|
|
2241
|
+
await this.requireTrading();
|
|
1854
2242
|
await this.getMetaAndAssetCtxs();
|
|
1855
2243
|
|
|
1856
2244
|
// HIP-3 perps only support isolated margin — override isCross and clamp leverage
|
|
@@ -1875,7 +2263,7 @@ export class HyperliquidClient {
|
|
|
1875
2263
|
asset: assetIndex,
|
|
1876
2264
|
isCross,
|
|
1877
2265
|
leverage,
|
|
1878
|
-
});
|
|
2266
|
+
}, this.vaultParam);
|
|
1879
2267
|
this.log('Leverage response:', JSON.stringify(response, null, 2));
|
|
1880
2268
|
return response;
|
|
1881
2269
|
} catch (error) {
|
|
@@ -1925,7 +2313,8 @@ export class HyperliquidClient {
|
|
|
1925
2313
|
m: durationMinutes,
|
|
1926
2314
|
t: randomize,
|
|
1927
2315
|
},
|
|
1928
|
-
|
|
2316
|
+
|
|
2317
|
+
}, this.vaultParam);
|
|
1929
2318
|
this.log('TWAP order response:', JSON.stringify(response, null, 2));
|
|
1930
2319
|
return response;
|
|
1931
2320
|
} catch (error) {
|
|
@@ -1950,7 +2339,7 @@ export class HyperliquidClient {
|
|
|
1950
2339
|
const response = await this.exchange.twapCancel({
|
|
1951
2340
|
a: assetIndex,
|
|
1952
2341
|
t: twapId,
|
|
1953
|
-
});
|
|
2342
|
+
}, this.vaultParam);
|
|
1954
2343
|
this.log('TWAP cancel response:', JSON.stringify(response, null, 2));
|
|
1955
2344
|
return response;
|
|
1956
2345
|
} catch (error) {
|