openbroker 1.0.85 → 1.0.87
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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/auto/report.ts +2 -2
- package/scripts/auto/runtime.ts +29 -12
- package/scripts/auto/types.ts +17 -1
- package/scripts/core/client.ts +167 -23
- package/scripts/core/types.ts +13 -4
- package/scripts/info/funding.ts +1 -1
- package/scripts/operations/limit-order.ts +1 -1
- package/scripts/operations/twap-cancel.ts +4 -2
- package/scripts/operations/twap.ts +8 -10
- package/scripts/plugin/tools.ts +15 -19
- package/scripts/setup/onboard.ts +8 -2
package/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: Hyperliquid trading plugin with background position monitoring and
|
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Requires Node.js 22+, network access to api.hyperliquid.xyz
|
|
6
6
|
homepage: https://www.npmjs.com/package/openbroker
|
|
7
|
-
metadata: {"author": "monemetrics", "version": "1.0.
|
|
7
|
+
metadata: {"author": "monemetrics", "version": "1.0.87", "openclaw": {"requires": {"bins": ["openbroker"], "env": ["HYPERLIQUID_PRIVATE_KEY"]}, "primaryEnv": "HYPERLIQUID_PRIVATE_KEY", "install": [{"id": "node", "kind": "node", "package": "openbroker", "bins": ["openbroker"], "label": "Install openbroker (npm)"}]}}
|
|
8
8
|
allowed-tools: ob_account ob_positions ob_funding ob_markets ob_search ob_spot ob_fills ob_orders ob_order_status ob_fees ob_candles ob_funding_history ob_trades ob_rate_limit ob_funding_scan ob_buy ob_sell ob_limit ob_trigger ob_tpsl ob_cancel ob_spot_buy ob_spot_sell ob_twap ob_twap_cancel ob_twap_status ob_bracket ob_chase ob_watcher_status ob_auto_run ob_auto_stop ob_auto_list Bash(openbroker:*)
|
|
9
9
|
---
|
|
10
10
|
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/scripts/auto/report.ts
CHANGED
|
@@ -338,12 +338,12 @@ function renderTextReport(data: ReturnType<typeof loadReport>, watchMode = false
|
|
|
338
338
|
console.log('\nEquity');
|
|
339
339
|
console.log('------');
|
|
340
340
|
if (report.equity.first) {
|
|
341
|
-
console.log(`First snapshot: ${formatUsd(report.equity.first.equity)} @ ${formatTimestamp(Number(report.equity.first.timestamp))}`);
|
|
341
|
+
console.log(`First snapshot: ${formatUsd(Number(report.equity.first.equity))} @ ${formatTimestamp(Number(report.equity.first.timestamp))}`);
|
|
342
342
|
} else {
|
|
343
343
|
console.log('First snapshot: -');
|
|
344
344
|
}
|
|
345
345
|
if (report.equity.latest) {
|
|
346
|
-
console.log(`Latest snapshot:${formatUsd(report.equity.latest.equity)} @ ${formatTimestamp(Number(report.equity.latest.timestamp))}`);
|
|
346
|
+
console.log(`Latest snapshot:${formatUsd(Number(report.equity.latest.equity))} @ ${formatTimestamp(Number(report.equity.latest.timestamp))}`);
|
|
347
347
|
} else {
|
|
348
348
|
console.log('Latest snapshot:-');
|
|
349
349
|
}
|
package/scripts/auto/runtime.ts
CHANGED
|
@@ -569,21 +569,19 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
569
569
|
}, 'ws');
|
|
570
570
|
}
|
|
571
571
|
|
|
572
|
-
//
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
572
|
+
// NOTE: order_filled is emitted from the userFill handler below, not from
|
|
573
|
+
// here. The previous implementation fired it from orderUpdate.status
|
|
574
|
+
// === 'filled' using update.order.sz as the size, but that field is the
|
|
575
|
+
// REMAINING size (0 on a terminal fill), not the fill delta — so every
|
|
576
|
+
// consumer saw size=0. Additionally, Hyperliquid does not emit
|
|
577
|
+
// orderUpdate events for pure partial fills that don't transition
|
|
578
|
+
// status, so partial fills were silently dropped entirely. Sourcing
|
|
579
|
+
// order_filled from userFill fixes both issues: sz there IS the fill
|
|
580
|
+
// delta, and the userFills stream fires on every fill (partial and
|
|
581
|
+
// terminal).
|
|
582
582
|
});
|
|
583
583
|
|
|
584
584
|
ws.on('userFill', (fill) => {
|
|
585
|
-
// userFill events are already covered by order_update with status=filled
|
|
586
|
-
// But this provides the realized PnL and fee data that order_update doesn't have
|
|
587
585
|
audit.recordFill({
|
|
588
586
|
coin: fill.coin,
|
|
589
587
|
side: fill.side === 'B' ? 'buy' : 'sell',
|
|
@@ -596,6 +594,25 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
596
594
|
crossed: fill.crossed,
|
|
597
595
|
}, fill.time);
|
|
598
596
|
log.debug(`Fill: ${fill.side === 'B' ? 'BUY' : 'SELL'} ${fill.sz} ${fill.coin} @ ${fill.px} (PnL: ${fill.closedPnl})`);
|
|
597
|
+
|
|
598
|
+
// Emit order_filled with the authoritative fill delta + fee/pnl from
|
|
599
|
+
// the userFills WS stream. Covers both partial and terminal fills.
|
|
600
|
+
if (eventBus.has('order_filled')) {
|
|
601
|
+
const size = parseFloat(fill.sz);
|
|
602
|
+
const price = parseFloat(fill.px);
|
|
603
|
+
const fee = parseFloat(fill.fee);
|
|
604
|
+
const closedPnl = parseFloat(fill.closedPnl);
|
|
605
|
+
void emitAutomationEvent('order_filled', {
|
|
606
|
+
coin: fill.coin,
|
|
607
|
+
oid: fill.oid,
|
|
608
|
+
side: fill.side === 'B' ? 'buy' : 'sell',
|
|
609
|
+
size,
|
|
610
|
+
price,
|
|
611
|
+
fee: Number.isFinite(fee) ? fee : undefined,
|
|
612
|
+
closedPnl: Number.isFinite(closedPnl) ? closedPnl : undefined,
|
|
613
|
+
crossed: fill.crossed,
|
|
614
|
+
}, 'ws');
|
|
615
|
+
}
|
|
599
616
|
});
|
|
600
617
|
|
|
601
618
|
ws.on('userEvent', (event) => {
|
package/scripts/auto/types.ts
CHANGED
|
@@ -46,7 +46,23 @@ export interface AutomationEventPayloads {
|
|
|
46
46
|
position_changed: { coin: string; oldSize: number; newSize: number; entryPrice: number };
|
|
47
47
|
pnl_threshold: { coin: string; unrealizedPnl: number; changePct: number; positionValue: number };
|
|
48
48
|
margin_warning: { marginUsedPct: number; equity: number; marginUsed: number };
|
|
49
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Fires on every trade fill — partial and terminal — sourced from the
|
|
51
|
+
* Hyperliquid `userFills` WS stream. `size` is the fill delta (NOT remaining
|
|
52
|
+
* size of the order). `fee` and `closedPnl` are in USD; `crossed` is true
|
|
53
|
+
* when this side was the taker. Fee/pnl/crossed are optional so that older
|
|
54
|
+
* consumers that only read coin/oid/side/size/price keep working.
|
|
55
|
+
*/
|
|
56
|
+
order_filled: {
|
|
57
|
+
coin: string;
|
|
58
|
+
oid: number;
|
|
59
|
+
side: 'buy' | 'sell';
|
|
60
|
+
size: number;
|
|
61
|
+
price: number;
|
|
62
|
+
fee?: number;
|
|
63
|
+
closedPnl?: number;
|
|
64
|
+
crossed?: boolean;
|
|
65
|
+
};
|
|
50
66
|
/** Real-time order lifecycle event via WebSocket (filled, canceled, rejected, triggered, etc.) */
|
|
51
67
|
order_update: {
|
|
52
68
|
coin: string;
|
package/scripts/core/client.ts
CHANGED
|
@@ -9,6 +9,8 @@ import type {
|
|
|
9
9
|
OrderResponse,
|
|
10
10
|
CancelResponse,
|
|
11
11
|
MetaAndAssetCtxs,
|
|
12
|
+
AssetMeta,
|
|
13
|
+
AssetCtx,
|
|
12
14
|
ClearinghouseState,
|
|
13
15
|
OpenOrder,
|
|
14
16
|
} from './types.js';
|
|
@@ -256,13 +258,16 @@ export class HyperliquidClient {
|
|
|
256
258
|
}
|
|
257
259
|
this.log('metaAndAssetCtxs response:', JSON.stringify(response, null, 2).slice(0, 500) + '...');
|
|
258
260
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
261
|
+
// Build the narrowed value locally so neither narrowing nor the `| null`
|
|
262
|
+
// field type can be lost across `await` / method-call boundaries below.
|
|
263
|
+
const meta: MetaAndAssetCtxs = {
|
|
264
|
+
meta: { universe: response[0].universe as AssetMeta[] },
|
|
265
|
+
assetCtxs: response[1] as AssetCtx[],
|
|
262
266
|
};
|
|
267
|
+
this.meta = meta;
|
|
263
268
|
|
|
264
269
|
// Build lookup maps for main dex
|
|
265
|
-
|
|
270
|
+
meta.meta.universe.forEach((asset, index) => {
|
|
266
271
|
this.assetMap.set(asset.name, index);
|
|
267
272
|
this.szDecimalsMap.set(asset.name, asset.szDecimals);
|
|
268
273
|
this.coinDexMap.set(asset.name, { dexName: null, dexIdx: 0, localName: asset.name });
|
|
@@ -274,7 +279,7 @@ export class HyperliquidClient {
|
|
|
274
279
|
this.hip3Loaded = true;
|
|
275
280
|
}
|
|
276
281
|
|
|
277
|
-
return
|
|
282
|
+
return meta;
|
|
278
283
|
}
|
|
279
284
|
|
|
280
285
|
/**
|
|
@@ -782,7 +787,19 @@ export class HyperliquidClient {
|
|
|
782
787
|
tokenId,
|
|
783
788
|
}),
|
|
784
789
|
});
|
|
785
|
-
const data = await response.json()
|
|
790
|
+
const data = await response.json() as {
|
|
791
|
+
name: string;
|
|
792
|
+
maxSupply: string;
|
|
793
|
+
totalSupply: string;
|
|
794
|
+
circulatingSupply: string;
|
|
795
|
+
szDecimals: number;
|
|
796
|
+
weiDecimals: number;
|
|
797
|
+
midPx: string;
|
|
798
|
+
markPx: string;
|
|
799
|
+
prevDayPx: string;
|
|
800
|
+
deployer: string;
|
|
801
|
+
deployTime: string;
|
|
802
|
+
} | null;
|
|
786
803
|
this.log('tokenDetails response:', JSON.stringify(data).slice(0, 500));
|
|
787
804
|
return data;
|
|
788
805
|
} catch {
|
|
@@ -807,7 +824,10 @@ export class HyperliquidClient {
|
|
|
807
824
|
headers: { 'Content-Type': 'application/json' },
|
|
808
825
|
body: JSON.stringify({ type: 'predictedFundings' }),
|
|
809
826
|
});
|
|
810
|
-
const data = await response.json()
|
|
827
|
+
const data = await response.json() as Array<[
|
|
828
|
+
string,
|
|
829
|
+
Array<[string, { fundingRate: string; nextFundingTime: number }]>
|
|
830
|
+
]>;
|
|
811
831
|
this.log('predictedFundings response length:', data?.length);
|
|
812
832
|
return data;
|
|
813
833
|
}
|
|
@@ -827,7 +847,7 @@ export class HyperliquidClient {
|
|
|
827
847
|
}> {
|
|
828
848
|
this.log('Fetching l2Book for:', coin);
|
|
829
849
|
// API accepts prefixed names directly (e.g., "xyz:CL")
|
|
830
|
-
let response
|
|
850
|
+
let response: Awaited<ReturnType<typeof this.info.l2Book>>;
|
|
831
851
|
try {
|
|
832
852
|
response = await this.info.l2Book({ coin });
|
|
833
853
|
} catch (error) {
|
|
@@ -838,8 +858,12 @@ export class HyperliquidClient {
|
|
|
838
858
|
throw new Error(`l2Book(${coin}) failed: ${this.describeError(error)}`);
|
|
839
859
|
}
|
|
840
860
|
|
|
841
|
-
|
|
842
|
-
|
|
861
|
+
if (!response || !Array.isArray(response.levels)) {
|
|
862
|
+
throw new Error(`l2Book(${coin}) returned empty/malformed payload.`);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const bids = (response.levels[0] ?? []) as Array<{ px: string; sz: string; n: number }>;
|
|
866
|
+
const asks = (response.levels[1] ?? []) as Array<{ px: string; sz: string; n: number }>;
|
|
843
867
|
|
|
844
868
|
const bestBid = bids.length > 0 ? parseFloat(bids[0].px) : 0;
|
|
845
869
|
const bestAsk = asks.length > 0 ? parseFloat(asks[0].px) : 0;
|
|
@@ -1015,7 +1039,7 @@ export class HyperliquidClient {
|
|
|
1015
1039
|
user: user ?? this.address,
|
|
1016
1040
|
}),
|
|
1017
1041
|
});
|
|
1018
|
-
const data = await response.json();
|
|
1042
|
+
const data = await response.json() as string | { abstraction?: string; mode?: string } | null;
|
|
1019
1043
|
this.log('userAbstraction response:', JSON.stringify(data));
|
|
1020
1044
|
|
|
1021
1045
|
// API may return a bare string or an object. Normalize to string for matching.
|
|
@@ -1194,10 +1218,13 @@ export class HyperliquidClient {
|
|
|
1194
1218
|
}
|
|
1195
1219
|
|
|
1196
1220
|
try {
|
|
1221
|
+
// approveBuilderFee is a wallet-level authorization (not a vault action)
|
|
1222
|
+
// and the SDK's `opts` parameter only carries an AbortSignal. Do not pass
|
|
1223
|
+
// vaultParam here — the signature is { signal? }, not a vault wrapper.
|
|
1197
1224
|
const response = await this.exchange.approveBuilderFee({
|
|
1198
1225
|
builder: targetBuilder as `0x${string}`,
|
|
1199
1226
|
maxFeeRate,
|
|
1200
|
-
}
|
|
1227
|
+
});
|
|
1201
1228
|
this.log('approveBuilderFee response:', response);
|
|
1202
1229
|
return { status: 'ok', response };
|
|
1203
1230
|
} catch (error) {
|
|
@@ -1242,7 +1269,18 @@ export class HyperliquidClient {
|
|
|
1242
1269
|
headers: { 'Content-Type': 'application/json' },
|
|
1243
1270
|
body: JSON.stringify(body),
|
|
1244
1271
|
});
|
|
1245
|
-
const data = await response.json()
|
|
1272
|
+
const data = await response.json() as Array<{
|
|
1273
|
+
time: number;
|
|
1274
|
+
hash: string;
|
|
1275
|
+
delta: {
|
|
1276
|
+
type: 'funding';
|
|
1277
|
+
coin: string;
|
|
1278
|
+
usdc: string;
|
|
1279
|
+
szi: string;
|
|
1280
|
+
fundingRate: string;
|
|
1281
|
+
nSamples: number | null;
|
|
1282
|
+
};
|
|
1283
|
+
}>;
|
|
1246
1284
|
this.log('userFunding response length:', data?.length);
|
|
1247
1285
|
return data;
|
|
1248
1286
|
}
|
|
@@ -1285,7 +1323,25 @@ export class HyperliquidClient {
|
|
|
1285
1323
|
headers: { 'Content-Type': 'application/json' },
|
|
1286
1324
|
body: JSON.stringify(body),
|
|
1287
1325
|
});
|
|
1288
|
-
const data = await response.json()
|
|
1326
|
+
const data = await response.json() as Array<{
|
|
1327
|
+
coin: string;
|
|
1328
|
+
px: string;
|
|
1329
|
+
sz: string;
|
|
1330
|
+
side: 'B' | 'A';
|
|
1331
|
+
time: number;
|
|
1332
|
+
startPosition: string;
|
|
1333
|
+
dir: string;
|
|
1334
|
+
closedPnl: string;
|
|
1335
|
+
fee: string;
|
|
1336
|
+
hash: string;
|
|
1337
|
+
oid: number;
|
|
1338
|
+
tid: number;
|
|
1339
|
+
crossed: boolean;
|
|
1340
|
+
feeToken: string;
|
|
1341
|
+
twapId: number | null;
|
|
1342
|
+
cloid: string | null;
|
|
1343
|
+
builderFee: string | null;
|
|
1344
|
+
}>;
|
|
1289
1345
|
this.log('userFills response length:', data?.length);
|
|
1290
1346
|
return data;
|
|
1291
1347
|
}
|
|
@@ -1328,7 +1384,28 @@ export class HyperliquidClient {
|
|
|
1328
1384
|
user: user ?? this.address,
|
|
1329
1385
|
}),
|
|
1330
1386
|
});
|
|
1331
|
-
const data = await response.json()
|
|
1387
|
+
const data = await response.json() as Array<{
|
|
1388
|
+
order: {
|
|
1389
|
+
coin: string;
|
|
1390
|
+
side: string;
|
|
1391
|
+
limitPx: string;
|
|
1392
|
+
sz: string;
|
|
1393
|
+
origSz: string;
|
|
1394
|
+
oid: number;
|
|
1395
|
+
timestamp: number;
|
|
1396
|
+
orderType: string;
|
|
1397
|
+
tif: string | null;
|
|
1398
|
+
cloid: string | null;
|
|
1399
|
+
triggerCondition: string;
|
|
1400
|
+
triggerPx: string;
|
|
1401
|
+
isTrigger: boolean;
|
|
1402
|
+
isPositionTpsl: boolean;
|
|
1403
|
+
reduceOnly: boolean;
|
|
1404
|
+
children: unknown[];
|
|
1405
|
+
};
|
|
1406
|
+
status: string;
|
|
1407
|
+
statusTimestamp: number;
|
|
1408
|
+
}>;
|
|
1332
1409
|
this.log('historicalOrders response length:', data?.length);
|
|
1333
1410
|
return data;
|
|
1334
1411
|
}
|
|
@@ -1374,7 +1451,30 @@ export class HyperliquidClient {
|
|
|
1374
1451
|
oid: typeof oid === 'string' ? oid : oid,
|
|
1375
1452
|
}),
|
|
1376
1453
|
});
|
|
1377
|
-
const data = await response.json()
|
|
1454
|
+
const data = await response.json() as {
|
|
1455
|
+
status: string;
|
|
1456
|
+
order?: {
|
|
1457
|
+
order: {
|
|
1458
|
+
coin: string;
|
|
1459
|
+
side: string;
|
|
1460
|
+
limitPx: string;
|
|
1461
|
+
sz: string;
|
|
1462
|
+
origSz: string;
|
|
1463
|
+
oid: number;
|
|
1464
|
+
timestamp: number;
|
|
1465
|
+
orderType: string;
|
|
1466
|
+
tif: string | null;
|
|
1467
|
+
cloid: string | null;
|
|
1468
|
+
triggerCondition: string;
|
|
1469
|
+
triggerPx: string;
|
|
1470
|
+
isTrigger: boolean;
|
|
1471
|
+
isPositionTpsl: boolean;
|
|
1472
|
+
reduceOnly: boolean;
|
|
1473
|
+
};
|
|
1474
|
+
status: string;
|
|
1475
|
+
statusTimestamp: number;
|
|
1476
|
+
};
|
|
1477
|
+
};
|
|
1378
1478
|
this.log('orderStatus response:', JSON.stringify(data).slice(0, 500));
|
|
1379
1479
|
return data;
|
|
1380
1480
|
}
|
|
@@ -1409,7 +1509,20 @@ export class HyperliquidClient {
|
|
|
1409
1509
|
user: user ?? this.address,
|
|
1410
1510
|
}),
|
|
1411
1511
|
});
|
|
1412
|
-
const data = await response.json()
|
|
1512
|
+
const data = await response.json() as {
|
|
1513
|
+
dailyUserVlm: Array<{ date: string; exchange: string; userCross: string; userAdd: string }>;
|
|
1514
|
+
feeSchedule: Record<string, unknown>;
|
|
1515
|
+
userCrossRate: string;
|
|
1516
|
+
userAddRate: string;
|
|
1517
|
+
userSpotCrossRate: string;
|
|
1518
|
+
userSpotAddRate: string;
|
|
1519
|
+
activeReferralDiscount: string;
|
|
1520
|
+
trial: unknown;
|
|
1521
|
+
feeTrialEscrow: string;
|
|
1522
|
+
nextTrialAvailableTimestamp: unknown;
|
|
1523
|
+
stakingLink: { stakingUser: string; status: string } | null;
|
|
1524
|
+
activeStakingDiscount: { basisPoints: number; discountRate: string } | null;
|
|
1525
|
+
};
|
|
1413
1526
|
this.log('userFees response:', JSON.stringify(data).slice(0, 500));
|
|
1414
1527
|
return data;
|
|
1415
1528
|
}
|
|
@@ -1448,7 +1561,18 @@ export class HyperliquidClient {
|
|
|
1448
1561
|
headers: { 'Content-Type': 'application/json' },
|
|
1449
1562
|
body: JSON.stringify({ type: 'candleSnapshot', req }),
|
|
1450
1563
|
});
|
|
1451
|
-
const data = await response.json()
|
|
1564
|
+
const data = await response.json() as Array<{
|
|
1565
|
+
t: number;
|
|
1566
|
+
T: number;
|
|
1567
|
+
s: string;
|
|
1568
|
+
i: string;
|
|
1569
|
+
o: string;
|
|
1570
|
+
c: string;
|
|
1571
|
+
h: string;
|
|
1572
|
+
l: string;
|
|
1573
|
+
v: string;
|
|
1574
|
+
n: number;
|
|
1575
|
+
}>;
|
|
1452
1576
|
this.log('candleSnapshot response length:', data?.length);
|
|
1453
1577
|
return data;
|
|
1454
1578
|
}
|
|
@@ -1480,7 +1604,12 @@ export class HyperliquidClient {
|
|
|
1480
1604
|
headers: { 'Content-Type': 'application/json' },
|
|
1481
1605
|
body: JSON.stringify(body),
|
|
1482
1606
|
});
|
|
1483
|
-
const data = await response.json()
|
|
1607
|
+
const data = await response.json() as Array<{
|
|
1608
|
+
coin: string;
|
|
1609
|
+
fundingRate: string;
|
|
1610
|
+
premium: string;
|
|
1611
|
+
time: number;
|
|
1612
|
+
}>;
|
|
1484
1613
|
this.log('fundingHistory response length:', data?.length);
|
|
1485
1614
|
return data;
|
|
1486
1615
|
}
|
|
@@ -1510,7 +1639,15 @@ export class HyperliquidClient {
|
|
|
1510
1639
|
headers: { 'Content-Type': 'application/json' },
|
|
1511
1640
|
body: JSON.stringify(body),
|
|
1512
1641
|
});
|
|
1513
|
-
const data = await response.json()
|
|
1642
|
+
const data = await response.json() as Array<{
|
|
1643
|
+
coin: string;
|
|
1644
|
+
side: 'B' | 'A';
|
|
1645
|
+
px: string;
|
|
1646
|
+
sz: string;
|
|
1647
|
+
time: number;
|
|
1648
|
+
hash: string;
|
|
1649
|
+
tid: number;
|
|
1650
|
+
}>;
|
|
1514
1651
|
this.log('recentTrades response length:', data?.length);
|
|
1515
1652
|
return data;
|
|
1516
1653
|
}
|
|
@@ -1537,7 +1674,12 @@ export class HyperliquidClient {
|
|
|
1537
1674
|
user: user ?? this.address,
|
|
1538
1675
|
}),
|
|
1539
1676
|
});
|
|
1540
|
-
const data = await response.json()
|
|
1677
|
+
const data = await response.json() as {
|
|
1678
|
+
cumVlm: string;
|
|
1679
|
+
nRequestsUsed: number;
|
|
1680
|
+
nRequestsCap: number;
|
|
1681
|
+
nRequestsSurplus: number;
|
|
1682
|
+
};
|
|
1541
1683
|
this.log('userRateLimit response:', JSON.stringify(data));
|
|
1542
1684
|
return data;
|
|
1543
1685
|
}
|
|
@@ -2137,7 +2279,9 @@ export class HyperliquidClient {
|
|
|
2137
2279
|
// Use the exact spot market key from spotMeta (e.g. "@230", "PURR/USDC").
|
|
2138
2280
|
// On testnet the tradable asset id and displayed market key can diverge.
|
|
2139
2281
|
const mids = await this.getAllMids();
|
|
2140
|
-
|
|
2282
|
+
// Record<string, string> lookup returns `string` under TS defaults but
|
|
2283
|
+
// is runtime-undefined when the key is absent — hence the explicit union.
|
|
2284
|
+
let midStr: string | undefined = mids[spotCoinKey];
|
|
2141
2285
|
|
|
2142
2286
|
// Fallback: allMids may omit spot pairs (especially on testnet).
|
|
2143
2287
|
// Try spotMetaAndAssetCtxs which returns markPx directly.
|
|
@@ -2295,7 +2439,7 @@ export class HyperliquidClient {
|
|
|
2295
2439
|
await this.getMetaAndAssetCtxs();
|
|
2296
2440
|
|
|
2297
2441
|
if (leverage) {
|
|
2298
|
-
await this.
|
|
2442
|
+
await this.updateLeverage(coin, leverage);
|
|
2299
2443
|
}
|
|
2300
2444
|
|
|
2301
2445
|
const assetIndex = this.getAssetIndex(coin);
|
package/scripts/core/types.ts
CHANGED
|
@@ -81,11 +81,18 @@ export interface CancelResponse {
|
|
|
81
81
|
|
|
82
82
|
// ============ Market Data Types ============
|
|
83
83
|
|
|
84
|
+
// Shape matches the upstream @nktkas/hyperliquid SDK response. Fields like
|
|
85
|
+
// `onlyIsolated` and `premium` are absent-or-null in practice for some assets,
|
|
86
|
+
// so they're modelled as optional / nullable. Index signature absorbs any
|
|
87
|
+
// additional SDK fields we don't consume here (marginTableId, growthMode, …).
|
|
84
88
|
export interface AssetMeta {
|
|
85
89
|
name: string;
|
|
86
90
|
szDecimals: number;
|
|
87
91
|
maxLeverage: number;
|
|
88
|
-
onlyIsolated
|
|
92
|
+
onlyIsolated?: boolean;
|
|
93
|
+
isDelisted?: boolean;
|
|
94
|
+
marginTableId?: number;
|
|
95
|
+
[key: string]: unknown;
|
|
89
96
|
}
|
|
90
97
|
|
|
91
98
|
export interface AssetCtx {
|
|
@@ -93,11 +100,13 @@ export interface AssetCtx {
|
|
|
93
100
|
openInterest: string;
|
|
94
101
|
prevDayPx: string;
|
|
95
102
|
dayNtlVlm: string;
|
|
96
|
-
premium: string;
|
|
103
|
+
premium: string | null;
|
|
97
104
|
oraclePx: string;
|
|
98
105
|
markPx: string;
|
|
99
|
-
midPx?: string;
|
|
100
|
-
impactPxs?: [string, string];
|
|
106
|
+
midPx?: string | null;
|
|
107
|
+
impactPxs?: [string, string] | string[] | null;
|
|
108
|
+
dayBaseVlm?: string;
|
|
109
|
+
[key: string]: unknown;
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
export interface Meta {
|
package/scripts/info/funding.ts
CHANGED
|
@@ -39,7 +39,7 @@ async function main() {
|
|
|
39
39
|
|
|
40
40
|
const hourlyRate = parseFloat(ctx.funding);
|
|
41
41
|
const annualizedRate = annualizeFundingRate(hourlyRate);
|
|
42
|
-
const premium = parseFloat(ctx.premium);
|
|
42
|
+
const premium = parseFloat(ctx.premium ?? '0');
|
|
43
43
|
const openInterest = parseFloat(ctx.openInterest);
|
|
44
44
|
const markPx = parseFloat(ctx.markPx);
|
|
45
45
|
|
|
@@ -129,7 +129,7 @@ async function main() {
|
|
|
129
129
|
console.log('\nResult');
|
|
130
130
|
console.log('------');
|
|
131
131
|
|
|
132
|
-
if (response.status === 'ok' && response.response) {
|
|
132
|
+
if (response.status === 'ok' && response.response && typeof response.response !== 'string') {
|
|
133
133
|
const statuses = response.response.data.statuses;
|
|
134
134
|
for (const status of statuses) {
|
|
135
135
|
if (status.filled) {
|
|
@@ -49,11 +49,13 @@ async function main() {
|
|
|
49
49
|
|
|
50
50
|
const response = await client.twapCancel(coin, twapId);
|
|
51
51
|
|
|
52
|
+
// SDK returns a success-only response shape; failures throw and are
|
|
53
|
+
// caught by the surrounding try/catch. `status` is the success literal.
|
|
52
54
|
const status = response.response.data.status;
|
|
53
55
|
if (typeof status === 'string' && status === 'success') {
|
|
54
56
|
console.log(`\nTWAP order ${twapId} cancelled successfully.`);
|
|
55
|
-
} else
|
|
56
|
-
console.error(`\
|
|
57
|
+
} else {
|
|
58
|
+
console.error(`\nUnexpected TWAP cancel response status: ${JSON.stringify(status)}`);
|
|
57
59
|
process.exit(1);
|
|
58
60
|
}
|
|
59
61
|
} catch (error) {
|
|
@@ -123,17 +123,15 @@ async function main() {
|
|
|
123
123
|
leverage,
|
|
124
124
|
);
|
|
125
125
|
|
|
126
|
+
// SDK's TwapOrderSuccessResponse excludes the error variant — the SDK
|
|
127
|
+
// throws on failure and the surrounding try/catch handles that. So
|
|
128
|
+
// `status` only ever carries `{ running }` here.
|
|
126
129
|
const status = response.response.data.status;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
console.log(`To check status: openbroker twap-status`);
|
|
133
|
-
} else if ('error' in status) {
|
|
134
|
-
console.error(`TWAP order failed: ${status.error}`);
|
|
135
|
-
process.exit(1);
|
|
136
|
-
}
|
|
130
|
+
console.log(`TWAP order placed successfully!`);
|
|
131
|
+
console.log(`TWAP ID: ${status.running.twapId}`);
|
|
132
|
+
console.log(`\nThe exchange is now executing your TWAP order over ${formatDuration(durationMinutes * 60)}.`);
|
|
133
|
+
console.log(`To cancel: openbroker twap-cancel --coin ${coin} --twap-id ${status.running.twapId}`);
|
|
134
|
+
console.log(`To check status: openbroker twap-status`);
|
|
137
135
|
} catch (error) {
|
|
138
136
|
console.error('Error:', error instanceof Error ? error.message : error);
|
|
139
137
|
process.exit(1);
|
package/scripts/plugin/tools.ts
CHANGED
|
@@ -1385,24 +1385,21 @@ export function createTools(watcherOrCtx: PositionWatcher | null | ToolsContext)
|
|
|
1385
1385
|
const midPrice = parseFloat(mids[coin]);
|
|
1386
1386
|
|
|
1387
1387
|
const response = await client.twapOrder(coin, isBuy, size, durationMinutes, randomize, reduceOnly, leverage);
|
|
1388
|
+
// SDK's TwapOrderSuccessResponse excludes errors; they'd throw and
|
|
1389
|
+
// be caught below. Status is always `{ running }` on this branch.
|
|
1388
1390
|
const status = response.response.data.status;
|
|
1389
1391
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
});
|
|
1402
|
-
} else if ('error' in status) {
|
|
1403
|
-
return error(status.error);
|
|
1404
|
-
}
|
|
1405
|
-
return error('Unexpected response');
|
|
1392
|
+
return json({
|
|
1393
|
+
twapId: status.running.twapId,
|
|
1394
|
+
coin,
|
|
1395
|
+
side: isBuy ? 'buy' : 'sell',
|
|
1396
|
+
size,
|
|
1397
|
+
durationMinutes,
|
|
1398
|
+
randomize,
|
|
1399
|
+
reduceOnly,
|
|
1400
|
+
estimatedNotional: midPrice ? midPrice * size : undefined,
|
|
1401
|
+
midPrice: midPrice || undefined,
|
|
1402
|
+
});
|
|
1406
1403
|
} catch (err) {
|
|
1407
1404
|
return error(err instanceof Error ? err.message : String(err));
|
|
1408
1405
|
}
|
|
@@ -1428,14 +1425,13 @@ export function createTools(watcherOrCtx: PositionWatcher | null | ToolsContext)
|
|
|
1428
1425
|
try {
|
|
1429
1426
|
const client = getClient();
|
|
1430
1427
|
const response = await client.twapCancel(coin, twapId);
|
|
1428
|
+
// SDK returns the success-only response; failures throw.
|
|
1431
1429
|
const status = response.response.data.status;
|
|
1432
1430
|
|
|
1433
1431
|
if (typeof status === 'string' && status === 'success') {
|
|
1434
1432
|
return json({ cancelled: true, coin, twapId });
|
|
1435
|
-
} else if (typeof status === 'object' && 'error' in status) {
|
|
1436
|
-
return error(status.error);
|
|
1437
1433
|
}
|
|
1438
|
-
return error(
|
|
1434
|
+
return error(`Unexpected TWAP cancel status: ${JSON.stringify(status)}`);
|
|
1439
1435
|
} catch (err) {
|
|
1440
1436
|
return error(err instanceof Error ? err.message : String(err));
|
|
1441
1437
|
}
|
package/scripts/setup/onboard.ts
CHANGED
|
@@ -387,8 +387,10 @@ Examples:
|
|
|
387
387
|
return setupApiWallet();
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
-
// Options 1 & 2: Master wallet flow
|
|
391
|
-
|
|
390
|
+
// Options 1 & 2: Master wallet flow. Initialised in the branches below —
|
|
391
|
+
// the compiler can't prove the while-loop in option 2 assigns, so we use
|
|
392
|
+
// `| undefined` and narrow at use sites (`assertDefined`).
|
|
393
|
+
let privateKey: `0x${string}` | undefined;
|
|
392
394
|
|
|
393
395
|
if (choice === '2') {
|
|
394
396
|
// User has existing key
|
|
@@ -417,6 +419,10 @@ Examples:
|
|
|
417
419
|
console.log('✅ New wallet created');
|
|
418
420
|
}
|
|
419
421
|
|
|
422
|
+
if (!privateKey) {
|
|
423
|
+
throw new Error('Internal error: privateKey was not set by onboarding flow.');
|
|
424
|
+
}
|
|
425
|
+
|
|
420
426
|
// Derive account from private key
|
|
421
427
|
const account = privateKeyToAccount(privateKey);
|
|
422
428
|
console.log(`\nWallet Address: ${account.address}\n`);
|